Compare commits

...
Author SHA1 Message Date
Aiden Cline 8f93e2826c refactor(codemode): give wrappers a kind and share host wrapping 2026-09-14 18:06:58 -05:00
Aiden Cline 00a2fc97f7 feat(codemode): cross Set, RegExp, and URLSearchParams to the host in a useful form (#49065) 2026-09-14 17:32:42 -05:00
Aiden Cline a54f2c12a8 test(codemode): follow the experimental instruction route rename (#49067) 2026-09-14 17:32:12 -05:00
Dax Raad 648d01eec7 refactor(protocol): normalize session resources 2026-09-14 17:51:48 -04:00
opencode-agent[bot]andrekram1-node cdee29a821 test(tui): update mini transport contract expectations (#49058)
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2026-09-14 16:37:13 -05:00
Aiden Cline 2b855fccb2 test(codemode): drop the slow split and matchAll cap cases (#49051) 2026-09-14 16:24:27 -05:00
opencode-agent[bot]andrekram1-node 4fa6ca00e5 fix(session-ui): label web search providers dynamically (#49045)
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2026-09-14 15:59:58 -05:00
Aiden Cline 76594882b4 fix(codemode): bound what one built-in can build with string, array, and promise caps (#49019) 2026-09-14 12:54:02 -05:00
Dax Raad 75f046212f refactor(protocol): mark transient session controls 2026-09-14 13:46:04 -04:00
Dax Raad 6bc60caab9 refactor(session): rename interrupt resume option 2026-09-14 13:33:03 -04:00
Aiden Cline 3468a092c9 feat(codemode): expose host classes and functions through extensions (#48941) 2026-09-14 12:29:00 -05:00
Dax Raad 60b88daddf fix(tui): show latest shell output 2026-09-14 13:03:10 -04:00
Dax Raad 8cf4e6b9b4 refactor(protocol): simplify session actions 2026-09-14 13:00:08 -04:00
Shoubhit Dash fee337c462 refactor(tui): anchor turn token usage on idle markers (#49001) 2026-09-14 21:35:01 +05:30
Shoubhit Dash 8378f6caa9 test(core): avoid stdin race in check-ignore flake test (#48995) 2026-09-14 20:31:19 +05:30
Shoubhit Dash 94804423f4 fix(core): match versioned usage event type in stats (#48997) 2026-09-14 20:30:13 +05:30
Dax c62d2e8336 refactor(core): split provider and model registries (#48901) 2026-09-14 10:34:11 -04:00
OpeOginniandShoubhit Dash f64ce6aad4 fix(tui): preserve form drafts across tabs (#48952)
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
2026-09-14 19:58:35 +05:30
Shoubhit Dash dac296f963 feat: preserve prompt cache across effort switches (#48513) 2026-09-14 18:35:46 +05:30
OpeOginni 55e1b3eb17 feat(tui): add inline home footer slot (#48798) 2026-09-14 17:34:03 +05:30
opencode-agent[bot]andBrendonovich f1149efee7 fix(session-ui): group file tool diffs (#48932)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-09-14 15:37:56 +08:00
Aiden Cline 0ee9f61e3d fix(tui): allow toggling several MCP servers at once (#48940) 2026-09-14 02:28:25 -05:00
Aiden Cline 41df55ac15 feat(core): carry the reason on MCP needs_auth status (#48939) 2026-09-14 02:09:29 -05:00
opencode-agent[bot] 8fc49da6eb chore: update nix node_modules hashes 2026-09-14 07:02:13 +00:00
Aiden Cline cbe8823671 feat(core): rework MCP client for SDK v2 and the 2026-07-28 revision (#48937) 2026-09-14 01:42:53 -05:00
Mohammad AandAiden Cline 6e43875123 fix(nix): enable Wayland clipboard images (#48928)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-09-14 01:00:04 -05:00
ReStranger ce380fba29 fix(nix): install opencode and keep opencode2 alias (#48662) 2026-09-14 00:59:22 -05:00
Aiden Cline 3045156fa6 fix(codemode): strip __proto__ keys at host object boundaries (#48926) 2026-09-14 00:49:42 -05:00
Luke Parker 0a3dc16961 fix(app): shorten context usage label (#48927) 2026-09-14 05:36:08 +00:00
Aiden Cline 1d4d9b5fa5 fix(ai): preserve OpenAI Chat image URLs (#48862) 2026-09-13 23:43:39 -05:00
Aiden Cline 752d980770 fix(ai): map Vertex service tier to header (#48886) 2026-09-13 23:43:16 -05:00
Aiden Cline 07225172df fix(ai): serialize undefined historical tool input (#48863) 2026-09-13 23:43:07 -05:00
Aiden Cline 223b0271e6 test: align platform and endpoint expectations (#48916) 2026-09-14 04:25:34 +00:00
Luke Parker 93a37958a7 fix(session-ui): remove sticky patch header gap (#48909) 2026-09-14 13:41:07 +10:00
Dax Raad 40f4778296 refactor(protocol): refine audited endpoints 2026-09-13 23:10:37 -04:00
Dax e9bb6d490b refactor(tui): remove terminal pane setting 2026-09-13 23:06:13 -04:00
Aiden Cline 1e697962bd fix: restore required typecheck status (#48913) 2026-09-13 22:03:22 -05:00
Aiden Cline 4418df5d62 test(app): align worker status URL (#48911) 2026-09-13 21:55:47 -05:00
Aiden Cline 052be04466 test: align coverage with v2 refactors (#48900) 2026-09-13 21:44:49 -05:00
Aiden Cline cd3a64b225 test: align status and shell expectations (#48906) 2026-09-13 21:32:42 -05:00
Dax Raad 48c9a0a8de feat(release): add interactive review 2026-09-13 22:22:27 -04:00
Aiden Cline 48875190ef feat(codemode): fail runaway recursion with a RangeError at 10000 nested calls (#48891) 2026-09-13 21:04:36 -05:00
opencode-agent[bot]andHona 476432de1e fix(core): restore Windows Git fast path (#48879)
Co-authored-by: Hona <10430890+Hona@users.noreply.github.com>
2026-09-14 12:03:03 +10:00
Luke Parker a71bb4d38c fix(app): match notice updates to used label styling (#48895) 2026-09-14 12:02:16 +10:00
Aiden Cline 5d3019e5a1 refactor(codemode): materialize interpreter failures once and locate them at the call boundary (#48770) 2026-09-13 20:19:31 -05:00
336 changed files with 9587 additions and 6358 deletions
+1
View File
@@ -9,6 +9,7 @@ on:
jobs:
check:
name: typecheck
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout repository
+1
View File
@@ -32,6 +32,7 @@ target
# Local dev files
opencode-dev
UPCOMING_CHANGELOG.md
RELEASE_REVIEW.md
logs/
*.bun-build
tsconfig.tsbuildinfo
+61 -61
View File
@@ -111,7 +111,7 @@ Review endpoints in document order. For each endpoint, select one disposition an
|---|---|---|---|---|---|
| [x] 025 | `GET` | `/api/integration` | `integration.list` | Keep | Full integration inventory is consumed by authentication and integration-selection clients. |
| [x] 026 | `GET` | `/api/integration/{integrationID}` | `integration.get` | Change | Missing integration now returns typed `404` instead of optional data. |
| [ ] 027 | `POST` | `/api/experimental/integration/wellknown` | `experimental.integration.wellknown.add` | | |
| [x] | `POST` | `/api/experimental/integration/wellknown` | `experimental.integration.wellknown.add` | Experimental-only | Retained outside the stable API commitment. |
| [ ] 028 | `POST` | `/api/integration/{integrationID}/connect/key` | `integration.connect.key` | | |
| [ ] 029 | `POST` | `/api/integration/{integrationID}/connect/oauth` | `integration.oauth.connect` | | |
| [ ] 030 | `GET` | `/api/integration/{integrationID}/connect/oauth/{attemptID}` | `integration.oauth.status` | | |
@@ -120,17 +120,17 @@ Review endpoints in document order. For each endpoint, select one disposition an
| [ ] 033 | `POST` | `/api/integration/{integrationID}/connect/command` | `integration.command.connect` | | |
| [ ] 034 | `GET` | `/api/integration/{integrationID}/connect/command/{attemptID}` | `integration.command.status` | | |
| [ ] 035 | `DELETE` | `/api/integration/{integrationID}/connect/command/{attemptID}` | `integration.command.cancel` | | |
| [ ] 036 | `GET` | `/api/mcp` | `mcp.list` | | |
| [ ] 037 | `PUT` | `/api/mcp/{server}` | `mcp.add` | | |
| [ ] 038 | `DELETE` | `/api/mcp/{server}` | `mcp.remove` | | |
| [ ] 039 | `POST` | `/api/mcp/{server}/connect` | `mcp.connect` | | |
| [ ] 040 | `POST` | `/api/mcp/{server}/disconnect` | `mcp.disconnect` | | |
| [ ] 041 | `GET` | `/api/mcp/resource` | `mcp.resource.catalog` | | |
| [ ] 042 | `PATCH` | `/api/credential/{credentialID}` | `credential.update` | | |
| [ ] 043 | `DELETE` | `/api/credential/{credentialID}` | `credential.remove` | | |
| [ ] 044 | `POST` | `/api/credential/{credentialID}/activate` | `credential.activate` | | |
| [ ] 045 | `GET` | `/api/websearch/provider` | `websearch.providers` | | |
| [ ] 046 | `POST` | `/api/websearch` | `websearch.query` | | |
| [x] 036 | `GET` | `/api/mcp` | `mcp.list` | Keep | MCP inventory and connection status retained. |
| [x] 037 | `PUT` | `/api/experimental/mcp/{server}` | `experimental.mcp.add` | Experimental-only | Runtime-only MCP override; does not persist configuration. |
| [x] 038 | `DELETE` | `/api/experimental/mcp/{server}` | `experimental.mcp.remove` | Experimental-only | Runtime removal override; missing server returns `404`. |
| [x] 039 | `POST` | `/api/experimental/mcp/{server}/connect` | `experimental.mcp.connect` | Experimental-only | Runtime connection override retained outside the stable API. |
| [x] 040 | `POST` | `/api/experimental/mcp/{server}/disconnect` | `experimental.mcp.disconnect` | Experimental-only | Runtime disconnection override retained outside the stable API. |
| [ ] 041 | `GET` | `/api/mcp/resource` | `mcp.resource.catalog` | | Deferred for later review. |
| [x] 042 | `PATCH` | `/api/credential/{credentialID}` | `credential.update` | Change | Removed redundant location query; credentials and events are global. |
| [x] 043 | `DELETE` | `/api/credential/{credentialID}` | `credential.remove` | Change | Removed redundant location query; credentials and events are global. |
| [x] 044 | `POST` | `/api/credential/{credentialID}/activate` | `credential.activate` | Change | Removed redundant location query; credentials and events are global. |
| [x] 045 | `GET` | `/api/websearch/provider` | `websearch.providers` | Keep | Provider availability remains location-scoped; singular resource path retained. |
| [x] 046 | `POST` | `/api/websearch` | `websearch.query` | Keep | Unknown provider remains an invalid request; published time documented as Unix epoch milliseconds. |
## Group 4: Session lifecycle
@@ -138,18 +138,18 @@ Review endpoints in document order. For each endpoint, select one disposition an
| Done | Method | Path | Operation ID | Decision | Notes |
|---|---|---|---|---|---|
| [ ] 047 | `GET` | `/api/session` | `session.list` | | |
| [ ] 048 | `POST` | `/api/session` | `session.create` | | |
| [ ] 049 | `GET` | `/api/session/stats` | `session.stats` | | |
| [ ] 050 | `GET` | `/api/session/active` | `session.active` | | |
| [ ] 051 | `GET` | `/api/session/{sessionID}` | `session.get` | | |
| [ ] 052 | `DELETE` | `/api/session/{sessionID}` | `session.remove` | | |
| [ ] 053 | `POST` | `/api/session/{sessionID}/fork` | `session.fork` | | |
| [ ] 054 | `POST` | `/api/session/{sessionID}/agent` | `session.switchAgent` | | |
| [ ] 055 | `POST` | `/api/session/{sessionID}/model` | `session.switchModel` | | |
| [ ] 056 | `POST` | `/api/session/{sessionID}/rename` | `session.rename` | | |
| [ ] 057 | `POST` | `/api/session/{sessionID}/move` | `session.move` | | |
| [ ] 058 | `POST` | `/api/session/{sessionID}/background` | `session.background` | | |
| [x] 047 | `GET` | `/api/session` | `session.list` | Keep | Existing filtering, ordering, and cursor contract retained for now. |
| [x] 048 | `POST` | `/api/session` | `session.create` | Keep | Existing creation contract retained; model reference includes optional variant. |
| [x] 049 | `GET` | `/api/experimental/session/stats` | `experimental.session.stats` | Experimental-only | Session analytics retained outside the stable API commitment. |
| [x] 050 | `GET` | `/api/session/active` | `session.active` | Keep | Status record retained for future active-state expansion. |
| [x] 051 | `GET` | `/api/session/{sessionID}` | `session.get` | Keep | Specific session read and typed `404` retained. |
| [x] 052 | `DELETE` | `/api/session/{sessionID}` | `session.remove` | Keep | Session and child deletion with typed `404` retained. |
| [x] 053 | `POST` | `/api/session/{sessionID}/fork` | `session.fork` | Change | Request now accepts optional branded `before` message ID; omission copies full history. |
| [x] 054 | `POST` | `/api/session/{sessionID}/agent` | `session.switchAgent` | Keep | Subsequent-execution agent selection retained. |
| [x] 055 | `POST` | `/api/session/{sessionID}/model` | `session.switchModel` | Keep | Subsequent-execution model and optional variant selection retained. |
| [x] 056 | `PATCH` | `/api/session/{sessionID}` | `session.rename` | Change | Title-only rename now uses the session resource path. |
| [x] 057 | `POST` | `/api/session/{sessionID}/move` | `session.move` | Change | Removed inaccurate local-change transfer claim; delivery behavior retained. |
| [x] 058 | `POST` | `/api/session/{sessionID}/background` | `session.background` | Keep | Backgroundable foreground tools transition to background observation; idle requests remain no-ops. |
## Group 5: Session execution and inputs
@@ -157,17 +157,17 @@ Review endpoints in document order. For each endpoint, select one disposition an
| Done | Method | Path | Operation ID | Decision | Notes |
|---|---|---|---|---|---|
| [ ] 059 | `POST` | `/api/session/{sessionID}/prompt` | `session.prompt` | | |
| [ ] 060 | `POST` | `/api/session/{sessionID}/command` | `session.command` | | |
| [ ] 061 | `POST` | `/api/session/{sessionID}/skill` | `session.skill` | | |
| [ ] 062 | `POST` | `/api/session/{sessionID}/synthetic` | `session.synthetic` | | |
| [ ] 063 | `POST` | `/api/session/{sessionID}/shell` | `session.shell` | | |
| [ ] 064 | `POST` | `/api/session/{sessionID}/compact` | `session.compact` | | |
| [ ] 065 | `POST` | `/api/session/{sessionID}/wait` | `session.wait` | | |
| [ ] 066 | `POST` | `/api/session/{sessionID}/generate` | `session.generate` | | |
| [ ] 067 | `POST` | `/api/session/{sessionID}/interrupt` | `session.interrupt` | | |
| [ ] 068 | `PUT` | `/api/session/{sessionID}/environment` | `session.environment` | | |
| [ ] 069 | `POST` | `/api/session/{sessionID}/view` | `session.view` | | |
| [x] 059 | `POST` | `/api/session/{sessionID}/prompt` | `session.prompt` | Keep | Durable admission, delivery mode, and admit-only resume control retained. |
| [x] 060 | `POST` | `/api/session/{sessionID}/command` | `session.command` | Change | Renamed request field from `command` to `name`; `204` retained. |
| [x] 061 | `POST` | `/api/experimental/session/{sessionID}/skill` | `experimental.session.skill` | Experimental-only | Skill ID is now the `id` field; standalone activation remains experimental. |
| [x] 062 | `POST` | `/api/session/{sessionID}/synthetic` | `session.synthetic` | Keep | Durable synthetic admission and delivery controls retained. |
| [x] 063 | `POST` | `/api/session/{sessionID}/shell` | `session.shell` | Change | Caller ID is now the optimistic shell message ID; server derives its event ID. |
| [x] 064 | `POST` | `/api/session/{sessionID}/compact` | `session.compact` | Keep | Durable compaction admission and delivery controls retained. |
| [x] 065 | `POST` | `/api/experimental/session/{sessionID}/wait` | `experimental.session.wait` | Experimental-only | Race-free idle barrier retained outside the stable API. |
| [x] 066 | `POST` | `/api/session/{sessionID}/generate` | `session.generate` | Keep | Transient generation from session context retained. |
| [x] 067 | `POST` | `/api/session/{sessionID}/interrupt` | `session.interrupt` | Change | Renamed `continue` to `resume` across public and internal interruption APIs. |
| [x] 068 | `PUT` | `/api/experimental/session/{sessionID}/environment` | `experimental.session.environment` | Experimental-only | Process-local environment replacement retained outside the stable API. |
| [x] 069 | `POST` | `/api/session/{sessionID}/view` | `session.view` | Change | Idle watermark now uses the standard epoch-millisecond timestamp schema. |
## Group 6: Session history and recovery
@@ -175,19 +175,19 @@ Review endpoints in document order. For each endpoint, select one disposition an
| Done | Method | Path | Operation ID | Decision | Notes |
|---|---|---|---|---|---|
| [ ] 070 | `POST` | `/api/session/import` | `session.import` | | |
| [ ] 071 | `GET` | `/api/session/{sessionID}/export` | `session.export` | | |
| [ ] 072 | `POST` | `/api/session/{sessionID}/revert/stage` | `session.revert.stage` | | |
| [ ] 073 | `POST` | `/api/session/{sessionID}/revert/clear` | `session.revert.clear` | | |
| [ ] 074 | `POST` | `/api/session/{sessionID}/revert/commit` | `session.revert.commit` | | |
| [ ] 075 | `GET` | `/api/session/{sessionID}/context` | `session.context` | | |
| [ ] 076 | `GET` | `/api/session/{sessionID}/diff` | `session.diff` | | |
| [ ] 077 | `GET` | `/api/session/{sessionID}/instructions/entries` | `session.instructions.entry.list` | | |
| [ ] 078 | `PUT` | `/api/session/{sessionID}/instructions/entries/{key}` | `session.instructions.entry.put` | | |
| [ ] 079 | `DELETE` | `/api/session/{sessionID}/instructions/entries/{key}` | `session.instructions.entry.remove` | | |
| [ ] 080 | `GET` | `/api/experimental/session/{sessionID}/log` | `session.log` | | |
| [ ] 081 | `GET` | `/api/session/{sessionID}/message/{messageID}` | `session.message` | | |
| [ ] 082 | `GET` | `/api/session/{sessionID}/message` | `message.list` | | |
| [x] 070 | `POST` | `/api/experimental/session/import` | `experimental.session.import` | Experimental-only | Existing projected transcript import contract retained outside the stable API. |
| [x] 071 | `GET` | `/api/experimental/session/{sessionID}/export` | `experimental.session.export` | Experimental-only | Existing projected transcript export contract retained outside the stable API. |
| [x] 072 | `POST` | `/api/session/{sessionID}/revert/stage` | `session.revert.stage` | Keep | Existing staged history and optional file restoration behavior retained. |
| [x] 073 | `DELETE` | `/api/session/{sessionID}/revert` | `session.revert.clear` | Change | Clearing staged revert now deletes the session revert resource. |
| [x] 074 | `POST` | `/api/session/{sessionID}/revert/commit` | `session.revert.commit` | Keep | Explicit staged-revert commit action retained. |
| [x] 075 | `GET` | `/api/session/{sessionID}/context` | `session.context` | Keep | Active model-context projection retained. |
| [x] 076 | `GET` | `/api/session/{sessionID}/diff` | `session.diff` | Keep | Turn-range structured diff contract retained. |
| [x] 077 | `GET` | `/api/experimental/session/{sessionID}/instructions/entries` | `experimental.session.instructions.entry.list` | Experimental-only | API-managed durable context entries retained outside the stable API. |
| [x] 078 | `PUT` | `/api/experimental/session/{sessionID}/instructions/entries/{key}` | `experimental.session.instructions.entry.put` | Experimental-only | API-managed durable context entries retained outside the stable API. |
| [x] 079 | `DELETE` | `/api/experimental/session/{sessionID}/instructions/entries/{key}` | `experimental.session.instructions.entry.remove` | Experimental-only | API-managed durable context entries retained outside the stable API. |
| [x] | `GET` | `/api/experimental/session/{sessionID}/log` | `session.log` | Experimental-only | Retained outside the stable API commitment. |
| [x] 081 | `GET` | `/api/session/{sessionID}/message/{messageID}` | `session.message.get` | Change | Normalized specific-message operation ID. |
| [x] 082 | `GET` | `/api/session/{sessionID}/message` | `session.message.list` | Change | Normalized session-scoped message-list operation ID. |
## Group 7: Inbox, permissions, and forms
@@ -195,7 +195,7 @@ Review endpoints in document order. For each endpoint, select one disposition an
| Done | Method | Path | Operation ID | Decision | Notes |
|---|---|---|---|---|---|
| [ ] 083 | `GET` | `/api/session/{sessionID}/inbox` | `session.inbox.list` | | |
| [x] 083 | `GET` | `/api/session/{sessionID}/inbox` | `session.inbox.list` | Change | Inbox timestamps now use the standard nested `time.created` shape. |
| [ ] 084 | `DELETE` | `/api/session/{sessionID}/inbox/{inboxID}` | `session.inbox.cancel` | | |
| [ ] 085 | `POST` | `/api/session/{sessionID}/inbox/{inboxID}/steer` | `session.inbox.steer` | | |
| [ ] 086 | `POST` | `/api/session/{sessionID}/inbox/{inboxID}/queue` | `session.inbox.queue` | | |
@@ -247,17 +247,17 @@ Review endpoints in document order. For each endpoint, select one disposition an
| [ ] 118 | `DELETE` | `/api/pty/{ptyID}` | `pty.remove` | | |
| [ ] 119 | `POST` | `/api/pty/{ptyID}/connect-token` | `pty.connect.token` | | |
| [ ] 120 | `GET` | `/api/pty/{ptyID}/connect` | `pty.connect` | | |
| [ ] 121 | `GET` | `/api/experimental/session/{sessionID}/terminal/read` | `server.experimental.persistentPty.read` | | |
| [ ] 122 | `GET` | `/api/experimental/session/{sessionID}/terminal` | `server.experimental.persistentPty.list` | | |
| [ ] 123 | `POST` | `/api/experimental/session/{sessionID}/terminal` | `server.experimental.persistentPty.create` | | |
| [ ] 124 | `POST` | `/api/experimental/persistent-pty/shutdown` | `server.experimental.persistentPty.shutdown` | | |
| [ ] 125 | `POST` | `/api/experimental/persistent-pty/handoff` | `server.experimental.persistentPty.handoff` | | |
| [ ] 126 | `GET` | `/api/experimental/persistent-pty/{ptyID}` | `server.experimental.persistentPty.get` | | |
| [ ] 127 | `PUT` | `/api/experimental/persistent-pty/{ptyID}` | `server.experimental.persistentPty.update` | | |
| [ ] 128 | `DELETE` | `/api/experimental/persistent-pty/{ptyID}` | `server.experimental.persistentPty.remove` | | |
| [ ] 129 | `GET` | `/api/experimental/persistent-pty/{ptyID}/snapshot` | `server.experimental.persistentPty.snapshot` | | |
| [ ] 130 | `POST` | `/api/experimental/persistent-pty/{ptyID}/connect-token` | `server.experimental.persistentPty.connectToken` | | |
| [ ] 131 | `GET` | `/api/experimental/persistent-pty/{ptyID}/connect` | `persistentPty.connect` | | |
| [x] | `GET` | `/api/experimental/session/{sessionID}/terminal/read` | `server.experimental.persistentPty.read` | Experimental-only | Retained outside the stable API commitment. |
| [x] | `GET` | `/api/experimental/session/{sessionID}/terminal` | `server.experimental.persistentPty.list` | Experimental-only | Retained outside the stable API commitment. |
| [x] | `POST` | `/api/experimental/session/{sessionID}/terminal` | `server.experimental.persistentPty.create` | Experimental-only | Retained outside the stable API commitment. |
| [x] | `POST` | `/api/experimental/persistent-pty/shutdown` | `server.experimental.persistentPty.shutdown` | Experimental-only | Retained outside the stable API commitment. |
| [x] | `POST` | `/api/experimental/persistent-pty/handoff` | `server.experimental.persistentPty.handoff` | Experimental-only | Retained outside the stable API commitment. |
| [x] | `GET` | `/api/experimental/persistent-pty/{ptyID}` | `server.experimental.persistentPty.get` | Experimental-only | Retained outside the stable API commitment. |
| [x] | `PUT` | `/api/experimental/persistent-pty/{ptyID}` | `server.experimental.persistentPty.update` | Experimental-only | Retained outside the stable API commitment. |
| [x] | `DELETE` | `/api/experimental/persistent-pty/{ptyID}` | `server.experimental.persistentPty.remove` | Experimental-only | Retained outside the stable API commitment. |
| [x] | `GET` | `/api/experimental/persistent-pty/{ptyID}/snapshot` | `server.experimental.persistentPty.snapshot` | Experimental-only | Retained outside the stable API commitment. |
| [x] | `POST` | `/api/experimental/persistent-pty/{ptyID}/connect-token` | `server.experimental.persistentPty.connectToken` | Experimental-only | Retained outside the stable API commitment. |
| [x] | `GET` | `/api/experimental/persistent-pty/{ptyID}/connect` | `persistentPty.connect` | Experimental-only | Retained outside the stable API commitment. |
| [ ] 132 | `GET` | `/api/shell` | `shell.list` | | |
| [ ] 133 | `POST` | `/api/shell` | `shell.create` | | |
| [ ] 134 | `GET` | `/api/shell/{id}` | `shell.get` | | |
@@ -276,4 +276,4 @@ Review endpoints in document order. For each endpoint, select one disposition an
| [ ] 140 | `GET` | `/api/event` | `event.subscribe` | | |
| [ ] 141 | `GET` | `/api/debug/location` | `debug.location.list` | | |
| [ ] 142 | `DELETE` | `/api/debug/location` | `debug.location.evict` | | |
| [ ] 143 | `GET` | `/api/experimental/migration/v1` | `experimental.migration.v1.status` | | |
| [x] | `GET` | `/api/experimental/migration/v1` | `experimental.migration.v1.status` | Experimental-only | Retained outside the stable API commitment. |
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

+16 -102
View File
@@ -14,6 +14,7 @@
"devDependencies": {
"@actions/artifact": "5.0.1",
"@ast-grep/cli": "0.44.0",
"@opencode/client": "workspace:*",
"@tsconfig/bun": "catalog:",
"@types/mime-types": "3.0.1",
"@types/react": "19.2.17",
@@ -357,7 +358,7 @@
"@ff-labs/fff-bun": "0.10.5",
"@ff-labs/fff-node": "0.10.5",
"@lydell/node-pty": "catalog:",
"@modelcontextprotocol/sdk": "1.29.0",
"@modelcontextprotocol/client": "2.0.0",
"@opencode-ai/pty": "0.1.13",
"@opencode/ai": "workspace:*",
"@opencode/codemode": "workspace:*",
@@ -394,6 +395,7 @@
"devDependencies": {
"@effect/platform-node": "catalog:",
"@effect/sql-sqlite-bun": "catalog:",
"@modelcontextprotocol/server": "2.0.0",
"@opencode/http-recorder": "workspace:*",
"@parcel/watcher-darwin-arm64": "2.5.1",
"@parcel/watcher-darwin-x64": "2.5.1",
@@ -1081,8 +1083,8 @@
"patchedDependencies": {
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
"@tanstack/virtual-core@3.17.8": "patches/@tanstack%2Fvirtual-core@3.17.8.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"ghostty-web@github:anomalyco/ghostty-web#83c0a07": "patches/ghostty-web@0.3.0.patch",
"@modelcontextprotocol/client@2.0.0": "patches/@modelcontextprotocol%2Fclient@2.0.0.patch",
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
"@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch",
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
@@ -1877,8 +1879,6 @@
"@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="],
"@hono/node-server": ["@hono/node-server@1.19.17", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ=="],
"@hono/standard-validator": ["@hono/standard-validator@0.2.0", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-pFq0UVAnjzXcDAgqFpDeVL3MOUPrlIh/kPqBDvbCYoThVhhS+Vf37VcdsakdOFFGiqoiYVxp3LifXFhGhp/rgQ=="],
"@ibm/plex": ["@ibm/plex@6.4.1", "", { "dependencies": { "@ibm/telemetry-js": "^1.5.1" } }, "sha512-fnsipQywHt3zWvsnlyYKMikcVI7E2fEwpiPnIHFqlbByXVfQfANAAeJk1IV4mNnxhppUIDlhU0TzwYwL++Rn2g=="],
@@ -2037,7 +2037,11 @@
"@mermaid-js/parser": ["@mermaid-js/parser@1.2.1", "", { "dependencies": { "@chevrotain/types": "~11.1.2" } }, "sha512-n12NohV3mrUyUL2o93IgG/ifeW9FTyeJn3zDxkhwa8MJ9Fxg3HQMlA3RiGmD/3UnJvheztkjjQAjA2T4LmUcpw=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
"@modelcontextprotocol/client": ["@modelcontextprotocol/client@2.0.0", "", { "dependencies": { "@modelcontextprotocol/core": "2.0.0", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "jose": "^6.1.3", "pkce-challenge": "^5.0.0", "zod": "^4.2.0" } }, "sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw=="],
"@modelcontextprotocol/core": ["@modelcontextprotocol/core@2.0.0", "", { "dependencies": { "zod": "^4.2.0" } }, "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA=="],
"@modelcontextprotocol/server": ["@modelcontextprotocol/server@2.0.0", "", { "dependencies": { "@modelcontextprotocol/core": "2.0.0", "zod": "^4.2.0" } }, "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw=="],
"@motionone/animation": ["@motionone/animation@10.18.0", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="],
@@ -3363,8 +3367,6 @@
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
@@ -3523,8 +3525,6 @@
"bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
"body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="],
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
"boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="],
@@ -3567,8 +3567,6 @@
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"bytestreamjs": ["bytestreamjs@2.0.1", "", {}, "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ=="],
"cac": ["cac@7.0.0", "", {}, "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ=="],
@@ -3691,9 +3689,7 @@
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
"content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
@@ -3701,16 +3697,12 @@
"cookie-es": ["cookie-es@2.0.1", "", {}, "sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA=="],
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
"core-js": ["core-js@3.50.0", "", {}, "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw=="],
"core-js-compat": ["core-js-compat@3.50.0", "", { "dependencies": { "browserslist": "^4.28.7" } }, "sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q=="],
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
"cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="],
"crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="],
@@ -3863,8 +3855,6 @@
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"deprecation": ["deprecation@2.3.1", "", {}, "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
@@ -3939,8 +3929,6 @@
"editorconfig": ["editorconfig@1.0.7", "", { "dependencies": { "@one-ini/wasm": "0.1.1", "commander": "^10.0.0", "minimatch": "^9.0.1", "semver": "^7.5.3" }, "bin": { "editorconfig": "bin/editorconfig" } }, "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"effect": ["effect@4.0.0-rc.112", "", { "dependencies": { "fast-check": "^4.9.0", "msgpackr": "^2.0.5" } }, "sha512-wXxwuh1Ywnv4cPRM3Wfa0vDwuOHnZ1TsTgHJkG9XgzND6inhBH9n1vBxhg3iIXOia/OrpmvVmd3lrD4vq6bF3A=="],
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
@@ -3979,8 +3967,6 @@
"emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
"engine.io-client": ["engine.io-client@6.6.6", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.21.0", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q=="],
@@ -4033,8 +4019,6 @@
"escape-goat": ["escape-goat@4.0.0", "", {}, "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
@@ -4057,8 +4041,6 @@
"eta": ["eta@4.6.0", "", {}, "sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
@@ -4077,10 +4059,6 @@
"exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
"express-rate-limit": ["express-rate-limit@8.6.2", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A=="],
"expressive-code": ["expressive-code@0.41.7", "", { "dependencies": { "@expressive-code/core": "^0.41.7", "@expressive-code/plugin-frames": "^0.41.7", "@expressive-code/plugin-shiki": "^0.41.7", "@expressive-code/plugin-text-markers": "^0.41.7" } }, "sha512-2wZjC8OQ3TaVEMcBtYY4Va3lo6J+Ai9jf3d4dbhURMJcU4Pbqe6EcHe424MIZI0VHUA1bR6xdpoHYi3yxokWqA=="],
"ext-list": ["ext-list@2.2.2", "", { "dependencies": { "mime-db": "^1.28.0" } }, "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA=="],
@@ -4127,8 +4105,6 @@
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
"find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
@@ -4153,14 +4129,10 @@
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
"framer-motion": ["framer-motion@8.5.5", "", { "dependencies": { "@motionone/dom": "^10.15.3", "hey-listen": "^1.0.8", "tslib": "^2.4.0" }, "optionalDependencies": { "@emotion/is-prop-valid": "^0.8.2" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-5IDx5bxkjWHWUF3CVJoSyUVOtrbAxtzYBBowRE2uYI/6VYhkEBD+rbTHEGuUmbGHRj6YqqSfoG7Aa1cLyWCrBA=="],
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
"fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="],
@@ -4335,8 +4307,6 @@
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"http-link-header": ["http-link-header@1.1.4", "", {}, "sha512-xT3GPW6/ZbGuw4UvwHqErSCEjNUlwbQJuZn9/q5U4WEKfp2kENVCAlousG1zLxHeaQ/ffOHUNpWamvkbBW0eNw=="],
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
@@ -4391,8 +4361,6 @@
"ip-address": ["ip-address@10.5.0", "", {}, "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="],
"is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
@@ -4457,8 +4425,6 @@
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
@@ -4725,12 +4691,8 @@
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
"media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="],
"merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"mermaid": ["mermaid@11.17.2", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.2.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.34.0", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.21", "dompurify": "^3.3.3", "es-toolkit": "^1.45.1", "fastdom": "1.0.12", "katex": "^0.16.47", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-V6K3C8EBdEsPFZXSKMJe6ppQOENxuHARr9GvHX4hh47lAbhMRD9qf4oEK7LoaRQxULMa80/qt5gHO73aCleBBg=="],
@@ -4955,8 +4917,6 @@
"oidc-token-hash": ["oidc-token-hash@5.2.0", "", {}, "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="],
@@ -5027,8 +4987,6 @@
"parseley": ["parseley@0.12.1", "", { "dependencies": { "leac": "^0.6.0", "peberminta": "^0.9.0" } }, "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="],
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
@@ -5151,8 +5109,6 @@
"protobufjs": ["protobufjs@7.6.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
"pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
@@ -5179,10 +5135,6 @@
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
"range-parser": ["range-parser@1.3.0", "", {}, "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw=="],
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
"react": ["react@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="],
"react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
@@ -5337,8 +5289,6 @@
"roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
@@ -5373,8 +5323,6 @@
"semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
"seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="],
"serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="],
@@ -5385,16 +5333,12 @@
"seroval-plugins": ["seroval-plugins@1.5.6", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-HXuLAX2pu/UByPpaeo/TaMfvMIi+1QqIoPJYCcAtU8QkVNwgR6MPlGuCQTErV1JwraaMbYaWVIBX7mppzGLATQ=="],
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
"set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"sharp": ["sharp@0.35.3", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.3", "@img/sharp-darwin-x64": "0.35.3", "@img/sharp-freebsd-wasm32": "0.35.3", "@img/sharp-libvips-darwin-arm64": "1.3.2", "@img/sharp-libvips-darwin-x64": "1.3.2", "@img/sharp-libvips-linux-arm": "1.3.2", "@img/sharp-libvips-linux-arm64": "1.3.2", "@img/sharp-libvips-linux-ppc64": "1.3.2", "@img/sharp-libvips-linux-riscv64": "1.3.2", "@img/sharp-libvips-linux-s390x": "1.3.2", "@img/sharp-libvips-linux-x64": "1.3.2", "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", "@img/sharp-libvips-linuxmusl-x64": "1.3.2", "@img/sharp-linux-arm": "0.35.3", "@img/sharp-linux-arm64": "0.35.3", "@img/sharp-linux-ppc64": "0.35.3", "@img/sharp-linux-riscv64": "0.35.3", "@img/sharp-linux-s390x": "0.35.3", "@img/sharp-linux-x64": "0.35.3", "@img/sharp-linuxmusl-arm64": "0.35.3", "@img/sharp-linuxmusl-x64": "0.35.3", "@img/sharp-webcontainers-wasm32": "0.35.3", "@img/sharp-win32-arm64": "0.35.3", "@img/sharp-win32-ia32": "0.35.3", "@img/sharp-win32-x64": "0.35.3" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
@@ -5519,8 +5463,6 @@
"stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="],
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
@@ -5657,8 +5599,6 @@
"toad-cache": ["toad-cache@3.7.4", "", {}, "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.8", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-b+5ynEFp4Woe5a22hzNQm42lD23t13ZMihVxHbzjA50zdcM9aOSJTIjdJ0PDSd4/50HbBXcpHiQsz6rM4N88ww=="],
"topojson-client": ["topojson-client@3.1.0", "", { "dependencies": { "commander": "2" }, "bin": { "topo2geo": "bin/topo2geo", "topomerge": "bin/topomerge", "topoquantize": "bin/topoquantize" } }, "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw=="],
@@ -5701,8 +5641,6 @@
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="],
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
"typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="],
@@ -5781,8 +5719,6 @@
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
"unstorage": ["unstorage@2.0.0-alpha.7", "", { "peerDependencies": { "@azure/app-configuration": "^1.11.0", "@azure/cosmos": "^4.9.1", "@azure/data-tables": "^13.3.2", "@azure/identity": "^4.13.0", "@azure/keyvault-secrets": "^4.10.0", "@azure/storage-blob": "^12.31.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.13.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.36.2", "@vercel/blob": ">=0.27.3", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "chokidar": "^4 || ^5", "db0": ">=0.3.4", "idb-keyval": "^6.2.2", "ioredis": "^5.9.3", "lru-cache": "^11.2.6", "mongodb": "^6 || ^7", "ofetch": "*", "uploadthing": "^7.7.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "chokidar", "db0", "idb-keyval", "ioredis", "lru-cache", "mongodb", "ofetch", "uploadthing"] }, "sha512-ELPztchk2zgFJnakyodVY3vJWGW9jy//keJ32IOJVGUMyaPydwcA1FtVvWqT0TNRch9H+cMNEGllfVFfScImog=="],
@@ -5813,8 +5749,6 @@
"validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"venice-ai-sdk-provider": ["venice-ai-sdk-provider@2.1.1", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.51", "@ai-sdk/provider": "^3.0.10", "@ai-sdk/provider-utils": "^4.0.30" }, "peerDependencies": { "ai": "^6.0.90" } }, "sha512-w3OHkuzzKZ3r2TOxER6myBYzZJNoDqol+DUHu3NnfBN/GETnUVxecZJab0CHQQ8GZc0jjzpFymepjcLDPS4SQg=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
@@ -6307,9 +6241,13 @@
"@mdx-js/mdx/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
"@modelcontextprotocol/sdk/hono": ["hono@4.13.3", "", {}, "sha512-r8AO2mYHoLxSHkgafNeC/BXyb2vWRxD3jem4Ts+ptav8oTG5FIRifAjuJEmZI4bSvvc2ns0GxmIYiZnHqN3mMw=="],
"@modelcontextprotocol/client/jose": ["jose@6.2.9", "", {}, "sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA=="],
"@modelcontextprotocol/sdk/jose": ["jose@6.2.9", "", {}, "sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA=="],
"@modelcontextprotocol/client/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"@modelcontextprotocol/core/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"@modelcontextprotocol/server/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"@npmcli/arborist/hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="],
@@ -6595,8 +6533,6 @@
"babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
"boxen/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"builder-util/js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="],
@@ -6675,8 +6611,6 @@
"esbuild-plugin-copy/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
"ffi-rs/@yuuang/ffi-rs-darwin-arm64": ["@yuuang/ffi-rs-darwin-arm64@1.3.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OueBlUFBT9IwD9pQnoYs0UszRBEySskfrEPXXfvKfGjL/DXnfn6kUheQ3oIP6sSmshVGNQUwrTCPo6feAa4QjA=="],
@@ -6811,8 +6745,6 @@
"roarr/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
"router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
"safe-array-concat/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
"safe-push-apply/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
@@ -6867,8 +6799,6 @@
"tw-to-css/tailwindcss": ["tailwindcss@3.3.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.2.12", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.18.2", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.1", "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "postcss-value-parser": "^4.2.0", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w=="],
"type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
"unplugin/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
"unused-filename/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
@@ -7205,32 +7135,24 @@
"@octokit/auth-app/@octokit/request/@octokit/types": ["@octokit/types@17.0.0", "", { "dependencies": { "@octokit/openapi-types": "^28.0.0" } }, "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q=="],
"@octokit/auth-app/@octokit/request/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
"@octokit/auth-app/@octokit/request-error/@octokit/types": ["@octokit/types@17.0.0", "", { "dependencies": { "@octokit/openapi-types": "^28.0.0" } }, "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q=="],
"@octokit/auth-oauth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.4", "", { "dependencies": { "@octokit/types": "^17.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA=="],
"@octokit/auth-oauth-app/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.1", "", { "dependencies": { "@octokit/types": "^17.0.0" } }, "sha512-+eaY7G2VVpSf2pc5Gn1+mph837V/d/TYTJAgWL9Tb0ogGYcpN3IlAVFgjL+Vv93F/sevrxkvsYCedtpLdcFLzA=="],
"@octokit/auth-oauth-app/@octokit/request/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
"@octokit/auth-oauth-app/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="],
"@octokit/auth-oauth-device/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.4", "", { "dependencies": { "@octokit/types": "^17.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA=="],
"@octokit/auth-oauth-device/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.1", "", { "dependencies": { "@octokit/types": "^17.0.0" } }, "sha512-+eaY7G2VVpSf2pc5Gn1+mph837V/d/TYTJAgWL9Tb0ogGYcpN3IlAVFgjL+Vv93F/sevrxkvsYCedtpLdcFLzA=="],
"@octokit/auth-oauth-device/@octokit/request/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
"@octokit/auth-oauth-device/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="],
"@octokit/auth-oauth-user/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.4", "", { "dependencies": { "@octokit/types": "^17.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA=="],
"@octokit/auth-oauth-user/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.1", "", { "dependencies": { "@octokit/types": "^17.0.0" } }, "sha512-+eaY7G2VVpSf2pc5Gn1+mph837V/d/TYTJAgWL9Tb0ogGYcpN3IlAVFgjL+Vv93F/sevrxkvsYCedtpLdcFLzA=="],
"@octokit/auth-oauth-user/@octokit/request/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
"@octokit/auth-oauth-user/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="],
"@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
@@ -7241,8 +7163,6 @@
"@octokit/oauth-methods/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.4", "", { "dependencies": { "@octokit/types": "^17.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA=="],
"@octokit/oauth-methods/@octokit/request/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
"@octokit/oauth-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="],
"@octokit/plugin-paginate-rest/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="],
@@ -8057,20 +7977,14 @@
"@octokit/plugin-paginate-rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.4", "", { "dependencies": { "@octokit/types": "^17.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA=="],
"@octokit/plugin-paginate-rest/@octokit/core/@octokit/request/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
"@octokit/plugin-paginate-rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="],
"@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.4", "", { "dependencies": { "@octokit/types": "^17.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA=="],
"@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/request/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
"@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="],
"@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.4", "", { "dependencies": { "@octokit/types": "^17.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA=="],
"@octokit/rest/@octokit/core/@octokit/request/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
"@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="],
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/node/lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
+1 -1
View File
@@ -90,7 +90,7 @@ stdenv.mkDerivation (finalAttrs: {
export OPENCODE_CLI_DIST="$TMPDIR/desktop-cli"
cli_package=$(bun -e 'import { getCurrentCli } from "./scripts/utils.ts"; console.log(getCurrentCli().package.replace("@opencode/", ""))')
mkdir -p "$OPENCODE_CLI_DIST/$cli_package/bin"
cp ${lib.getExe opencode} "$OPENCODE_CLI_DIST/$cli_package/bin/opencode2"
cp ${lib.getExe opencode} "$OPENCODE_CLI_DIST/$cli_package/bin/opencode"
bun run build
npx electron-builder --dir \
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-euVUyj0CzjCA1nYbN2vKctEPzLkUlNGTK2dMNbackqM=",
"aarch64-linux": "sha256-qQkjqaxpjAae+rohoWI601QnrgKYghJ+ttqeiQBTwCM=",
"aarch64-darwin": "sha256-HYWs31TJlDZsDBNmbPARo16r7zNKy9x840uHGcUMYsk=",
"x86_64-darwin": "sha256-89FOrX813FENk3u8RAHCfyD7voaZWW++Z4Gpa3SkOJs="
"x86_64-linux": "sha256-MLA+R+7GovdBAVtJQ1pCOtXt16s6uC7Aplfjy5k3Q2M=",
"aarch64-linux": "sha256-mCfBpIpdeTDBSvUaKU7zGCAeV5UfkEaf3dYl5Y3okvg=",
"aarch64-darwin": "sha256-VmDVOpRFfR5f9GlboLJWHREcG8ClpClHU27p7tb9BkI=",
"x86_64-darwin": "sha256-wKw5EPQzuc9IptLLgJnBi6XWr3qN8Ze3GCElVFXLs6g="
}
}
+14 -4
View File
@@ -8,6 +8,7 @@
makeBinaryWrapper,
models-dev,
ripgrep,
wayland,
installShellFiles,
versionCheckHook,
writableTmpDirAsHomeHook,
@@ -62,9 +63,10 @@ stdenvNoCC.mkDerivation (finalAttrs: {
installPhase = ''
runHook preInstall
install -Dm755 dist/cli-*/bin/opencode2 $out/bin/opencode2
install -Dm755 dist/cli-*/bin/opencode $out/bin/opencode
wrapProgram $out/bin/opencode2 \
# OpenTUI dlopens Wayland for clipboard images.
wrapProgram $out/bin/opencode \
--prefix PATH : ${
lib.makeBinPath (
[
@@ -73,13 +75,21 @@ stdenvNoCC.mkDerivation (finalAttrs: {
# bun runs sysctl to detect if running on rosetta2
++ lib.optional stdenvNoCC.hostPlatform.isDarwin sysctl
)
}
} ${lib.optionalString stdenvNoCC.hostPlatform.isLinux ''
--prefix LD_LIBRARY_PATH : ${lib.makeLibraryPath [ wayland ]}
''}
ln -s opencode $out/bin/opencode2
runHook postInstall
'';
postInstall = lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) ''
# trick yargs into also generating zsh completions
installShellCompletion --cmd opencode \
--bash <($out/bin/opencode completion) \
--zsh <(SHELL=/bin/zsh $out/bin/opencode completion)
installShellCompletion --cmd opencode2 \
--bash <($out/bin/opencode2 completion) \
--zsh <(SHELL=/bin/zsh $out/bin/opencode2 completion)
@@ -101,7 +111,7 @@ stdenvNoCC.mkDerivation (finalAttrs: {
description = "The open source coding agent";
homepage = "https://opencode.ai";
license = lib.licenses.mit;
mainProgram = "opencode2";
mainProgram = "opencode";
inherit (node_modules.meta) platforms;
};
})
+3 -2
View File
@@ -116,6 +116,7 @@
"devDependencies": {
"@actions/artifact": "5.0.1",
"@ast-grep/cli": "0.44.0",
"@opencode/client": "workspace:*",
"@types/react": "19.2.17",
"@types/react-dom": "19.2.3",
"@tsconfig/bun": "catalog:",
@@ -175,10 +176,10 @@
"pacote@21.5.0": "patches/pacote@21.5.0.patch",
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"@tanstack/virtual-core@3.17.8": "patches/@tanstack%2Fvirtual-core@3.17.8.patch",
"@ff-labs/fff-bun@0.10.5": "patches/@ff-labs%2Ffff-bun@0.10.5.patch",
"ghostty-web@github:anomalyco/ghostty-web#83c0a07": "patches/ghostty-web@0.3.0.patch",
"vite@8.2.2": "patches/vite@8.2.2.patch"
"vite@8.2.2": "patches/vite@8.2.2.patch",
"@modelcontextprotocol/client@2.0.0": "patches/@modelcontextprotocol%2Fclient@2.0.0.patch"
}
}
+4
View File
@@ -166,6 +166,10 @@ Native chronological system messages are route/model-specific. Open Responses lo
The wrapped-user fallback preserves ordering while visibly lowering authority. Never silently pass a raw chronological `role: "system"` through a route that might reject it. Do not insert raw retrieved documents, tool output, or web content into privileged chronological system updates; keep untrusted content in ordinary user/tool channels.
### Effort Updates
`Message.effort({ effort, previous })` is a chronological "reasoning effort changed here" marker (`undefined` means the model default). Changing a top-level effort invalidates the whole provider prompt cache, so protocols with a native per-message update (`Protocol.supportsEffortUpdates`) keep the top-level effort at the first marker's `previous` and lower each marker in place: Anthropic Messages emits an empty `role: "system"` message with `output_config.effort` plus the `mid-conversation-output-config-2026-07-01` beta, and OpenAI Responses emits `configuration_update` items. `applyEffortUpdates` runs in `prepareRequest` and strips the markers for every other route, so a protocol without support keeps today's plain top-level behaviour. When the last marker disagrees with the effort the request asks for (reverted or forked history), `resolveEffortUpdates` strips the markers and falls back to a plain top-level change.
### Tools
Tool loops are represented in common messages and events:
+9 -2
View File
@@ -12,6 +12,7 @@
// count against the four-breakpoint budget; auto only fills remaining slots.
import { CacheHint, type CachePolicy, type CachePolicyObject } from "./schema/options.js"
import { LLMRequest, Message, ToolDefinition, type ContentPart, type ToolEntry } from "./schema/messages.js"
import { effortUpdate } from "./effort-updates.js"
const AUTO: CachePolicyObject = {
tools: true,
@@ -121,9 +122,15 @@ const markMessages = (
return markMessageAt(messages, lastIndexOfRole(messages, "user"), hint, budget)
if (strategy === "latest-assistant")
return markMessageAt(messages, lastIndexOfRole(messages, "assistant"), hint, budget)
const start = Math.max(0, messages.length - strategy.tail)
let start = messages.length
let remaining = strategy.tail
while (remaining > 0 && start > 0) {
start -= 1
if (effortUpdate(messages[start]!) === undefined) remaining -= 1
}
let next = messages
for (let i = start; i < messages.length; i++) next = markMessageAt(next, i, hint, budget)
for (let i = start; i < messages.length; i++)
if (effortUpdate(messages[i]!) === undefined) next = markMessageAt(next, i, hint, budget)
return next
}
+25
View File
@@ -0,0 +1,25 @@
// Changing the top-level reasoning effort invalidates the provider prompt cache. Protocols with a native
// per-message update keep it frozen and lower `Message.effort(...)` markers instead; other routes strip them.
import { LLMRequest, type EffortPart, type Message } from "./schema/messages.js"
export const effortUpdate = (message: Message): EffortPart | undefined => {
if (message.role !== "system" || message.content.length !== 1) return undefined
const part = message.content[0]
return part.type === "effort" ? part : undefined
}
export const stripEffortUpdates = (request: LLMRequest) => {
const messages = request.messages.filter((message) => effortUpdate(message) === undefined)
return messages.length === request.messages.length ? request : LLMRequest.update(request, { messages })
}
export const applyEffortUpdates = (request: LLMRequest): LLMRequest =>
request.model.route.supportsEffortUpdates?.(request) ? request : stripEffortUpdates(request)
// Reverted or forked history can leave the last marker disagreeing with the requested effort.
export const resolveEffortUpdates = (request: LLMRequest, current: string | undefined) => {
const updates = request.messages.flatMap((message) => effortUpdate(message) ?? [])
if (updates.length === 0) return { request, effort: current }
if (updates.at(-1)?.effort !== current) return { request: stripEffortUpdates(request), effort: current }
return { request, effort: updates[0]?.previous }
}
+50 -18
View File
@@ -27,6 +27,7 @@ import {
} from "../schema/index.js"
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js"
import { classifyProviderFailure } from "../provider-error.js"
import { effortUpdate, resolveEffortUpdates } from "../effort-updates.js"
import * as Cache from "./utils/cache.js"
import { Lifecycle } from "./utils/lifecycle.js"
import { ToolSchemaProjection } from "./utils/tool-schema.js"
@@ -36,6 +37,7 @@ const ADAPTER = "anthropic-messages"
export const DEFAULT_BASE_URL = "https://api.anthropic.com/v1"
export const PATH = "/messages"
export const DEFAULT_MAX_TOKENS = 32_000
const DEFAULT_EFFORT = "high"
const SSE_EVENTS = new Set([
"message",
@@ -286,7 +288,11 @@ type AnthropicToolResultBlock = Schema.Schema.Type<typeof AnthropicToolResultBlo
const AnthropicMessage = Schema.Union([
Schema.Struct({ role: Schema.Literal("user"), content: Schema.Array(AnthropicUserBlock) }),
Schema.Struct({ role: Schema.Literal("assistant"), content: Schema.Array(AnthropicAssistantBlock) }),
Schema.Struct({ role: Schema.Literal("system"), content: Schema.Array(AnthropicTextBlock) }),
Schema.Struct({
role: Schema.Literal("system"),
content: Schema.Array(AnthropicTextBlock),
output_config: Schema.optional(Schema.Struct({ effort: Schema.String })),
}),
]).pipe(Schema.toTaggedUnion("role"))
type AnthropicMessage = Schema.Schema.Type<typeof AnthropicMessage>
@@ -877,6 +883,12 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
for (const [index, message] of request.messages.entries()) {
if (message.role === "system") {
const update = effortUpdate(message)
if (update) {
// Accepted at any position, so the text-update placement rules do not apply.
messages.push({ role: "system", content: [], output_config: { effort: update.effort ?? DEFAULT_EFFORT } })
continue
}
if (splitsLocalToolResults(request.messages, index))
return yield* invalid("Anthropic Messages system updates cannot split a local tool call from its tool result")
if (supportsNativeSystemUpdates(request) && canUseNativeSystemUpdate(request, index)) {
@@ -1034,18 +1046,11 @@ const resolveOptions = Effect.fn("AnthropicMessages.resolveOptions")(function* (
ProviderShared.isRecord(rawOutputConfig) && ProviderShared.isRecord(rawOutputConfig.format)
? (rawOutputConfig.format as { type: "json_schema"; schema: Record<string, unknown> })
: undefined
const output_config =
outputConfigEffort === undefined && outputConfigFormat === undefined
? undefined
: {
...(outputConfigEffort === undefined ? {} : { effort: outputConfigEffort }),
...(outputConfigFormat === undefined ? {} : { format: outputConfigFormat }),
}
const thinking = yield* resolveThinking(input?.thinking)
return {
thinking: applyThinkingBindingDefault(request.model, thinking),
effort: outputConfigEffort,
output_config,
format: outputConfigFormat,
service_tier,
metadata,
container,
@@ -1054,15 +1059,30 @@ const resolveOptions = Effect.fn("AnthropicMessages.resolveOptions")(function* (
}
})
// Accept gateway namespaces and Vertex suffixes without treating a snapshot date as a minor version.
const claudeVersion = (id: string) => {
const match = /(?:^|[./])claude-(?<family>[a-z]+)-(?<major>\d+)(?:[.-](?<minor>\d{1,2}))?(?:$|[-:@])/.exec(
id.toLowerCase(),
)?.groups
if (!match) return undefined
return { family: match.family, major: Number(match.major), minor: Number(match.minor ?? 0) }
}
const supportsThinkingBlockBinding = (model: LLMRequest["model"]) => {
const override = model.compatibility?.supportsThinkingBlockBinding
if (override !== undefined) return override
// Accept gateway namespaces and Vertex suffixes without treating a snapshot date as a minor version.
const version = /(?:^|[./])claude-[a-z]+-(?<major>\d+)(?:[.-](?<minor>\d{1,2}))?(?:$|[-:@])/i.exec(model.id)?.groups
if (!version) return false
const major = Number(version.major)
const minor = Number(version.minor ?? 0)
return major > 5 || (major === 5 && minor >= 1)
const version = claudeVersion(model.id)
return version !== undefined && (version.major > 5 || (version.major === 5 && version.minor >= 1))
}
const supportsEffortUpdates = (model: LLMRequest["model"]) => {
const override = model.compatibility?.supportsEffortUpdates
if (override !== undefined) return override
const version = claudeVersion(model.id)
if (version === undefined) return false
if (version.family === "opus") return version.major >= 5
if (version.family !== "fable" && version.family !== "mythos") return false
return version.major > 5 || (version.major === 5 && version.minor >= 1)
}
const applyThinkingBindingDefault = (model: LLMRequest["model"], thinking: AnthropicThinking | undefined) => {
@@ -1104,13 +1124,15 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
const management = yield* ProviderShared.validateWith(
Schema.decodeUnknownEffect(Schema.UndefinedOr(ContextManagement)),
)(request.providerOptions?.contextManagement)
const options = yield* resolveOptions(request)
const updates = resolveEffortUpdates(request, options.effort)
const generation = request.generation
const toolSchemaCompatibility = request.model.compatibility?.toolSchema
// Allocate the 4-breakpoint budget in invalidation order: tools → system →
// messages. Tools live highest in the cache hierarchy, so when callers
// over-mark we keep their tool hints and shed the message-tail ones first.
const breakpoints = Cache.newBreakpoints(ANTHROPIC_BREAKPOINT_CAP)
const flattened = ProviderShared.flattenToolRequest(request)
const flattened = ProviderShared.flattenToolRequest(updates.request)
const tools =
flattened.tools.length === 0
? undefined
@@ -1138,7 +1160,13 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
`Anthropic Messages: dropped ${breakpoints.dropped} cache breakpoint(s); the API allows at most ${ANTHROPIC_BREAKPOINT_CAP} per request.`,
)
}
const options = yield* resolveOptions(request)
const output_config =
updates.effort === undefined && options.format === undefined
? undefined
: {
...(updates.effort === undefined ? {} : { effort: updates.effort }),
...(options.format === undefined ? {} : { format: options.format }),
}
const body = {
model: request.model.id,
system,
@@ -1152,7 +1180,7 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
top_k: generation?.topK,
stop_sequences: generation?.stop,
thinking: options.thinking,
output_config: options.output_config,
output_config,
// top-level passthrough per SDK MessageCreateParamsBase:4638,4643,4649,4654,4670
cache_control: options.cache_control,
container: options.container,
@@ -1677,6 +1705,7 @@ export const protocol = Protocol.make({
}),
step,
},
supportsEffortUpdates: (request) => supportsEffortUpdates(request.model),
})
export const transport = <
@@ -1718,6 +1747,9 @@ function requiredBetaHeaders(body: Pick<AnthropicMessagesBody, "messages" | "con
)
if (requestsCompaction || replaysCompaction) betas.push("compact-2026-01-12")
if (body.messages.some((message) => message.role === "system" && message.output_config !== undefined))
betas.push("mid-conversation-output-config-2026-07-01")
const thinking = body.thinking
if (thinking && thinking.type !== "disabled" && thinking.block_binding)
betas.push("thinking-binding-controls-2026-08-01")
+21 -3
View File
@@ -20,6 +20,7 @@ import {
} from "../schema/index.js"
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js"
import { classifyProviderFailure } from "../provider-error.js"
import { effortUpdate } from "../effort-updates.js"
import { OpenResponsesOptions } from "./utils/open-responses-options.js"
import { Lifecycle } from "./utils/lifecycle.js"
import { ToolSchemaProjection } from "./utils/tool-schema.js"
@@ -164,6 +165,13 @@ export const CompactionItem = Schema.Struct({
encrypted_content: Schema.String,
})
// Kept out of the baseline `InputItem` union: only the OpenAI extension accepts it.
export const ConfigurationUpdate = Schema.Struct({
type: Schema.Literal("configuration_update"),
reasoning: Schema.Struct({ effort: OpenResponsesOptions.ReasoningEffort }),
})
type ConfigurationUpdate = Schema.Schema.Type<typeof ConfigurationUpdate>
export const InputItem = Schema.Union([
CompactionItem,
Schema.Struct({ role: Schema.tag("system"), content: Schema.String }),
@@ -208,6 +216,7 @@ export type HostedToolReplayItem = {
type LoweredInputItem =
| OpenResponsesInputItem
| HostedToolReplayItem
| ConfigurationUpdate
| {
readonly type: "message"
readonly id?: string
@@ -522,7 +531,7 @@ const lowerToolCall = (part: ToolCallPart, providerMetadataKey: string): OpenRes
call_id: part.id,
name: part.name,
namespace: part.namespace,
arguments: ProviderShared.encodeJson(part.input),
arguments: ProviderShared.encodeJson(part.input === undefined ? {} : part.input),
}
}
@@ -634,6 +643,8 @@ const lowerToolResultOutput = Effect.fnUntraced(function* (
return yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, request, adapter))
})
const DEFAULT_EFFORT = "medium"
const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (
request: LLMRequest,
adapter: ProviderAdapter,
@@ -646,6 +657,14 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (
Schema.decodeUnknownEffect(Schema.UndefinedOr(MessageMetadata)),
)(message.providerMetadata?.[providerMetadataKey])
if (message.role === "system") {
const update = effortUpdate(message)
if (update) {
// Consecutive updates are rejected, so a newer one replaces its predecessor.
const last = input.at(-1)
if (last !== undefined && "type" in last && last.type === "configuration_update") input.pop()
input.push({ type: "configuration_update", reasoning: { effort: update.effort ?? DEFAULT_EFFORT } })
continue
}
input.push({
role: "developer",
content: ProviderShared.joinText(yield* ProviderShared.systemUpdateText(adapter.name, message)),
@@ -789,8 +808,7 @@ export const lowerConversation = Effect.fn("OpenResponses.lowerConversation")(fu
}
})
export const lowerGeneration = (request: LLMRequest) => {
const options = OpenResponsesOptions.resolve(request)
export const lowerGeneration = (request: LLMRequest, options = OpenResponsesOptions.resolve(request)) => {
const generation = request.generation
const cacheKey = ProviderShared.promptCacheKey(request)
const parallelToolCalls = resolveParallelToolCalls(request)
+6 -2
View File
@@ -315,7 +315,7 @@ const lowerToolCall = (part: ToolCallPart, options: LoweringOptions): OpenAIChat
type: "function",
function: {
name: part.name,
arguments: ProviderShared.encodeJson(part.input),
arguments: ProviderShared.encodeJson(part.input === undefined ? {} : part.input),
},
})
@@ -323,7 +323,11 @@ const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part: MediaPart
const media = ProviderShared.normalizeMedia(part)
if (!media.mime.startsWith("image/"))
return yield* ProviderShared.invalidRequest(`OpenAI Chat does not support media type ${part.mediaType}`)
return { type: "image_url" as const, image_url: { url: media.dataUrl } }
const url =
typeof part.data === "string" && (part.data.startsWith("https://") || part.data.startsWith("http://"))
? part.data
: media.dataUrl
return { type: "image_url" as const, image_url: { url } }
})
const openAICompatibleReasoningContent = (native: unknown) =>
+23 -4
View File
@@ -6,7 +6,9 @@ import { Endpoint } from "../route/endpoint.js"
import { Protocol } from "../route/protocol.js"
import { HttpTransport } from "../route/transport/index.js"
import { LLMRequest, mergeJsonRecords, type JsonSchema, type ToolDefinition, type ToolEntry } from "../schema/index.js"
import { resolveEffortUpdates } from "../effort-updates.js"
import { OpenResponses } from "./open-responses.js"
import { OpenResponsesOptions } from "./utils/open-responses-options.js"
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js"
import { OpenAIImage } from "./utils/openai-image.js"
import { ResponsesHostedTools } from "./utils/responses-hosted-tools.js"
@@ -94,9 +96,15 @@ const OpenAIResponsesToolChoice = Schema.Union([
Schema.Struct({ type: Schema.tag("image_generation") }),
])
const OpenAIResponsesInputItem = Schema.Union([
OpenResponses.InputItem,
OpenAIResponsesHostedToolItem,
OpenResponses.ConfigurationUpdate,
])
const OpenAIResponsesCoreFields = {
...OpenResponses.coreFields,
input: Schema.Array(Schema.Union([OpenResponses.InputItem, OpenAIResponsesHostedToolItem])),
input: Schema.Array(OpenAIResponsesInputItem),
tools: optionalArray(OpenAIResponsesTools),
tool_choice: Schema.optional(OpenAIResponsesToolChoice),
context_management: Schema.optional(
@@ -119,7 +127,7 @@ export type OpenAIResponsesBody = Schema.Schema.Type<typeof OpenAIResponsesBody>
export const CompactionTrigger = Schema.Struct({ type: Schema.Literal("compaction_trigger") })
const CheckpointBody = Schema.Struct({
...OpenAIResponsesBody.fields,
input: Schema.Array(Schema.Union([OpenResponses.InputItem, OpenAIResponsesHostedToolItem, CompactionTrigger])),
input: Schema.Array(Schema.Union([OpenAIResponsesInputItem, CompactionTrigger])),
store: Schema.Literal(false),
prompt_cache_retention: optionalNull(Schema.String),
prompt_cache_options: optionalNull(
@@ -133,6 +141,14 @@ const adapter = {
restoreHostedToolItem: (item: unknown) => (Schema.is(OpenAIResponsesHostedToolItem)(item) ? item : undefined),
} satisfies OpenResponses.ProviderAdapter
// Only GPT-6 Astra accepts `configuration_update`, and never alongside automatic `context_management` compaction.
const supportsEffortUpdates = (request: LLMRequest) => {
if (request.providerOptions?.contextManagement !== undefined) return false
const override = request.model.compatibility?.supportsEffortUpdates
if (override !== undefined) return override
return /(?:^|\/)gpt-6-astra$/i.test(request.model.id)
}
const nativeImageToolInput = (tool: ToolDefinition) => {
const native = tool.native?.openai
return ProviderShared.isRecord(native) && native.type === "image_generation" ? native : undefined
@@ -189,10 +205,12 @@ const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request:
const management = yield* ProviderShared.validateWith(
Schema.decodeUnknownEffect(Schema.UndefinedOr(ContextManagement)),
)(request.providerOptions?.contextManagement)
const options = OpenResponsesOptions.resolve(request)
const updates = resolveEffortUpdates(request, options.reasoningEffort)
const toolSchemaCompatibility = request.model.compatibility?.toolSchema
return yield* decodeBody({
...(yield* OpenResponses.lowerConversation(request, adapter)),
...OpenResponses.lowerGeneration(request),
...(yield* OpenResponses.lowerConversation(updates.request, adapter)),
...OpenResponses.lowerGeneration(request, { ...options, reasoningEffort: updates.effort }),
context_management: management?.map((edit) => ({ type: edit.type, compact_threshold: edit.compactThreshold })),
tools:
request.tools.length === 0
@@ -297,6 +315,7 @@ export const protocol = Protocol.make({
step,
terminal: OpenResponses.terminal,
},
supportsEffortUpdates,
})
const endpoint = Endpoint.path<OpenAIResponsesBody>(PATH, { baseURL: DEFAULT_BASE_URL })
@@ -1,12 +1,7 @@
import { Option, Schema } from "effect"
import type { LLMRequest } from "../../schema/index.js"
import { ReasoningEffort, ReasoningEfforts, type LLMRequest } from "../../schema/index.js"
export const ReasoningEfforts = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const
export type ReasoningEffort = (typeof ReasoningEfforts)[number] | (string & {})
export const ReasoningEffort = Schema.declare<ReasoningEffort>(
(value): value is ReasoningEffort => typeof value === "string",
{ title: "ReasoningEffort" },
)
export { ReasoningEffort, ReasoningEfforts }
export const TextVerbosities = ["low", "medium", "high"] as const
export type TextVerbosity = (typeof TextVerbosities)[number] | (string & {})
@@ -11,6 +11,7 @@ import {
mergeJsonRecords,
} from "../../schema/index.js"
import type { CompactOperation } from "../../route/client.js"
import { stripEffortUpdates } from "../../effort-updates.js"
import { Endpoint } from "../../route/endpoint.js"
import { RequestExecutor } from "../../route/executor.js"
import { HttpTransport } from "../../route/transport/index.js"
@@ -75,7 +76,8 @@ const Response = Schema.Struct({
export const make = (adapter: OpenResponses.ProviderAdapter): CompactOperation =>
Effect.fn("ResponsesCompaction.execute")(function* (request, executor, options) {
const route = request.model.route
const native = yield* OpenResponses.lowerConversation(request, adapter)
// The standalone compaction endpoint rejects histories containing configuration updates.
const native = yield* OpenResponses.lowerConversation(stripEffortUpdates(request), adapter)
const body = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Body))(
mergeJsonRecords(
{
+5 -1
View File
@@ -37,7 +37,7 @@ export type Settings = ProviderPackage.Settings &
}
const fromRequest = Effect.fn("GoogleVertex.fromRequest")(function* (request: LLMRequest) {
const body = yield* Gemini.protocol.body.from(request)
const { serviceTier: _, ...body } = yield* Gemini.protocol.body.from(request)
// Vertex's native REST schema rejects `id` on FunctionCall/FunctionResponse parts with HTTP 400,
// unlike AI Studio, so history minted there cannot be lowered verbatim.
const contents = body.contents.map((content) => ({
@@ -75,6 +75,10 @@ const route = Route.make({
return `/${model.startsWith("endpoints/") ? model : `models/${model}`}:streamGenerateContent?alt=sse`
}),
auth: Auth.none,
headers: ({ request }): Record<string, string> => {
const serviceTier = request.providerOptions?.serviceTier
return typeof serviceTier === "string" ? { "x-vertex-ai-llm-shared-request-type": serviceTier } : {}
},
framing: Framing.sse,
})
+6 -1
View File
@@ -7,6 +7,7 @@ import { HttpTransport } from "./transport/index.js"
import type { HttpMiddleware, Transport, TransportRuntime, WebSocketChannelExecutor } from "./transport/index.js"
import type { Protocol } from "./protocol.js"
import { applyCachePolicy } from "../cache-policy.js"
import { applyEffortUpdates } from "../effort-updates.js"
import { normalizeToolHistory } from "../tool-history.js"
import { sanitizeSurrogates } from "../utils/sanitize.js"
import * as ProviderShared from "../protocols/shared.js"
@@ -55,6 +56,7 @@ export interface Route<
readonly transport: Transport<Body, Prepared, unknown>
readonly defaults: RouteDefaults
readonly body: RouteBody<Body>
readonly supportsEffortUpdates?: (request: LLMRequest) => boolean
readonly with: {
<Next extends CompactionOperations | undefined>(
patch: RoutePatch<Body, Prepared> & { readonly compact: Next },
@@ -388,6 +390,7 @@ function makeFromTransport<Body, Prepared, Frame, Event, State>(
transport: routeInput.transport,
defaults: routeInput.defaults ?? {},
body: protocol.body,
supportsEffortUpdates: protocol.supportsEffortUpdates,
with: (patch: RoutePatch<Body, Prepared>) => {
const { compact, id, provider, providerMetadataKey, auth, transport, endpoint, ...defaults } = patch
return build({
@@ -558,7 +561,9 @@ const prepareRequest = (request: LLMRequest) => {
[...new Map(tools.map((tool) => [`${tool.type}:${tool.name}`, tool])).values()].map((tool) =>
tool.type === "tool" ? tool : { ...tool, tools: dedupe(tool.tools) },
)
const resolved = applyCachePolicy(LLMRequest.update(sanitized, { tools: dedupe(sanitized.tools) }))
const resolved = applyCachePolicy(
applyEffortUpdates(LLMRequest.update(sanitized, { tools: dedupe(sanitized.tools) })),
)
const headers = resolved.model.route.headers?.({ request: resolved })
return headers === undefined
? resolved
+2
View File
@@ -41,6 +41,8 @@ export interface Protocol<Body, Frame, Event, State> {
readonly body: ProtocolBody<Body>
/** Response side: streaming state machine. */
readonly stream: ProtocolStream<Frame, Event, State>
/** Whether `body.from` lowers `Message.effort(...)` markers; wrappers around another `body.from` must forward it. */
readonly supportsEffortUpdates?: (request: LLMRequest) => boolean
}
export interface ProtocolBody<Body> {
+13
View File
@@ -9,6 +9,7 @@ import {
LanguageModelSchema,
type LanguageModel,
ProviderOptions,
ReasoningEffort,
} from "./options.js"
import { ProviderID } from "./ids.js"
@@ -217,6 +218,14 @@ export const CompactionPart = Object.assign(compactionPartSchema, {
Schema.decodeUnknownSync(compactionPartSchema)({ type: "compaction", ...input }),
})
/** Reasoning effort changed here, from `previous` to `effort`; `undefined` is the model default. */
export const EffortPart = Schema.Struct({
type: Schema.Literal("effort"),
effort: Schema.optional(ReasoningEffort),
previous: Schema.optional(ReasoningEffort),
}).annotate({ identifier: "LLM.Content.Effort" })
export type EffortPart = Schema.Schema.Type<typeof EffortPart>
export const ContentPart = Schema.Union([
TextPart,
MediaPart,
@@ -224,6 +233,7 @@ export const ContentPart = Schema.Union([
ToolResultPart,
ReasoningPart,
CompactionPart,
EffortPart,
]).pipe(Schema.toTaggedUnion("type"))
export type ContentPart = Schema.Schema.Type<typeof ContentPart>
@@ -265,6 +275,9 @@ export namespace Message {
*/
export const system = (content: SystemContentInput) => make({ role: "system", content })
export const effort = (input: { readonly effort?: ReasoningEffort; readonly previous?: ReasoningEffort }) =>
make({ role: "system", content: [{ type: "effort", effort: input.effort, previous: input.previous }] })
export const tool = (result: ToolResultPart | Parameters<typeof ToolResultPart.make>[0]) =>
make({ role: "tool", content: ["type" in result ? result : ToolResultPart.make(result)] })
}
+9
View File
@@ -140,6 +140,13 @@ export namespace LanguageModelDefaults {
}
}
export const ReasoningEfforts = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const
export type ReasoningEffort = (typeof ReasoningEfforts)[number] | (string & {})
export const ReasoningEffort = Schema.declare<ReasoningEffort>(
(value): value is ReasoningEffort => typeof value === "string",
{ title: "ReasoningEffort" },
)
export const LanguageModelToolSchemaCompatibility = Schema.Literals(["gemini", "moonshot"])
export type LanguageModelToolSchemaCompatibility = Schema.Schema.Type<typeof LanguageModelToolSchemaCompatibility>
@@ -165,6 +172,8 @@ export class LanguageModelCompatibility extends Schema.Class<LanguageModelCompat
requireSignature: Schema.optional(Schema.Boolean),
/** Supports Anthropic's thinking-prefix mismatch controls. Overrides model-ID detection. */
supportsThinkingBlockBinding: Schema.optional(Schema.Boolean),
/** Supports per-message effort updates. Overrides model-ID detection. */
supportsEffortUpdates: Schema.optional(Schema.Boolean),
}) {}
export namespace LanguageModelCompatibility {
+441
View File
@@ -0,0 +1,441 @@
import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { LLM, LLMRequest, Message, ToolCallPart } from "../src/index.js"
import { Auth, LLMClient } from "../src/route.js"
import { compileRequest } from "../src/route/client.js"
import { AnthropicMessages } from "../src/protocols/anthropic-messages.js"
import { OpenAIResponses } from "../src/protocols/openai-responses.js"
import { Gemini } from "../src/protocols/gemini.js"
import { GoogleVertexMessages, OpenAI } from "../src/providers.js"
import { applyCachePolicy } from "../src/cache-policy.js"
import { applyEffortUpdates } from "../src/effort-updates.js"
import { it, testEffect } from "./lib/effect.js"
import { dynamicResponse } from "./lib/http.js"
import { sseEvents } from "./lib/sse.js"
const anthropic = (id: string, compatibility?: { readonly supportsEffortUpdates?: boolean }) =>
AnthropicMessages.route
.with({ endpoint: { baseURL: "https://api.anthropic.test/v1/" }, auth: Auth.header("x-api-key", "test") })
.model({ id, compatibility })
const openai = (id: string, compatibility?: { readonly supportsEffortUpdates?: boolean }) =>
OpenAIResponses.route
.with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") })
.model({ id, compatibility })
const opus5 = anthropic("claude-opus-5")
const astra = openai("gpt-6-astra")
const lowFromHigh = Message.effort({ effort: "low", previous: "high" })
const conversation = [Message.user("Before."), lowFromHigh, Message.user("After.")]
const systemMessages = (body: AnthropicMessages.AnthropicMessagesBody) =>
body.messages.filter((message) => message.role === "system")
const updates = (body: OpenAIResponses.OpenAIResponsesBody) =>
body.input.filter((item) => "type" in item && item.type === "configuration_update")
describe("applyEffortUpdates", () => {
test("keeps the request identity without markers and for protocols that lower them", () => {
const plain = LLM.request({ model: opus5, prompt: "hi" })
expect(applyEffortUpdates(plain)).toBe(plain)
const supported = LLM.request({ model: opus5, messages: conversation, providerOptions: { effort: "low" } })
expect(applyEffortUpdates(supported)).toBe(supported)
})
it.effect("compiles markers away for protocols without per-message effort", () =>
Effect.gen(function* () {
const model = Gemini.route
.with({
endpoint: { baseURL: "https://generativelanguage.test/v1beta/" },
auth: Auth.header("x-goog-api-key", "test"),
})
.model({ id: "gemini-3.5-flash" })
const withMarkers = yield* compileRequest(LLM.request({ model, messages: conversation }))
const withoutMarkers = yield* compileRequest(
LLM.request({ model, messages: [Message.user("Before."), Message.user("After.")] }),
)
expect(withMarkers.body).toEqual(withoutMarkers.body)
}),
)
})
describe("cache policy", () => {
it.effect("walks the tail breakpoint back past a trailing effort marker", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: opus5,
messages: [Message.user("first"), Message.assistant("reply"), Message.user("latest"), lowFromHigh],
providerOptions: { effort: "low" },
cache: "auto",
}),
)
expect(prepared.body.messages).toEqual([
{ role: "user", content: [{ type: "text", text: "first" }] },
{ role: "assistant", content: [{ type: "text", text: "reply" }] },
{ role: "user", content: [{ type: "text", text: "latest", cache_control: { type: "ephemeral" } }] },
{ role: "system", content: [], output_config: { effort: "low" } },
])
}),
)
})
describe("Anthropic Messages effort updates", () => {
it.effect("lowers markers to per-turn system messages and freezes the top-level effort", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({ model: opus5, messages: conversation, providerOptions: { effort: "low" }, cache: "none" }),
)
expect(prepared.body.output_config).toEqual({ effort: "high" })
expect(prepared.body.messages).toEqual([
{ role: "user", content: [{ type: "text", text: "Before." }] },
{ role: "system", content: [], output_config: { effort: "low" } },
{ role: "user", content: [{ type: "text", text: "After." }] },
])
}),
)
it.effect("omits the frozen effort for the model default and sends `high` for a switch back to it", () =>
Effect.gen(function* () {
const format = { type: "json_schema" as const, schema: { type: "object" } }
const prepared = yield* compileRequest(
LLM.request({
model: opus5,
messages: [
Message.user("One."),
Message.effort({ effort: "low" }),
Message.user("Two."),
Message.effort({ previous: "low" }),
Message.user("Three."),
],
providerOptions: { output_config: { format } },
cache: "none",
}),
)
expect(prepared.body.output_config).toEqual({ format })
expect(systemMessages(prepared.body)).toEqual([
{ role: "system", content: [], output_config: { effort: "low" } },
{ role: "system", content: [], output_config: { effort: "high" } },
])
}),
)
it.effect("requests the mid-conversation output config beta only when markers are sent", () =>
Effect.gen(function* () {
for (const [model, expected] of [
[opus5, true],
[anthropic("claude-sonnet-5"), false],
] as const) {
const request = LLM.request({
model,
messages: conversation,
providerOptions: { effort: "low" },
http: { headers: { "anthropic-beta": "existing-beta" } },
})
const compiled = yield* compileRequest(request)
const prepared = yield* AnthropicMessages.route.prepareTransport(compiled.body, request)
const betas = prepared.request.headers["anthropic-beta"]!.split(",")
expect(betas).toContain("existing-beta")
expect(betas.includes("mid-conversation-output-config-2026-07-01")).toBe(expected)
}
}),
)
it.effect("accepts a marker between a tool call and its result", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: opus5,
messages: [
Message.user("Weather?"),
Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: {} })]),
lowFromHigh,
Message.tool({ id: "call_1", name: "lookup", result: { temp: 72 } }),
],
providerOptions: { effort: "low" },
cache: "none",
}),
)
expect(prepared.body.messages).toEqual([
{ role: "user", content: [{ type: "text", text: "Weather?" }] },
{ role: "assistant", content: [{ type: "tool_use", id: "call_1", name: "lookup", input: {} }] },
{ role: "system", content: [], output_config: { effort: "low" } },
{ role: "user", content: [{ type: "tool_result", tool_use_id: "call_1", content: '{"temp":72}' }] },
])
}),
)
it.effect("falls back to a plain top-level effort when history drifted from the current effort", () =>
Effect.gen(function* () {
const drifted = yield* compileRequest(
LLM.request({ model: opus5, messages: conversation, providerOptions: { effort: "medium" }, cache: "none" }),
)
const plain = yield* compileRequest(
LLM.request({
model: opus5,
messages: [Message.user("Before."), Message.user("After.")],
providerOptions: { effort: "medium" },
cache: "none",
}),
)
expect(drifted.body).toEqual(plain.body)
expect(drifted.body.output_config).toEqual({ effort: "medium" })
}),
)
for (const [id, supported] of [
["claude-opus-5", true],
["claude-opus-5-20260901", true],
["anthropic/claude-opus-5", true],
["claude-fable-5-1", true],
["claude-mythos-5-1", true],
["claude-fable-5", false],
["claude-opus-4-8", false],
["claude-sonnet-5", false],
["kimi-k2.5", false],
] as const) {
it.effect(`${supported ? "lowers" : "strips"} markers for ${id}`, () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({ model: anthropic(id), messages: conversation, providerOptions: { effort: "low" } }),
)
expect(systemMessages(prepared.body)).toHaveLength(supported ? 1 : 0)
expect(prepared.body.output_config).toEqual({ effort: supported ? "high" : "low" })
}),
)
}
it.effect("honors the compatibility override in both directions", () =>
Effect.gen(function* () {
const enabled = yield* compileRequest(
LLM.request({
model: anthropic("claude-sonnet-5", { supportsEffortUpdates: true }),
messages: conversation,
providerOptions: { effort: "low" },
}),
)
const disabled = yield* compileRequest(
LLM.request({
model: anthropic("claude-opus-5", { supportsEffortUpdates: false }),
messages: conversation,
providerOptions: { effort: "low" },
}),
)
expect(systemMessages(enabled.body)).toHaveLength(1)
expect(systemMessages(disabled.body)).toHaveLength(0)
}),
)
it.effect("strips markers on the Vertex Anthropic route, whose protocol wrapper does not forward support", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: GoogleVertexMessages.configure({ accessToken: "test", location: "global", project: "test" }).model(
"claude-opus-5",
),
messages: conversation,
providerOptions: { effort: "low" },
}),
)
expect(systemMessages(prepared.body)).toHaveLength(0)
expect(prepared.body.output_config).toEqual({ effort: "low" })
}),
)
})
describe("OpenAI Responses effort updates", () => {
it.effect("lowers markers to configuration_update items and freezes reasoning.effort", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({ model: astra, messages: conversation, providerOptions: { reasoningEffort: "low" } }),
)
expect(prepared.body.reasoning).toEqual({ effort: "high" })
expect(prepared.body.input).toEqual([
{ role: "user", content: [{ type: "input_text", text: "Before." }] },
{ type: "configuration_update", reasoning: { effort: "low" } },
{ role: "user", content: [{ type: "input_text", text: "After." }] },
])
}),
)
it.effect("coalesces consecutive updates so the newest wins", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: astra,
messages: [
Message.user("Before."),
Message.effort({ effort: "low", previous: "medium" }),
Message.effort({ effort: "xhigh", previous: "low" }),
Message.user("After."),
],
providerOptions: { reasoningEffort: "xhigh" },
}),
)
expect(prepared.body.reasoning).toEqual({ effort: "medium" })
expect(prepared.body.input).toEqual([
{ role: "user", content: [{ type: "input_text", text: "Before." }] },
{ type: "configuration_update", reasoning: { effort: "xhigh" } },
{ role: "user", content: [{ type: "input_text", text: "After." }] },
])
}),
)
it.effect("omits reasoning.effort for the model default and sends `medium` for a switch back to it", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: astra,
messages: [
Message.user("One."),
Message.effort({ effort: "low" }),
Message.user("Two."),
Message.effort({ previous: "low" }),
Message.user("Three."),
],
}),
)
expect(prepared.body.reasoning).toBeUndefined()
expect(updates(prepared.body)).toEqual([
{ type: "configuration_update", reasoning: { effort: "low" } },
{ type: "configuration_update", reasoning: { effort: "medium" } },
])
}),
)
it.effect("falls back to a plain top-level effort when history drifted from the current effort", () =>
Effect.gen(function* () {
const drifted = yield* compileRequest(
LLM.request({ model: astra, messages: conversation, providerOptions: { reasoningEffort: "xhigh" } }),
)
const plain = yield* compileRequest(
LLM.request({
model: astra,
messages: [Message.user("Before."), Message.user("After.")],
providerOptions: { reasoningEffort: "xhigh" },
}),
)
expect(drifted.body).toEqual(plain.body)
expect(drifted.body.reasoning).toEqual({ effort: "xhigh" })
}),
)
it.effect("strips markers when automatic context management is enabled", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: astra,
messages: conversation,
providerOptions: { reasoningEffort: "low", contextManagement: [{ type: "compaction" }] },
}),
)
expect(updates(prepared.body)).toEqual([])
expect(prepared.body.reasoning).toEqual({ effort: "low" })
expect(prepared.body.context_management).toEqual([{ type: "compaction" }])
}),
)
for (const [id, supported] of [
["gpt-6-astra", true],
["openai/gpt-6-astra", true],
["gpt-6-astra-2026-09-01", false],
["gpt-5.6-sol", false],
] as const) {
it.effect(`${supported ? "lowers" : "strips"} markers for ${id}`, () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({ model: openai(id), messages: conversation, providerOptions: { reasoningEffort: "low" } }),
)
expect(updates(prepared.body)).toHaveLength(supported ? 1 : 0)
expect(prepared.body.reasoning).toEqual({ effort: supported ? "high" : "low" })
}),
)
}
it.effect("honors the compatibility override in both directions", () =>
Effect.gen(function* () {
const enabled = yield* compileRequest(
LLM.request({
model: openai("gpt-5.5", { supportsEffortUpdates: true }),
messages: conversation,
providerOptions: { reasoningEffort: "low" },
}),
)
const disabled = yield* compileRequest(
LLM.request({
model: openai("gpt-6-astra", { supportsEffortUpdates: false }),
messages: conversation,
providerOptions: { reasoningEffort: "low" },
}),
)
expect(updates(enabled.body)).toHaveLength(1)
expect(updates(disabled.body)).toHaveLength(0)
}),
)
const checkpoint = { type: "compaction", id: "cmp_1", encrypted_content: "opaque" }
const compactRequest = LLM.request({
model: OpenAI.configure({ apiKey: "fixture" }).responses("gpt-6-astra"),
messages: conversation,
providerOptions: { reasoningEffort: "low" },
})
testEffect(
dynamicResponse(({ text, respond }) =>
Effect.sync(() => {
const body = JSON.parse(text)
expect(body.reasoning).toEqual({ effort: "high" })
expect(body.input).toEqual([
{ role: "user", content: [{ type: "input_text", text: "Before." }] },
{ type: "configuration_update", reasoning: { effort: "low" } },
{ role: "user", content: [{ type: "input_text", text: "After." }] },
{ type: "compaction_trigger" },
])
return respond(sseEvents({ type: "response.completed", response: { id: "resp_1", output: [checkpoint] } }), {
headers: { "content-type": "text/event-stream" },
})
}),
),
).effect("keeps configuration updates in the checkpoint body", () =>
Effect.gen(function* () {
const result = yield* LLMClient.compact(compactRequest, { mechanism: "trigger" })
expect(result.checkpoint.encrypted).toBe("opaque")
}),
)
testEffect(
dynamicResponse(({ request, text, respond }) =>
Effect.sync(() => {
expect(new URL(request.url).pathname).toEndWith("/responses/compact")
expect(JSON.parse(text).input).toEqual([
{ role: "user", content: [{ type: "input_text", text: "Before." }] },
{ role: "user", content: [{ type: "input_text", text: "After." }] },
])
return respond(JSON.stringify({ object: "response.compaction", output: [checkpoint] }))
}),
),
).effect("drops markers from the compaction endpoint body", () =>
Effect.gen(function* () {
const result = yield* LLMClient.compact(compactRequest, { mechanism: "endpoint" })
expect(result.replacement.map((message) => message.content[0]?.type)).toEqual(["compaction"])
}),
)
})
@@ -75,6 +75,45 @@ describe("Google Vertex providers", () => {
}),
)
it.effect("maps service tiers to the Vertex shared PayGo header", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLM.request({
model: GoogleVertex.configure({
accessToken: "vertex-token",
location: "global",
project: "vertex-project",
providerOptions: { serviceTier: "flex" },
}).model("gemini-2.5-flash"),
prompt: "Say hello.",
}),
).pipe(
Effect.provide(
dynamicResponse((input) =>
Effect.gen(function* () {
const request = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
expect(request.headers.get("x-vertex-ai-llm-shared-request-type")).toBe("flex")
expect(yield* Effect.promise(() => request.json())).not.toHaveProperty("serviceTier")
return input.respond(
sseEvents({
candidates: [
{
content: { role: "model", parts: [{ text: "Hello." }] },
finishReason: "STOP",
},
],
}),
{ headers: { "content-type": "text/event-stream" } },
)
}),
),
),
)
expect(response.text).toBe("Hello.")
}),
)
it.effect("strips function call ids Vertex does not accept from lowered bodies", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
@@ -0,0 +1,73 @@
import { expect } from "bun:test"
import { Effect, Schema } from "effect"
import { LLM, Message, ToolCallPart } from "../../src/index.js"
import { OpenAIChat } from "../../src/protocols/openai-chat.js"
import { OpenAIResponses } from "../../src/protocols/openai-responses.js"
import { Auth } from "../../src/route/auth.js"
import { compileRequest } from "../../src/route/client.js"
import { it } from "../lib/effect.js"
for (const route of [OpenAIChat.route, OpenAIResponses.route]) {
const model = route
.with({ endpoint: { baseURL: "https://api.openai.test/v1" }, auth: Auth.bearer("test") })
.model({ id: "gpt-4.1-mini" })
const chat = route.id === "openai-chat"
it.effect(`${route.id} serializes schema-valid undefined historical tool input as an empty object`, () =>
Effect.gen(function* () {
const message = Schema.decodeUnknownSync(Message)({
role: "assistant",
content: [{ type: "tool-call", id: "call_1", name: "lookup", input: undefined }],
})
expect(Schema.is(Message)(message)).toBe(true)
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
Message.user("Look up the value."),
message,
Message.tool({ id: "call_1", name: "lookup", result: "Missing input", resultType: "error" }),
],
}),
)
if (chat)
expect(prepared.body.messages).toContainEqual({
role: "assistant",
content: null,
tool_calls: [{ id: "call_1", type: "function", function: { name: "lookup", arguments: "{}" } }],
})
if (!chat)
expect(prepared.body.input).toContainEqual({
type: "function_call",
call_id: "call_1",
name: "lookup",
arguments: "{}",
})
expect(message.content[0]).toEqual({ type: "tool-call", id: "call_1", name: "lookup", input: undefined })
}),
)
it.effect(`${route.id} preserves defined historical tool inputs`, () =>
Effect.gen(function* () {
for (const [input, encoded] of [
[null, "null"],
[[], "[]"],
[42, "42"],
["invalid", '"invalid"'],
[{ value: "original" }, '{"value":"original"}'],
] as const) {
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input })]),
Message.tool({ id: "call_1", name: "lookup", result: "Invalid input", resultType: "error" }),
],
}),
)
if (chat) expect(prepared.body.messages[0].tool_calls[0].function.arguments).toBe(encoded)
if (!chat) expect(prepared.body.input[0].arguments).toBe(encoded)
}
}),
)
}
@@ -698,6 +698,54 @@ describe("OpenAI Chat route", () => {
}),
)
it.effect("preserves HTTP and HTTPS image URLs in user content", () =>
Effect.gen(function* () {
const urls = ["https://example.com/image.png?size=64#preview", "http://example.com/image.jpg"]
const prepared = yield* compileRequest(
LLM.request({
model,
prompt: urls.map((data) => ({ type: "media" as const, mediaType: "image/png", data })),
}),
)
expect(prepared.body.messages).toEqual([
{ role: "user", content: urls.map((url) => ({ type: "image_url", image_url: { url } })) },
])
}),
)
it.effect("preserves remote image URLs from tool results", () =>
Effect.gen(function* () {
const url = "https://example.com/tool-image.png?version=2"
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
Message.user("Describe the image."),
Message.assistant([ToolCallPart.make({ id: "call_image", name: "read_image", input: {} })]),
Message.tool({
id: "call_image",
name: "read_image",
resultType: "content",
result: [
{ type: "text", text: "Image attached." },
{ type: "file", mime: "image/png", uri: url },
],
}),
],
}),
)
expect(prepared.body.messages).toContainEqual({
role: "tool",
tool_call_id: "call_image",
content: "Image attached.",
})
expect(prepared.body.messages.at(-1)).toEqual({
role: "user",
content: [{ type: "image_url", image_url: { url } }],
})
}),
)
it.effect("rejects non-image media that cannot be lowered", () =>
Effect.gen(function* () {
const error = yield* compileRequest(
@@ -4,7 +4,7 @@ import { expectSessionTitle } from "../../utils/waits"
import { fixture, pageMessages } from "./session-timeline-stress.fixture"
import { installStressSessionTabs, installTimelineSettings, stressSessionHref } from "./timeline-test-helpers"
import { waitForStableTimeline } from "./session-tab-switch-probe"
import type { CatalogUpdated } from "@opencode/client/promise"
import type { ModelUpdated } from "@opencode/client/promise"
benchmark("measures retained renderer memory with a large model catalog", async ({ page, report }) => {
benchmark.setTimeout(120_000)
@@ -77,17 +77,17 @@ benchmark("measures retained renderer memory with a large model catalog", async
selected.name = "Updated catalog model"
await page.evaluate(
(event) => {
const host = window as Window & { __mockServerStream?: { push: (events: CatalogUpdated[]) => void } }
const host = window as Window & { __mockServerStream?: { push: (events: ModelUpdated[]) => void } }
if (!host.__mockServerStream) throw new Error("Missing fixture event stream")
host.__mockServerStream.push([event])
},
{
id: "evt_catalog_refresh",
id: "evt_model_refresh",
created: Date.now(),
type: "catalog.updated",
type: "model.updated",
location: { directory: fixture.directory },
data: {},
} satisfies CatalogUpdated,
} satisfies ModelUpdated,
)
await expect(page.locator('[data-action="composer-model"]')).toContainText(selected.name)
report(
@@ -11,8 +11,9 @@ test("renames, exports, and deletes a home session from its context menu", async
project: fixture.project,
pageMessages,
})
await page.route("**/api/session/*/rename", async (route) => {
const sessionID = new URL(route.request().url()).pathname.split("/").at(-2)
await page.route("**/api/session/*", async (route) => {
if (route.request().method() !== "PATCH") return route.fallback()
const sessionID = new URL(route.request().url()).pathname.split("/").at(-1)
const session = sessions.find((item) => item.id === sessionID)
const payload: unknown = route.request().postDataJSON()
if (!payload || typeof payload !== "object" || !("title" in payload) || typeof payload.title !== "string")
@@ -30,16 +30,16 @@ for (const shared of [true, false]) {
sessions: [{ id: sessionID, projectID, directory: workspace, title }],
pageMessages: () => ({ items: [] }),
})
await page.route("**/api/mcp**", async (route) => {
await page.route(/\/api\/(?:experimental\/)?mcp(?:[/?]|$)/, async (route) => {
if (route.request().method() === "OPTIONS") return route.fallback()
const url = new URL(route.request().url())
const target = url.searchParams.get("location[directory]") ?? directory
requests.push({ path: url.pathname, directory: target })
if (url.pathname === "/api/mcp/figma-desktop/connect") {
if (url.pathname === "/api/experimental/mcp/figma-desktop/connect") {
connected.add(target)
return route.fulfill({ status: 204 })
}
if (url.pathname === "/api/mcp/figma-desktop/disconnect") {
if (url.pathname === "/api/experimental/mcp/figma-desktop/disconnect") {
connected.delete(target)
return route.fulfill({ status: 204 })
}
@@ -72,7 +72,7 @@ for (const shared of [true, false]) {
await expect(toggle).toBeChecked()
await expect(toggle).toBeEnabled()
expect(connected).toEqual(new Set([workspace]))
expect(requests).toContainEqual({ path: "/api/mcp/figma-desktop/connect", directory: workspace })
expect(requests).toContainEqual({ path: "/api/experimental/mcp/figma-desktop/connect", directory: workspace })
expect(requests).toContainEqual({ path: "/api/mcp/resource", directory: workspace })
expect(requests.every((request) => request.directory === workspace)).toBe(true)
await testInfo.attach("workspace-connected", { body: await page.screenshot(), contentType: "image/png" })
@@ -82,7 +82,7 @@ for (const shared of [true, false]) {
await expect(toggle).not.toBeChecked()
await expect(toggle).toBeEnabled()
expect(connected.size).toBe(0)
expect(requests).toContainEqual({ path: "/api/mcp/figma-desktop/disconnect", directory: workspace })
expect(requests).toContainEqual({ path: "/api/experimental/mcp/figma-desktop/disconnect", directory: workspace })
expect(requests.every((request) => request.directory === workspace)).toBe(true)
})
}
@@ -109,16 +109,16 @@ for (const surface of ["popover", "dialog"] as const) {
sessions: [{ id: sessionID, projectID, directory: workspace, title }],
pageMessages: () => ({ items: [] }),
})
await page.route("**/api/mcp**", async (route) => {
await page.route(/\/api\/(?:experimental\/)?mcp(?:[/?]|$)/, async (route) => {
if (route.request().method() === "OPTIONS") return route.fallback()
const url = new URL(route.request().url())
const target = url.searchParams.get("location[directory]") ?? directory
requests.push({ path: url.pathname, directory: target })
if (url.pathname === "/api/mcp/figma-desktop/disconnect") {
if (url.pathname === "/api/experimental/mcp/figma-desktop/disconnect") {
state.status = "disabled"
return route.fulfill({ status: 204 })
}
if (url.pathname === "/api/mcp/figma-desktop/connect") {
if (url.pathname === "/api/experimental/mcp/figma-desktop/connect") {
state.status = state.fail ? "failed" : "connected"
// Connection failures are reported by the refreshed status, not the HTTP response.
return route.fulfill({ status: 204 })
@@ -167,7 +167,7 @@ for (const surface of ["popover", "dialog"] as const) {
await expect(toggle).toBeChecked({ checked: surface === "popover" })
await expect(toggle).toBeEnabled()
expect(requests.filter((request) => request.path.endsWith("/connect"))).toEqual([
{ path: "/api/mcp/figma-desktop/connect", directory: workspace },
{ path: "/api/experimental/mcp/figma-desktop/connect", directory: workspace },
])
expect(requests.every((request) => request.directory === workspace)).toBe(true)
await expect(toast).toHaveCSS("opacity", "1")
@@ -343,7 +343,7 @@ async function openDraft(page: Page, worktree = "main", options: { git?: boolean
(route) => route.fulfill({ json: { location: { directory }, data: { branch: {} } } }),
)
}
await page.route("**/api/mcp**", async (route) => {
await page.route(/\/api\/(?:experimental\/)?mcp(?:[/?]|$)/, async (route) => {
if (route.request().method() === "OPTIONS") return route.fallback()
const url = new URL(route.request().url())
const target = url.searchParams.get("location[directory]") ?? directory
@@ -423,7 +423,7 @@ test("executes a selected slash command after creating its worktree", async ({ p
.toEqual([
{
sessionID: pending.sessionID,
body: { command: "review", text: "latest commit", files: [], agents: [], skills: [], delivery: "steer" },
body: { name: "review", text: "latest commit", files: [], agents: [], skills: [], delivery: "steer" },
},
])
await expect(pending.shimmer).toHaveCount(0)
@@ -0,0 +1,212 @@
import { expect, test } from "@playwright/test"
import { timelinePresets } from "@opencode/session-ui/timeline/detail"
import { createTwoFilesPatch } from "diff"
import {
assistantMessage,
setupTimeline,
textPart,
toolPart,
userMessage,
} from "../performance/timeline-stability/fixture"
const before = Array.from({ length: 80 }, (_, index) => `export const value${index} = ${index}\n`).join("")
const after = before.replaceAll(" = ", " = 1 + ")
const files = ["src/a.ts", "src/b.ts"].map((file) => ({
file,
status: "modified",
additions: 80,
deletions: 80,
patch: createTwoFilesPatch(file, file, before, after),
}))
const scenarios = [
{
name: "grouped patch",
placement: "grouped",
tools: [toolPart("prt_sticky_patch", "patch", "completed", {}, { metadata: { files } })],
files: ["a", "b"],
title: false,
},
{
name: "standalone patch",
placement: "separate",
tools: [toolPart("prt_sticky_patch", "patch", "completed", {}, { metadata: { files } })],
files: ["a", "b"],
title: false,
},
{
name: "grouped edit",
placement: "grouped",
tools: [
toolPart(
"prt_sticky_edit",
"edit",
"completed",
{ path: "src/a.ts", oldString: before, newString: after },
{ metadata: { files: [files[0]] } },
),
],
files: ["a"],
title: false,
},
{
name: "running edit input fallback",
placement: "grouped",
tools: [toolPart("prt_sticky_edit", "edit", "running", { path: "src/a.ts", oldString: before, newString: after })],
files: ["a"],
title: false,
},
{
name: "grouped write",
placement: "grouped",
tools: [toolPart("prt_sticky_write", "write", "completed", { path: "src/a.ts", content: after })],
files: ["a"],
title: false,
},
{
name: "running write input fallback",
placement: "grouped",
tools: [toolPart("prt_sticky_write", "write", "running", { path: "src/a.ts", content: after })],
files: ["a"],
title: false,
},
{
name: "merged edit write and patch",
placement: "separate",
tools: [
toolPart("prt_sticky_edit", "edit", "completed", {}, { metadata: { files: [files[0]] } }),
toolPart("prt_sticky_write", "write", "completed", { path: "src/b.ts", content: after }),
toolPart(
"prt_sticky_patch",
"patch",
"completed",
{},
{ metadata: { files: [{ ...files[0], file: "src/c.ts" }] } },
),
],
files: ["a", "b", "c"],
title: false,
},
{
name: "created and deleted patch files",
placement: "grouped",
tools: [
toolPart(
"prt_sticky_patch",
"patch",
"completed",
{},
{
metadata: {
files: [
{
file: "src/a.ts",
status: "added",
additions: 80,
deletions: 0,
patch: createTwoFilesPatch("src/a.ts", "src/a.ts", "", after),
},
{
file: "src/b.ts",
status: "deleted",
additions: 0,
deletions: 80,
patch: createTwoFilesPatch("src/b.ts", "src/b.ts", before, ""),
},
],
},
},
),
],
files: ["a", "b"],
title: false,
},
] as const
for (const scenario of scenarios) {
for (const width of [1400, 390]) {
for (const direction of ["ltr", "rtl"]) {
test(`${scenario.name}: file headers stay flush at ${width}px in ${direction}`, async ({ page }, info) => {
await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([...scenario.tools, textPart("prt_after_patch", "Following explanation.\n\n".repeat(60))]),
],
settings: {
timelineDetail: {
...timelinePresets[2].value,
edit: { placement: scenario.placement, details: "collapsed" },
},
},
reducedMotion: true,
viewport: { width, height: 900 },
})
await page.evaluate((direction) => (document.documentElement.dir = direction), direction)
if (scenario.placement === "grouped") {
await page.locator('[data-component="context-tool-group-trigger"]').click()
}
const patch = page.locator('[data-scope="apply-patch"]')
await expect(patch).toHaveCount(1)
const scroller = page.locator('[data-slot="session-timeline-scroll"] .scroll-view__viewport')
const toolTitle = scroller.locator('[data-slot="collapsible-trigger"][data-locked]')
await expect(toolTitle).toHaveCount(scenario.title ? 1 : 0)
await expect(scroller.locator("[data-session-title]")).toHaveCount(width === 1400 ? 1 : 0)
for (const file of scenario.files) {
const name = new RegExp(`${file}\\.ts`)
const trigger = patch.getByRole("button", { name })
const header = patch.getByRole("heading", { name })
await expect(trigger).toHaveAttribute("aria-expanded", "false")
await trigger.click()
await expect(trigger).toHaveAttribute("aria-expanded", "true")
const content = patch.getByRole("region", { name })
await expect
.poll(() => content.evaluate((element) => element.getBoundingClientRect().height))
.toBeGreaterThan(900)
// Leave follow-latest mode before positioning the viewport inside this file.
await scroller.hover()
await page.mouse.wheel(0, -100)
await content.evaluate((element) => {
const viewport = element.closest<HTMLElement>(".scroll-view__viewport")!
viewport.scrollTop += element.getBoundingClientRect().top - viewport.getBoundingClientRect().top + 160
})
await expect
.poll(() =>
content.evaluate((element) => {
const viewport = element.closest<HTMLElement>(".scroll-view__viewport")!
return element.getBoundingClientRect().top - viewport.getBoundingClientRect().top
}),
)
.toBeLessThan(0)
await expect
.poll(() =>
header.evaluate((element) => {
const viewport = element.closest<HTMLElement>(".scroll-view__viewport")!
const title = viewport.querySelector("[data-session-title]")?.firstElementChild
const toolTitle = element
.closest('[data-component="edit-tool"]')
?.querySelector('[data-slot="collapsible-trigger"][data-locked]')
const top = viewport.getBoundingClientRect().top + (title?.getBoundingClientRect().height ?? 0)
const rect = element.getBoundingClientRect()
const trigger = element.querySelector("button")!
return {
gap: Math.abs(rect.top - top - (toolTitle?.getBoundingClientRect().height ?? 0)),
titleGap: toolTitle ? Math.abs(toolTitle.getBoundingClientRect().top - top) : 0,
clickable: trigger.contains(
document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2),
),
}
}),
)
.toEqual({ gap: 0, titleGap: 0, clickable: true })
await page.screenshot({ path: info.outputPath(`${file}.png`) })
}
await scroller.evaluate((element) => (element.scrollTop = element.scrollHeight))
await expect(
patch.getByRole("heading", { name: new RegExp(`${scenario.files.at(-1)}\\.ts`) }),
).not.toBeInViewport()
})
}
}
}
@@ -12,7 +12,7 @@ const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${pr
type InboxRow = {
id: string
sessionID: string
timeCreated: number
time: { created: number }
type: "user"
payload: { text: string; metadata?: Record<string, unknown> }
delivery: "steer" | "queue"
@@ -22,7 +22,7 @@ function createQueueMock(seed: string[], messages: SessionMessageInfo[] = []) {
const rows: InboxRow[] = seed.map((text, index) => ({
id: `inb_seed_${index + 1}`,
sessionID,
timeCreated: 1700000000000 + index,
time: { created: 1700000000000 + index },
type: "user",
payload: { text },
delivery: "queue",
@@ -59,7 +59,7 @@ function createQueueMock(seed: string[], messages: SessionMessageInfo[] = []) {
const row: InboxRow = {
id: typeof input.body.id === "string" ? input.body.id : `inb_mock_${sequence}`,
sessionID: input.sessionID,
timeCreated: Date.now(),
time: { created: Date.now() },
type: "user",
payload: {
text: typeof input.body.text === "string" ? input.body.text : "",
@@ -11,9 +11,9 @@ test.beforeEach(async ({ page }) => {
project: fixture.project,
pageMessages,
})
await page.route("**/api/session/*/rename", async (route) => {
if (route.request().method() !== "POST") return route.fallback()
const id = new URL(route.request().url()).pathname.split("/").at(-2)
await page.route("**/api/session/*", async (route) => {
if (route.request().method() !== "PATCH") return route.fallback()
const id = new URL(route.request().url()).pathname.split("/").at(-1)
const session = sessions.find((item) => item.id === id)
const payload: unknown = route.request().postDataJSON()
if (
@@ -59,8 +59,10 @@ test("cancels the session heading with Escape", async ({ page }) => {
})
test("keeps the draft when saving the session heading fails", async ({ page }) => {
await page.route("**/api/session/*/rename", (route) =>
route.fulfill({ status: 500, headers: { "access-control-allow-origin": "*" } }),
await page.route("**/api/session/*", (route) =>
route.request().method() === "PATCH"
? route.fulfill({ status: 500, headers: { "access-control-allow-origin": "*" } })
: route.fallback(),
)
await page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true }).click()
const input = page.locator('input[data-slot="session-title-child"]')
@@ -6,7 +6,7 @@ test("every MCP row hit area toggles exactly once and keeps the submenu open", a
await mockStressTimeline(page)
const state = { enabled: true }
const writes: string[] = []
await page.route("**/api/mcp**", (route) => {
await page.route(/\/api\/(?:experimental\/)?mcp(?:[/?]|$)/, (route) => {
if (route.request().method() === "OPTIONS") return route.fallback()
const url = new URL(route.request().url())
const directory = url.searchParams.get("location[directory]")
@@ -62,7 +62,7 @@ test("every MCP row hit area toggles exactly once and keeps the submenu open", a
await expect(submenu).toBeVisible()
if (target === "keyboard") await expect(toggle).toBeFocused()
expect(writes).toHaveLength(index + 1)
expect(writes[index]).toBe(`/api/mcp/figma/${enabled ? "connect" : "disconnect"}`)
expect(writes[index]).toBe(`/api/experimental/mcp/figma/${enabled ? "connect" : "disconnect"}`)
}
})
@@ -72,7 +72,7 @@ test("MCP authentication starts before a slow resource catalog finishes", async
const attempts: string[] = []
const resources = Promise.withResolvers<void>()
await context.route("https://auth.example.test/**", (route) => route.fulfill({ body: "Sign in" }))
await page.route("**/api/mcp**", async (route) => {
await page.route(/\/api\/(?:experimental\/)?mcp(?:[/?]|$)/, async (route) => {
if (route.request().method() === "OPTIONS") return route.fallback()
const url = new URL(route.request().url())
if (url.pathname.endsWith("/connect")) {
@@ -113,7 +113,7 @@ test("passes through non-event fetches", async ({ page }) => {
return response.json()
})
expect(health).toEqual({ version: "2.0.0", pid: 1, urls: ["http://localhost"] })
expect(health).toEqual({ version: "2.0.0", pid: 1, urls: [] })
await expect.poll(async () => (await timeline.transport.connections()).length).toBe(1)
})
@@ -96,7 +96,7 @@ test("single-server settings expose scoped pages without a server picker", async
(request) => request.method() === "PATCH" && new URL(request.url()).pathname === "/api/experimental/config",
)
await page.getByRole("option", { name: "bash", exact: true }).click()
expect((await updated).postDataJSON()).toEqual({ shell: "/bin/bash" })
expect((await updated).postDataJSON()).toEqual({ shell: "bash" })
})
test("project settings open as a nested autosaving view", async ({ page }) => {
@@ -335,7 +335,7 @@ fixture("upgrades the legacy shared precache only after old tabs close", async (
fixture("does not substitute cached HTML for API or missing asset navigations", async ({ page, site }) => {
await install(page, site.url)
const api = await page.goto(`${site.url}/api/status`)
expect(await api?.json()).toEqual({ version: "test", pid: 1, urls: [site.url] })
expect(await api?.json()).toEqual({ version: "test", pid: 1, urls: ["http://localhost"] })
expect(api?.fromServiceWorker()).toBe(false)
const asset = await page.goto(`${site.url}/_assets/missing.js`)
expect(asset?.status()).toBe(404)
+2 -2
View File
@@ -233,7 +233,7 @@ const Group = HttpApiGroup.make("mock")
}),
)
.add(
HttpApiEndpoint.post("sessionRename", "/api/session/:sessionID/rename", {
HttpApiEndpoint.patch("sessionRename", "/api/session/:sessionID", {
params: SessionParams,
payload: JsonPayload,
success: NoContent,
@@ -254,7 +254,7 @@ const Group = HttpApiGroup.make("mock")
}),
)
.add(
HttpApiEndpoint.post("sessionRevertClear", "/api/session/:sessionID/revert/clear", {
HttpApiEndpoint.delete("sessionRevertClear", "/api/session/:sessionID/revert", {
params: SessionParams,
success: NoContent,
}),
+1 -1
View File
@@ -437,7 +437,7 @@ function mockHandlers(config: MockServerConfig, state: { cursors: Map<string, st
data: {
id: typeof body.id === "string" ? body.id : `inb_mock_${Date.now()}`,
sessionID: ctx.params.sessionID,
timeCreated: Date.now(),
time: { created: Date.now() },
type: "user",
payload: {
text: typeof body.text === "string" ? body.text : "",
@@ -187,7 +187,7 @@ function ComposerStory(props: {
)
},
onStop: () =>
setStory("activity", props.continueOnStop ? "POST /interrupt · continue: true" : "Stop requested"),
setStory("activity", props.continueOnStop ? "POST /interrupt · resume: true" : "Stop requested"),
},
},
})
+1 -1
View File
@@ -635,7 +635,7 @@ describe("Composer submission", () => {
expect(requests).toEqual([
{
sessionID: target.id,
command: "review",
name: "review",
text: "https://github.com/example/repo/pull/1",
files: [],
agents: [],
+2 -3
View File
@@ -1,6 +1,5 @@
import { SessionMessage } from "@opencode/schema/session-message"
import type { SessionMessageUser } from "@opencode/client/promise"
import { Event } from "@opencode/schema/event"
import type { Accessor } from "solid-js"
import type { PromptHistoryComment } from "./history/entry"
import type { ImageAttachmentPart, Prompt } from "./state"
@@ -278,7 +277,7 @@ function restoreSubmission(
}
async function sendShell(session: ComposerSession, value: ComposerSubmission) {
await session.api.shell({ sessionID: session.id, id: Event.ID.create(), command: value.text })
await session.api.shell({ sessionID: session.id, id: value.id, command: value.text })
}
function findCommand(commands: ReturnType<ComposerSubmitInput["commands"]>, text: string) {
@@ -301,7 +300,7 @@ async function sendCommand(
if (value.delivery === "steer") await applySelection(session, value.selection, track)
await session.api.command({
sessionID: session.id,
command: command.command,
name: command.command,
text: command.arguments,
files: request.files.map((file) => ({ uri: file.uri, name: file.name, mention: file.mention })),
agents: request.agents,
+1 -1
View File
@@ -576,7 +576,7 @@ export const dict = {
"context.stats.lastActivity": "Last Activity",
"context.usage.tokens": "Tokens",
"context.usage.usage": "Context Usage",
"context.usage.usage": "Context",
"context.usage.cost": "Cost",
"context.usage.clickToView": "Click to view context",
"context.usage.view": "View context usage",
@@ -69,7 +69,7 @@ export const DialogFork: Component = () => {
const dir = base64Encode(location().directory)
serverSDK.api.session
.fork({ sessionID, boundary: { type: "before", messageID: item.id } })
.fork({ sessionID, before: item.id })
.then((forked) => {
data.session.remember(forked)
dialog.close()
+1 -1
View File
@@ -37,7 +37,7 @@ export function createActiveComposerAdapter(input: {
}),
interrupt: () =>
server.api.session
.interrupt({ sessionID: id, continue: true })
.interrupt({ sessionID: id, resume: true })
.then(() => undefined)
.catch(() => undefined),
}
@@ -6,7 +6,7 @@ const queued = [
{
id: "msg_original",
sessionID: "ses_1",
timeCreated: 1,
time: { created: 1 },
type: "user",
delivery: "queue",
payload: { text: "original" },
@@ -14,7 +14,7 @@ const queued = [
{
id: "msg_replacement",
sessionID: "ses_1",
timeCreated: 2,
time: { created: 2 },
type: "user",
delivery: "queue",
payload: { text: "edited" },
+1 -1
View File
@@ -70,7 +70,7 @@ export function createSessionRevert(input: {
.map((item) => item.id)
void server.api.session.inbox
.list({ sessionID })
.then((rows) => rows.filter((row) => row.type === "user" && row.timeCreated <= cutoff).map((row) => row.id))
.then((rows) => rows.filter((row) => row.type === "user" && row.time.created <= cutoff).map((row) => row.id))
.catch(() => [])
.then((authoritative) => {
new Set([...local, ...authoritative]).forEach(
@@ -23,7 +23,7 @@ describe("visibleTimelineMessages", () => {
const steer = {
id: "msg_3",
sessionID: "ses_1",
timeCreated: 3,
time: { created: 3 },
type: "user",
delivery: "steer",
payload: { text: "queued" },
@@ -135,7 +135,7 @@ describe("visibleTimelineMessages", () => {
{
id: "msg_3",
sessionID: "ses_1",
timeCreated: 3,
time: { created: 3 },
type: "user",
delivery: "queue",
payload: { text: "queued" },
@@ -105,7 +105,7 @@ export const SettingsProviders: Component<{
const credentials = integration.data?.connections.filter((item) => item.type === "credential") ?? []
if (credentials.length === 0) throw new Error(`No removable credentials found for ${name}`)
await Promise.all(
credentials.map((credential) => serverSdk.api.credential.remove({ credentialID: credential.id, location })),
credentials.map((credential) => serverSdk.api.credential.remove({ credentialID: credential.id })),
)
showToast({
variant: "success",
+1 -1
View File
@@ -369,7 +369,7 @@ export async function streamTurn(input: {
}
const assistant = assistantMessageID
? await input.client.session
.message({ sessionID: input.sessionID, messageID: assistantMessageID })
.message.get({ sessionID: input.sessionID, messageID: assistantMessageID })
.catch(() => undefined)
: undefined
return response(
+1 -2
View File
@@ -263,7 +263,6 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti
forkSession: async (params) => {
const forked = await input.client.session.fork({
sessionID: params.sessionId,
boundary: { type: "through" },
})
const state = await attach(forked, forked.location.directory, params.mcpServers ?? [])
await replay(state)
@@ -378,7 +377,7 @@ async function submitPrompt(client: OpenCodeClient, session: Attached, prompt: P
return client.session.command(
{
sessionID: session.id,
command: prompt.command.name,
name: prompt.command.name,
text: prompt.slash?.args ?? "",
files: prompt.files,
delivery: "steer",
@@ -3,7 +3,7 @@ import { Effect, Option } from "effect"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { handlePromptErrors, requireInteractive } from "../../../ui/prompt"
import { createClient, loadIntegrations, location, request } from "./shared"
import { createClient, loadIntegrations, request } from "./shared"
import { chooseCredential, chooseIntegration } from "./account"
export default Runtime.handler(
@@ -35,7 +35,7 @@ const logout = Effect.fn("cli.auth.logout.run")(function* (input: {
const credentialID = yield* chooseCredential(integration, "log out", input.credential)
const progress = spinner()
progress.start("Removing credential...")
yield* request((signal) => client.credential.remove({ credentialID, location }, { signal })).pipe(
yield* request((signal) => client.credential.remove({ credentialID }, { signal })).pipe(
Effect.tap(() => Effect.sync(() => progress.stop(`Removed account from ${integration.name}`))),
Effect.tapCause(() => Effect.sync(() => progress.stop("Failed to remove credential", 1))),
)
@@ -3,7 +3,7 @@ import { Effect, Option } from "effect"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { handlePromptErrors, requireInteractive } from "../../../ui/prompt"
import { createClient, loadIntegrations, location, request } from "./shared"
import { createClient, loadIntegrations, request } from "./shared"
import { chooseCredential, chooseIntegration } from "./account"
export default Runtime.handler(
@@ -35,7 +35,7 @@ const switchAccount = Effect.fn("cli.auth.switch.run")(function* (input: {
const credentialID = yield* chooseCredential(integration, "switch to", input.credential)
const progress = spinner()
progress.start("Switching account...")
yield* request((signal) => client.credential.activate({ credentialID, location }, { signal })).pipe(
yield* request((signal) => client.credential.activate({ credentialID }, { signal })).pipe(
Effect.tap(() => Effect.sync(() => progress.stop(`Switched account for ${integration.name}`))),
Effect.tapCause(() => Effect.sync(() => progress.stop("Failed to switch account", 1))),
)
@@ -29,7 +29,7 @@ export default Runtime.handler(
yield* Effect.forEach(
credentials,
(connection) => Effect.promise(() => client.credential.remove({ credentialID: connection.id, location })),
(connection) => Effect.promise(() => client.credential.remove({ credentialID: connection.id })),
{ discard: true },
)
process.stdout.write(`Removed OAuth credentials for ${input.name}` + EOL)
@@ -39,7 +39,7 @@ export default Runtime.handler(
}),
)
const response = yield* Effect.promise(() =>
fetch(new URL("/api/session/import", server.endpoint.url), {
fetch(new URL("/api/experimental/session/import", server.endpoint.url), {
method: "POST",
headers: { ...Service.headers(server.endpoint), "content-type": "application/json" },
body: JSON.stringify({
+2 -2
View File
@@ -112,7 +112,7 @@ async function selectSession(input: {
return {
session: input.fork
? await input.client.session
.fork({ sessionID: explicit.id, boundary: { type: "through" } }, ...requestOptions(input.signal))
.fork({ sessionID: explicit.id }, ...requestOptions(input.signal))
.catch((error) => {
throw new SessionTargetMutationError(error)
})
@@ -126,7 +126,7 @@ async function selectSession(input: {
return {
session: input.fork
? await input.client.session
.fork({ sessionID: selected.id, boundary: { type: "through" } }, ...requestOptions(input.signal))
.fork({ sessionID: selected.id }, ...requestOptions(input.signal))
.catch((error) => {
throw new SessionTargetMutationError(error)
})
@@ -236,6 +236,7 @@ describe("acp service directory behavior", () => {
headers: [{ name: "Authorization", value: "Bearer x" }],
}
let created = 0
const mcp = "/api/experimental/mcp/"
await using fixture = makeACPFixture({
fetch(request) {
if (request.method === "POST" && request.path === "/api/session") {
@@ -245,7 +246,7 @@ describe("acp service directory behavior", () => {
if (request.method === "GET" && request.path === "/api/session/ses_1") {
return Response.json({ data: makeSession("ses_1") })
}
if (request.method === "PUT" && request.path.startsWith("/api/mcp/")) {
if (request.method === "PUT" && request.path.startsWith(mcp)) {
return new Response(null, { status: 204 })
}
return undefined
@@ -257,9 +258,9 @@ describe("acp service directory behavior", () => {
await fixture.service.resumeSession({ cwd: "/workspace", sessionId: "ses_1", mcpServers: [changed] })
await fixture.service.newSession({ cwd: "/workspace", mcpServers: [local] })
const adds = fixture.requests.filter((request) => request.method === "PUT" && request.path.startsWith("/api/mcp/"))
const adds = fixture.requests.filter((request) => request.method === "PUT" && request.path.startsWith(mcp))
expect(adds).toHaveLength(4)
expect(adds.filter((request) => request.path === "/api/mcp/tools").map((request) => request.body)).toEqual([
expect(adds.filter((request) => request.path === `${mcp}tools`).map((request) => request.body)).toEqual([
{
config: {
type: "local",
@@ -282,7 +283,7 @@ describe("acp service directory behavior", () => {
},
},
])
expect(adds.find((request) => request.path === "/api/mcp/docs")?.body).toEqual({
expect(adds.find((request) => request.path === `${mcp}docs`)?.body).toEqual({
config: {
type: "remote",
url: "https://example.com/mcp",
@@ -168,7 +168,7 @@ describe("acp service lifecycle", () => {
method: "POST",
path: "/api/session/ses_loaded/fork",
query: {},
body: { boundary: { type: "through" } },
body: {},
})
})
+3 -2
View File
@@ -24,7 +24,8 @@ describe("acp service", () => {
if (url.pathname === "/api/command")
return Response.json({ location, data: [{ name: "review", template: "" }] })
if (url.pathname === "/api/session" && request.method === "POST") return Response.json({ data: session })
if (url.pathname === "/api/mcp/docs" && request.method === "PUT") return new Response(null, { status: 204 })
if (url.pathname === "/api/experimental/mcp/docs" && request.method === "PUT")
return new Response(null, { status: 204 })
return new Response(null, { status: 404 })
},
})
@@ -53,7 +54,7 @@ describe("acp service", () => {
expect(result.configOptions?.map((option) => option.id)).toEqual(["model", "effort", "mode"])
expect(requests).toContainEqual({
method: "PUT",
path: "/api/mcp/docs",
path: "/api/experimental/mcp/docs",
body: {
config: { type: "local", command: ["bun", "docs.ts"], environment: { TOKEN: "x" } },
},
+3 -1
View File
@@ -68,6 +68,7 @@ export async function createAcpFixture(options: { readonly skill?: string } = {}
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-cli-acp-"))
const home = path.join(root, "workspace")
const config = path.join(root, "config")
const models = path.join(root, "models.json")
const skills = path.join(root, "skills")
await Promise.all([fs.mkdir(home, { recursive: true }), fs.mkdir(config, { recursive: true })])
if (options.skill) {
@@ -93,6 +94,7 @@ export async function createAcpFixture(options: { readonly skill?: string } = {}
path.join(config, "opencode.json"),
JSON.stringify(verifierConfig(`http://127.0.0.1:${llm.port}/v1`, options.skill ? skills : undefined)),
)
await Bun.write(models, "{}")
const processes = new Set<AcpProcess>()
return {
@@ -106,7 +108,7 @@ export async function createAcpFixture(options: { readonly skill?: string } = {}
OPENCODE_CONFIG: undefined,
OPENCODE_CONFIG_CONTENT: undefined,
OPENCODE_DISABLE_AUTOUPDATE: "true",
OPENCODE_MODELS_PATH: undefined,
OPENCODE_MODELS_PATH: models,
...extraEnv,
}),
})
+4 -4
View File
@@ -62,7 +62,7 @@ test("export is raw by default and supports explicit sanitization", async () =>
const url = new URL(request.url)
if (url.pathname === "/api/status") return status()
if (url.pathname === `/api/session/${info.id}`) return Response.json({ data: info })
if (url.pathname === `/api/session/${info.id}/export`) {
if (url.pathname === `/api/experimental/session/${info.id}/export`) {
sanitization.push(url.searchParams.get("sanitize") ?? "")
return Response.json({ data: url.searchParams.get("sanitize") === "true" ? sanitizedTransfer : transfer })
}
@@ -128,7 +128,7 @@ test("export reports a missing session without a stack trace", async () => {
fetch(request) {
const url = new URL(request.url)
if (url.pathname === "/api/status") return status()
if (url.pathname === `/api/session/${sessionID}/export`) {
if (url.pathname === `/api/experimental/session/${sessionID}/export`) {
return Response.json(
{ _tag: "SessionNotFoundError", sessionID, message: `Session not found: ${sessionID}` },
{ status: 404 },
@@ -165,7 +165,7 @@ test("import validates a file and sends it to the resolved location", async () =
project: { id: "global", directory: root, canonical: root },
})
}
if (url.pathname === "/api/session/import") {
if (url.pathname === "/api/experimental/session/import") {
imported = await request.json()
return Response.json({ data: { ...info, location: { directory: root } } })
}
@@ -208,7 +208,7 @@ test("import reports an existing session without a stack trace", async () => {
project: { id: "global", directory: root, canonical: root },
})
}
if (url.pathname === "/api/session/import") return new Response("Conflict", { status: 409 })
if (url.pathname === "/api/experimental/session/import") return new Response("Conflict", { status: 409 })
return new Response("Not found", { status: 404 })
},
})
+1 -1
View File
@@ -265,7 +265,7 @@ async function run(input: {
values.push(...input.turn(messageID))
wake?.()
wake = undefined
return ok({ id: messageID, sessionID: "ses_1", timeCreated: 1 }) as never
return ok({ id: messageID, sessionID: "ses_1", time: { created: 1 } }) as never
})
await runNonInteractivePrompt({
client: sdk,
+1 -6
View File
@@ -1,12 +1,7 @@
import type { ModelApi, ProviderApi, WebsearchApi } from "./api/api.js"
import type { WebsearchApi } from "./api/api.js"
export type { RpcApi, RpcClient } from "./rpc.js"
export type * from "./api/api.js"
export type WebSearchApi<E = never> = WebsearchApi<E>
export interface CatalogApi<E = never> {
readonly provider: ProviderApi<E>
readonly model: ModelApi<E>
}
+16 -25
View File
@@ -16,10 +16,10 @@ import type { SessionInbox } from "@opencode/schema/session-inbox"
import type { PromptInput } from "@opencode/schema/prompt-input"
import type { AgentAttachment } from "@opencode/schema/prompt"
import type { Skill } from "@opencode/schema/skill"
import type { Event } from "@opencode/schema/event"
import type { FileDiff } from "@opencode/schema/file-diff"
import type { InstructionEntry } from "@opencode/schema/instruction-entry"
import type { Schema } from "effect"
import type { Event } from "@opencode/schema/event"
import type { EventLog } from "@opencode/schema/event-log"
import type { Shell } from "@opencode/schema/shell"
import type { Provider } from "@opencode/schema/provider"
@@ -217,7 +217,7 @@ export type SessionRemoveInput = { readonly sessionID: Session.ID }
export type SessionRemoveOutput = void
export type SessionRemoveOperation<E = never> = (input: SessionRemoveInput) => Effect.Effect<SessionRemoveOutput, E>
export type SessionForkInput = { readonly sessionID: Session.ID; readonly boundary: Session.ForkRequestBoundary }
export type SessionForkInput = { readonly sessionID: Session.ID; readonly before?: SessionMessage.ID | undefined }
export type SessionForkOutput = Session.Info
export type SessionForkOperation<E = never> = (input: SessionForkInput) => Effect.Effect<SessionForkOutput, E>
@@ -261,7 +261,7 @@ export type SessionPromptOperation<E = never> = (input: SessionPromptInput) => E
export type SessionCommandInput = {
readonly sessionID: Session.ID
readonly command: string
readonly name: string
readonly text: string
readonly files?: ReadonlyArray<PromptInput.FileAttachment> | undefined
readonly agents?: ReadonlyArray<AgentAttachment> | undefined
@@ -273,8 +273,7 @@ export type SessionCommandOperation<E = never> = (input: SessionCommandInput) =>
export type SessionSkillInput = {
readonly sessionID: Session.ID
readonly id?: SessionMessage.ID | undefined
readonly skill: Skill.ID
readonly id: Skill.ID
readonly resume?: boolean | undefined
}
export type SessionSkillOutput = void
@@ -296,7 +295,7 @@ export type SessionSyntheticOperation<E = never> = (
export type SessionShellInput = {
readonly sessionID: Session.ID
readonly id?: Event.ID | undefined
readonly id?: SessionMessage.ID | undefined
readonly command: string
}
export type SessionShellOutput = void
@@ -1308,7 +1307,7 @@ export type SessionLogOutput =
| EventLog.Synced
export type SessionLogOperation<E = never> = (input: SessionLogInput) => Stream.Stream<SessionLogOutput, E>
export type SessionInterruptInput = { readonly sessionID: Session.ID; readonly continue?: boolean | undefined }
export type SessionInterruptInput = { readonly sessionID: Session.ID; readonly resume?: boolean | undefined }
export type SessionInterruptOutput = { readonly interrupted: boolean }
export type SessionInterruptOperation<E = never> = (
input: SessionInterruptInput,
@@ -1320,9 +1319,11 @@ export type SessionBackgroundOperation<E = never> = (
input: SessionBackgroundInput,
) => Effect.Effect<SessionBackgroundOutput, E>
export type SessionMessageInput = { readonly sessionID: Session.ID; readonly messageID: SessionMessage.ID }
export type SessionMessageOutput = SessionMessage.Info
export type SessionMessageOperation<E = never> = (input: SessionMessageInput) => Effect.Effect<SessionMessageOutput, E>
export type SessionMessageGetInput = { readonly sessionID: Session.ID; readonly messageID: SessionMessage.ID }
export type SessionMessageGetOutput = SessionMessage.Info
export type SessionMessageGetOperation<E = never> = (
input: SessionMessageGetInput,
) => Effect.Effect<SessionMessageGetOutput, E>
export type SessionEnvironmentInput = {
readonly sessionID: Session.ID
@@ -1333,7 +1334,7 @@ export type SessionEnvironmentOperation<E = never> = (
input: SessionEnvironmentInput,
) => Effect.Effect<SessionEnvironmentOutput, E>
export type SessionViewInput = { readonly sessionID: Session.ID; readonly idle: number }
export type SessionViewInput = { readonly sessionID: Session.ID; readonly idle: DateTime.Utc }
export type SessionViewOutput = void
export type SessionViewOperation<E = never> = (input: SessionViewInput) => Effect.Effect<SessionViewOutput, E>
@@ -1382,7 +1383,7 @@ export interface SessionApi<E = never> {
readonly log: SessionLogOperation<E>
readonly interrupt: SessionInterruptOperation<E>
readonly background: SessionBackgroundOperation<E>
readonly message: SessionMessageOperation<E>
readonly message: { readonly get: SessionMessageGetOperation<E> }
readonly environment: SessionEnvironmentOperation<E>
readonly view: SessionViewOperation<E>
}
@@ -1641,29 +1642,19 @@ export interface McpApi<E = never> {
readonly resource: { readonly catalog: McpResourceCatalogOperation<E> }
}
export type CredentialUpdateInput = {
readonly credentialID: Credential.ID
readonly location?: { readonly directory?: string | undefined } | undefined
readonly label: string
}
export type CredentialUpdateInput = { readonly credentialID: Credential.ID; readonly label: string }
export type CredentialUpdateOutput = void
export type CredentialUpdateOperation<E = never> = (
input: CredentialUpdateInput,
) => Effect.Effect<CredentialUpdateOutput, E>
export type CredentialActivateInput = {
readonly credentialID: Credential.ID
readonly location?: { readonly directory?: string | undefined } | undefined
}
export type CredentialActivateInput = { readonly credentialID: Credential.ID }
export type CredentialActivateOutput = void
export type CredentialActivateOperation<E = never> = (
input: CredentialActivateInput,
) => Effect.Effect<CredentialActivateOutput, E>
export type CredentialRemoveInput = {
readonly credentialID: Credential.ID
readonly location?: { readonly directory?: string | undefined } | undefined
}
export type CredentialRemoveInput = { readonly credentialID: Credential.ID }
export type CredentialRemoveOutput = void
export type CredentialRemoveOperation<E = never> = (
input: CredentialRemoveInput,
+15 -21
View File
@@ -89,8 +89,8 @@ import type {
SessionInterruptOutput,
SessionBackgroundInput,
SessionBackgroundOutput,
SessionMessageInput,
SessionMessageOutput,
SessionMessageGetInput,
SessionMessageGetOutput,
SessionEnvironmentInput,
SessionEnvironmentOutput,
SessionViewInput,
@@ -430,7 +430,7 @@ const EndpointSessionRemove = (raw: RawClient["server.session"]) => (input: Sess
const EndpointSessionFork = (raw: RawClient["server.session"]) => (input: SessionForkInput) =>
preserveEffect<SessionForkOutput>()(
raw["session.fork"]({ params: { sessionID: input["sessionID"] }, payload: { boundary: input["boundary"] } }).pipe(
raw["session.fork"]({ params: { sessionID: input["sessionID"] }, payload: { before: input["before"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
),
@@ -490,7 +490,7 @@ const EndpointSessionCommand = (raw: RawClient["server.session"]) => (input: Ses
raw["session.command"]({
params: { sessionID: input["sessionID"] },
payload: {
command: input["command"],
name: input["name"],
text: input["text"],
files: input["files"],
agents: input["agents"],
@@ -504,7 +504,7 @@ const EndpointSessionSkill = (raw: RawClient["server.session"]) => (input: Sessi
preserveEffect<SessionSkillOutput>()(
raw["session.skill"]({
params: { sessionID: input["sessionID"] },
payload: { id: input["id"], skill: input["skill"], resume: input["resume"] },
payload: { id: input["id"], resume: input["resume"] },
}).pipe(Effect.mapError(mapClientError)),
)
@@ -668,10 +668,9 @@ const EndpointSessionLog = (raw: RawClient["server.session"]) => (input: Session
const EndpointSessionInterrupt = (raw: RawClient["server.session"]) => (input: SessionInterruptInput) =>
preserveEffect<SessionInterruptOutput>()(
raw["session.interrupt"]({
params: { sessionID: input["sessionID"] },
query: { continue: input["continue"] },
}).pipe(Effect.mapError(mapClientError)),
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] }, query: { resume: input["resume"] } }).pipe(
Effect.mapError(mapClientError),
),
)
const EndpointSessionBackground = (raw: RawClient["server.session"]) => (input: SessionBackgroundInput) =>
@@ -679,8 +678,8 @@ const EndpointSessionBackground = (raw: RawClient["server.session"]) => (input:
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
)
const EndpointSessionMessage = (raw: RawClient["server.session"]) => (input: SessionMessageInput) =>
preserveEffect<SessionMessageOutput>()(
const EndpointSessionMessageGet = (raw: RawClient["server.session"]) => (input: SessionMessageGetInput) =>
preserveEffect<SessionMessageGetOutput>()(
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
@@ -747,7 +746,7 @@ const adaptGroupSession = (raw: RawClient["server.session"]) => ({
log: EndpointSessionLog(raw),
interrupt: EndpointSessionInterrupt(raw),
background: EndpointSessionBackground(raw),
message: EndpointSessionMessage(raw),
message: { get: EndpointSessionMessageGet(raw) },
environment: EndpointSessionEnvironment(raw),
view: EndpointSessionView(raw),
})
@@ -969,25 +968,20 @@ const EndpointCredentialUpdate = (raw: RawClient["server.credential"]) => (input
preserveEffect<CredentialUpdateOutput>()(
raw["credential.update"]({
params: { credentialID: input["credentialID"] },
query: { location: input["location"] },
payload: { label: input["label"] },
}).pipe(Effect.mapError(mapClientError)),
)
const EndpointCredentialActivate = (raw: RawClient["server.credential"]) => (input: CredentialActivateInput) =>
preserveEffect<CredentialActivateOutput>()(
raw["credential.activate"]({
params: { credentialID: input["credentialID"] },
query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError)),
raw["credential.activate"]({ params: { credentialID: input["credentialID"] } }).pipe(
Effect.mapError(mapClientError),
),
)
const EndpointCredentialRemove = (raw: RawClient["server.credential"]) => (input: CredentialRemoveInput) =>
preserveEffect<CredentialRemoveOutput>()(
raw["credential.remove"]({
params: { credentialID: input["credentialID"] },
query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError)),
raw["credential.remove"]({ params: { credentialID: input["credentialID"] } }).pipe(Effect.mapError(mapClientError)),
)
const adaptGroupCredential = (raw: RawClient["server.credential"]) => ({
-1
View File
@@ -8,7 +8,6 @@ export { OpenCode } from "./client.js"
export type {
AgentApi,
AppApi,
CatalogApi,
CommandApi,
ConfigApi,
EventApi,
-5
View File
@@ -21,8 +21,3 @@ export type SessionApi = Client["session"]
export type SkillApi = Client["skill"]
export type VcsApi = Client["vcs"]
export type WorktreeApi = Client["worktree"]
export interface CatalogApi {
readonly provider: ProviderApi
readonly model: ModelApi
}
+41 -42
View File
@@ -83,8 +83,8 @@ import type {
SessionInterruptOutput,
SessionBackgroundInput,
SessionBackgroundOutput,
SessionMessageInput,
SessionMessageOutput,
SessionMessageGetInput,
SessionMessageGetOutput,
SessionEnvironmentInput,
SessionEnvironmentOutput,
SessionViewInput,
@@ -517,7 +517,7 @@ export function make(options: ClientOptions) {
request<{ readonly data: SessionStatsOutput }>(
{
method: "GET",
path: `/api/session/stats`,
path: `/api/experimental/session/stats`,
query: {
from: input?.["from"],
to: input?.["to"],
@@ -555,7 +555,7 @@ export function make(options: ClientOptions) {
request<{ readonly data: SessionImportOutput }>(
{
method: "POST",
path: `/api/session/import`,
path: `/api/experimental/session/import`,
body: { info: input["info"], messages: input["messages"], location: input["location"] },
successStatus: 200,
declaredStatuses: [400, 401, 404, 409],
@@ -567,7 +567,7 @@ export function make(options: ClientOptions) {
request<{ readonly data: SessionExportOutput }>(
{
method: "GET",
path: `/api/session/${encodeURIComponent(input.sessionID)}/export`,
path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/export`,
query: { sanitize: input["sanitize"] },
successStatus: 200,
declaredStatuses: [400, 401, 404, 500],
@@ -613,7 +613,7 @@ export function make(options: ClientOptions) {
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/fork`,
body: { boundary: input["boundary"] },
body: { before: input["before"] },
successStatus: 200,
declaredStatuses: [400, 401, 404],
empty: false,
@@ -647,8 +647,8 @@ export function make(options: ClientOptions) {
rename: (input: SessionRenameInput, requestOptions?: RequestOptions) =>
request<SessionRenameOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/rename`,
method: "PATCH",
path: `/api/session/${encodeURIComponent(input.sessionID)}`,
body: { title: input["title"] },
successStatus: 204,
declaredStatuses: [400, 401, 404],
@@ -695,7 +695,7 @@ export function make(options: ClientOptions) {
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/command`,
body: {
command: input["command"],
name: input["name"],
text: input["text"],
files: input["files"],
agents: input["agents"],
@@ -712,8 +712,8 @@ export function make(options: ClientOptions) {
request<SessionSkillOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/skill`,
body: { id: input["id"], skill: input["skill"], resume: input["resume"] },
path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/skill`,
body: { id: input["id"], resume: input["resume"] },
successStatus: 204,
declaredStatuses: [400, 401, 404],
empty: true,
@@ -767,7 +767,7 @@ export function make(options: ClientOptions) {
request<SessionWaitOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/wait`,
path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/wait`,
successStatus: 204,
declaredStatuses: [400, 401, 404, 503],
empty: true,
@@ -790,8 +790,8 @@ export function make(options: ClientOptions) {
clear: (input: SessionRevertClearInput, requestOptions?: RequestOptions) =>
request<SessionRevertClearOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`,
method: "DELETE",
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert`,
successStatus: 204,
declaredStatuses: [400, 401, 404, 409, 500],
empty: true,
@@ -885,7 +885,7 @@ export function make(options: ClientOptions) {
request<{ readonly data: SessionInstructionsEntryListOutput }>(
{
method: "GET",
path: `/api/session/${encodeURIComponent(input.sessionID)}/instructions/entries`,
path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/instructions/entries`,
successStatus: 200,
declaredStatuses: [400, 401, 404],
empty: false,
@@ -896,7 +896,7 @@ export function make(options: ClientOptions) {
request<SessionInstructionsEntryPutOutput>(
{
method: "PUT",
path: `/api/session/${encodeURIComponent(input.sessionID)}/instructions/entries/${encodeURIComponent(input.key)}`,
path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/instructions/entries/${encodeURIComponent(input.key)}`,
body: { value: input["value"] },
successStatus: 204,
declaredStatuses: [400, 401, 404, 413],
@@ -908,7 +908,7 @@ export function make(options: ClientOptions) {
request<SessionInstructionsEntryRemoveOutput>(
{
method: "DELETE",
path: `/api/session/${encodeURIComponent(input.sessionID)}/instructions/entries/${encodeURIComponent(input.key)}`,
path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/instructions/entries/${encodeURIComponent(input.key)}`,
successStatus: 204,
declaredStatuses: [400, 401, 404],
empty: true,
@@ -946,7 +946,7 @@ export function make(options: ClientOptions) {
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/interrupt`,
query: { continue: input["continue"] },
query: { resume: input["resume"] },
successStatus: 200,
declaredStatuses: [400, 401, 404],
empty: false,
@@ -964,22 +964,24 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
message: (input: SessionMessageInput, requestOptions?: RequestOptions) =>
request<{ readonly data: SessionMessageOutput }>(
{
method: "GET",
path: `/api/session/${encodeURIComponent(input.sessionID)}/message/${encodeURIComponent(input.messageID)}`,
successStatus: 200,
declaredStatuses: [400, 401, 404],
empty: false,
},
requestOptions,
).then((value) => value.data),
message: {
get: (input: SessionMessageGetInput, requestOptions?: RequestOptions) =>
request<{ readonly data: SessionMessageGetOutput }>(
{
method: "GET",
path: `/api/session/${encodeURIComponent(input.sessionID)}/message/${encodeURIComponent(input.messageID)}`,
successStatus: 200,
declaredStatuses: [400, 401, 404],
empty: false,
},
requestOptions,
).then((value) => value.data),
},
environment: (input: SessionEnvironmentInput, requestOptions?: RequestOptions) =>
request<SessionEnvironmentOutput>(
{
method: "PUT",
path: `/api/session/${encodeURIComponent(input.sessionID)}/environment`,
path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/environment`,
body: { variables: input["variables"] },
successStatus: 204,
declaredStatuses: [400, 401, 404],
@@ -1129,7 +1131,7 @@ export function make(options: ClientOptions) {
query: { location: input["location"] },
body: { key: input["key"], answer: input["answer"], label: input["label"] },
successStatus: 204,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 404],
empty: true,
},
requestOptions,
@@ -1156,7 +1158,7 @@ export function make(options: ClientOptions) {
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth/${encodeURIComponent(input.attemptID)}`,
query: { location: input["location"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 404],
empty: false,
},
requestOptions,
@@ -1169,7 +1171,7 @@ export function make(options: ClientOptions) {
query: { location: input["location"] },
body: { code: input["code"] },
successStatus: 204,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 404],
empty: true,
},
requestOptions,
@@ -1196,7 +1198,7 @@ export function make(options: ClientOptions) {
query: { location: input["location"] },
body: { methodID: input["methodID"], label: input["label"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 404],
empty: false,
},
requestOptions,
@@ -1208,7 +1210,7 @@ export function make(options: ClientOptions) {
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/command/${encodeURIComponent(input.attemptID)}`,
query: { location: input["location"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 404],
empty: false,
},
requestOptions,
@@ -1244,7 +1246,7 @@ export function make(options: ClientOptions) {
request<McpAddOutput>(
{
method: "PUT",
path: `/api/mcp/${encodeURIComponent(input.server)}`,
path: `/api/experimental/mcp/${encodeURIComponent(input.server)}`,
query: { location: input["location"] },
body: { config: input["config"] },
successStatus: 204,
@@ -1257,7 +1259,7 @@ export function make(options: ClientOptions) {
request<McpRemoveOutput>(
{
method: "DELETE",
path: `/api/mcp/${encodeURIComponent(input.server)}`,
path: `/api/experimental/mcp/${encodeURIComponent(input.server)}`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [400, 401, 404],
@@ -1269,7 +1271,7 @@ export function make(options: ClientOptions) {
request<McpConnectOutput>(
{
method: "POST",
path: `/api/mcp/${encodeURIComponent(input.server)}/connect`,
path: `/api/experimental/mcp/${encodeURIComponent(input.server)}/connect`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [400, 401, 404],
@@ -1281,7 +1283,7 @@ export function make(options: ClientOptions) {
request<McpDisconnectOutput>(
{
method: "POST",
path: `/api/mcp/${encodeURIComponent(input.server)}/disconnect`,
path: `/api/experimental/mcp/${encodeURIComponent(input.server)}/disconnect`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [400, 401, 404],
@@ -1310,7 +1312,6 @@ export function make(options: ClientOptions) {
{
method: "PATCH",
path: `/api/credential/${encodeURIComponent(input.credentialID)}`,
query: { location: input["location"] },
body: { label: input["label"] },
successStatus: 204,
declaredStatuses: [400, 401],
@@ -1323,7 +1324,6 @@ export function make(options: ClientOptions) {
{
method: "POST",
path: `/api/credential/${encodeURIComponent(input.credentialID)}/activate`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [400, 401],
empty: true,
@@ -1335,7 +1335,6 @@ export function make(options: ClientOptions) {
{
method: "DELETE",
path: `/api/credential/${encodeURIComponent(input.credentialID)}`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [400, 401],
empty: true,
+58 -46
View File
@@ -286,7 +286,7 @@ export type McpStatusDisabled = { status: "disabled" }
export type McpStatusFailed = { status: "failed"; error: string }
export type McpStatusNeedsAuth = { status: "needs_auth" }
export type McpStatusNeedsAuth = { status: "needs_auth"; error: string }
export type McpResource = { server: string; name: string; uri: string; description?: string; mimeType?: string }
@@ -422,6 +422,8 @@ export type WebSearchProvider = { id: string; name: string }
export type WebSearchResult = { url: string; title?: string; content?: string; time: { published?: number } }
export type McpProtocol = "legacy" | "auto" | "2026-07-28"
export type ConfigWorktree = { directory: string }
export type ConfigShellOption = { path: string; name: string; acceptable: boolean }
@@ -527,7 +529,7 @@ export type SessionProviderContext = { version: 1; provenance: SessionProviderCo
export type SessionInboxSynthetic = {
id: string
sessionID: string
timeCreated: number
time: { created: number }
type: "synthetic"
payload: SessionInboxSyntheticPayload
delivery: SessionInboxDelivery
@@ -536,7 +538,7 @@ export type SessionInboxSynthetic = {
export type SessionInboxCompaction = {
id: string
sessionID: string
timeCreated: number
time: { created: number }
type: "compaction"
payload: SessionInboxCompactionPayload
delivery: SessionInboxDelivery
@@ -849,11 +851,20 @@ export type IntegrationUpdated = {
data: {}
}
export type CatalogUpdated = {
export type ProviderUpdated = {
id: string
created: number
metadata?: { [x: string]: any }
type: "catalog.updated"
type: "provider.updated"
location?: LocationRef
data: {}
}
export type ModelUpdated = {
id: string
created: number
metadata?: { [x: string]: any }
type: "model.updated"
location?: LocationRef
data: {}
}
@@ -1618,7 +1629,7 @@ export type VcsInfo = { branch: VcsBranch }
export type SessionInboxMove = {
id: string
sessionID: string
timeCreated: number
time: { created: number }
type: "move"
delivery: SessionInboxDelivery
payload: SessionInboxMovePayload
@@ -1990,6 +2001,7 @@ export type ConfigEntry =
disabled?: boolean
codemode?: boolean
timeout?: { startup?: number; catalog?: number; execution?: number }
protocol?: McpProtocol
}
| {
type: "remote"
@@ -2007,6 +2019,7 @@ export type ConfigEntry =
disabled?: boolean
codemode?: boolean
timeout?: { startup?: number; catalog?: number; execution?: number }
protocol?: McpProtocol
}
}
}
@@ -2094,7 +2107,7 @@ export type ConfigEntry =
export type SessionInboxUser = {
id: string
sessionID: string
timeCreated: number
time: { created: number }
type: "user"
payload: SessionInboxUserPayload
delivery: SessionInboxDelivery
@@ -2294,7 +2307,8 @@ export type V2Event =
| CredentialUpdated
| CredentialSwitched
| IntegrationUpdated
| CatalogUpdated
| ProviderUpdated
| ModelUpdated
| AgentUpdated
| SessionCreated
| SessionAgentSelected
@@ -2510,6 +2524,24 @@ export type IntegrationNotFoundError = {
export const isIntegrationNotFoundError = (value: unknown): value is IntegrationNotFoundError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "IntegrationNotFoundError"
export type IntegrationAttemptNotFoundError = {
readonly _tag: "IntegrationAttemptNotFoundError"
readonly integrationID: string
readonly attemptID: string
readonly message: string
}
export const isIntegrationAttemptNotFoundError = (value: unknown): value is IntegrationAttemptNotFoundError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "IntegrationAttemptNotFoundError"
export type IntegrationMethodNotFoundError = {
readonly _tag: "IntegrationMethodNotFoundError"
readonly integrationID: string
readonly methodID: string
readonly message: string
}
export const isIntegrationMethodNotFoundError = (value: unknown): value is IntegrationMethodNotFoundError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "IntegrationMethodNotFoundError"
export type McpServerNotFoundError = {
readonly _tag: "McpServerNotFoundError"
readonly server: string
@@ -3832,9 +3864,7 @@ export type SessionRemoveOutput = void
export type SessionForkInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"]
readonly boundary: {
readonly boundary: { readonly type: "before"; readonly messageID: string } | { readonly type: "through" }
}["boundary"]
readonly before?: { readonly before?: string | undefined }["before"]
}
export type SessionForkOutput = { data: SessionInfo }["data"]
@@ -4046,8 +4076,8 @@ export type SessionPromptOutput = { data: SessionInboxUser }["data"]
export type SessionCommandInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"]
readonly command: {
readonly command: string
readonly name: {
readonly name: string
readonly text: string
readonly files?: ReadonlyArray<{
readonly uri: string
@@ -4064,9 +4094,9 @@ export type SessionCommandInput = {
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: ("steer" | "queue") | null
}["command"]
}["name"]
readonly text: {
readonly command: string
readonly name: string
readonly text: string
readonly files?: ReadonlyArray<{
readonly uri: string
@@ -4085,7 +4115,7 @@ export type SessionCommandInput = {
readonly delivery?: ("steer" | "queue") | null
}["text"]
readonly files?: {
readonly command: string
readonly name: string
readonly text: string
readonly files?: ReadonlyArray<{
readonly uri: string
@@ -4104,7 +4134,7 @@ export type SessionCommandInput = {
readonly delivery?: ("steer" | "queue") | null
}["files"]
readonly agents?: {
readonly command: string
readonly name: string
readonly text: string
readonly files?: ReadonlyArray<{
readonly uri: string
@@ -4123,7 +4153,7 @@ export type SessionCommandInput = {
readonly delivery?: ("steer" | "queue") | null
}["agents"]
readonly skills?: {
readonly command: string
readonly name: string
readonly text: string
readonly files?: ReadonlyArray<{
readonly uri: string
@@ -4142,7 +4172,7 @@ export type SessionCommandInput = {
readonly delivery?: ("steer" | "queue") | null
}["skills"]
readonly delivery?: {
readonly command: string
readonly name: string
readonly text: string
readonly files?: ReadonlyArray<{
readonly uri: string
@@ -4166,21 +4196,8 @@ export type SessionCommandOutput = void
export type SessionSkillInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"]
readonly id?: {
readonly id?: string | undefined
readonly skill: string
readonly resume?: boolean | undefined
}["id"]
readonly skill: {
readonly id?: string | undefined
readonly skill: string
readonly resume?: boolean | undefined
}["skill"]
readonly resume?: {
readonly id?: string | undefined
readonly skill: string
readonly resume?: boolean | undefined
}["resume"]
readonly id: { readonly id: string; readonly resume?: boolean | undefined }["id"]
readonly resume?: { readonly id: string; readonly resume?: boolean | undefined }["resume"]
}
export type SessionSkillOutput = void
@@ -4364,7 +4381,7 @@ export type SessionLogOutput = SessionLogItem
export type SessionInterruptInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"]
readonly continue?: { readonly continue?: boolean | undefined }["continue"]
readonly resume?: { readonly resume?: boolean | undefined }["resume"]
}
export type SessionInterruptOutput = SessionInterruptResponse
@@ -4373,12 +4390,12 @@ export type SessionBackgroundInput = { readonly sessionID: { readonly sessionID:
export type SessionBackgroundOutput = void
export type SessionMessageInput = {
export type SessionMessageGetInput = {
readonly sessionID: { readonly sessionID: string; readonly messageID: string }["sessionID"]
readonly messageID: { readonly sessionID: string; readonly messageID: string }["messageID"]
}
export type SessionMessageOutput = { data: SessionMessageInfo }["data"]
export type SessionMessageGetOutput = { data: SessionMessageInfo }["data"]
export type SessionEnvironmentInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"]
@@ -4639,6 +4656,7 @@ export type McpAddInput = {
readonly disabled?: boolean
readonly codemode?: boolean
readonly timeout?: { readonly startup?: number; readonly catalog?: number; readonly execution?: number }
readonly protocol?: "legacy" | "auto" | "2026-07-28"
}
| {
readonly type: "remote"
@@ -4656,6 +4674,7 @@ export type McpAddInput = {
readonly disabled?: boolean
readonly codemode?: boolean
readonly timeout?: { readonly startup?: number; readonly catalog?: number; readonly execution?: number }
readonly protocol?: "legacy" | "auto" | "2026-07-28"
}
}["config"]
}
@@ -4691,23 +4710,16 @@ export type McpResourceCatalogOutput = { location: LocationPublicRef; data: McpR
export type CredentialUpdateInput = {
readonly credentialID: { readonly credentialID: string }["credentialID"]
readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly label: { readonly label: string }["label"]
}
export type CredentialUpdateOutput = void
export type CredentialActivateInput = {
readonly credentialID: { readonly credentialID: string }["credentialID"]
readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
}
export type CredentialActivateInput = { readonly credentialID: { readonly credentialID: string }["credentialID"] }
export type CredentialActivateOutput = void
export type CredentialRemoveInput = {
readonly credentialID: { readonly credentialID: string }["credentialID"]
readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
}
export type CredentialRemoveInput = { readonly credentialID: { readonly credentialID: string }["credentialID"] }
export type CredentialRemoveOutput = void
-1
View File
@@ -4,7 +4,6 @@ export * from "./generated/index.js"
export { OpenCode } from "./client.js"
export type {
AgentApi,
CatalogApi,
CommandApi,
ConfigApi,
EventApi,
+12 -9
View File
@@ -389,8 +389,8 @@ export function createData(config: CreateDataInput) {
message.update(item.sessionID, (draft, index) => {
const row =
item.type === "user"
? { id: item.id, type: "user" as const, ...item.payload, time: { created: item.timeCreated } }
: { id: item.id, type: "synthetic" as const, ...item.payload, time: { created: item.timeCreated } }
? { id: item.id, type: "user" as const, ...item.payload, time: { created: item.time.created } }
: { id: item.id, type: "synthetic" as const, ...item.payload, time: { created: item.time.created } }
const position = index.get(item.id)
if (position === undefined) return message.append(draft, index, row)
draft[position] = row
@@ -671,7 +671,7 @@ export function createData(config: CreateDataInput) {
})
refresh(() =>
api()
.session.message({ sessionID: event.data.sessionID, messageID: messageIDFromEvent(event.id) })
.session.message.get({ sessionID: event.data.sessionID, messageID: messageIDFromEvent(event.id) })
.then((item) => {
message.update(event.data.sessionID, (draft, index) => {
const position = index.get(item.id)
@@ -772,7 +772,7 @@ export function createData(config: CreateDataInput) {
admitLocal({
id: event.data.inboxID,
sessionID: event.data.sessionID,
timeCreated: event.created,
time: { created: event.created },
...event.data.item,
})
if (event.data.item.type === "compaction") {
@@ -1187,10 +1187,13 @@ export function createData(config: CreateDataInput) {
if (!event.location) return
const location = event.location
switch (event.type) {
case "catalog.updated":
result.location.model.invalidate(location)
case "provider.updated":
result.location.provider.invalidate(location)
refresh(() => Promise.all([result.location.model.sync(location), result.location.provider.sync(location)]))
refresh(() => result.location.provider.sync(location))
break
case "model.updated":
result.location.model.invalidate(location)
refresh(() => result.location.model.sync(location))
break
case "agent.updated":
result.location.agent.invalidate(location)
@@ -1466,7 +1469,7 @@ export function createData(config: CreateDataInput) {
admitLocal({
id,
sessionID: input.sessionID,
timeCreated: Date.now(),
time: { created: Date.now() },
type: "compaction",
delivery: "steer",
payload: {},
@@ -1517,7 +1520,7 @@ export function createData(config: CreateDataInput) {
admitLocal({
id,
sessionID: request.sessionID,
timeCreated: Date.now(),
time: { created: Date.now() },
type: "user",
delivery: request.delivery ?? "steer",
// Files and skills stay off the optimistic row: their durable
+7 -7
View File
@@ -99,17 +99,17 @@ test("session instructions methods use the public HTTP contract", async () => {
expect(requests).toEqual([
{
method: "GET",
url: "http://localhost:3000/api/session/ses_test/instructions/entries",
url: "http://localhost:3000/api/experimental/session/ses_test/instructions/entries",
body: undefined,
},
{
method: "PUT",
url: "http://localhost:3000/api/session/ses_test/instructions/entries/review-notes",
url: "http://localhost:3000/api/experimental/session/ses_test/instructions/entries/review-notes",
body: { value: { text: "Check the diff", priority: 1 } },
},
{
method: "DELETE",
url: "http://localhost:3000/api/session/ses_test/instructions/entries/review-notes",
url: "http://localhost:3000/api/experimental/session/ses_test/instructions/entries/review-notes",
body: undefined,
},
])
@@ -257,7 +257,7 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
.log({ sessionID: Session.ID.make("ses_test"), after: Event.Seq.make(0) })
.pipe(Stream.runCollect)
const interrupted = yield* client.session.interrupt({ sessionID: Session.ID.make("ses_test") })
const message = yield* client.session.message({
const message = yield* client.session.message.get({
sessionID: Session.ID.make("ses_test"),
messageID: SessionMessage.ID.make("msg_model"),
})
@@ -276,7 +276,7 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
expect(result.created.id).toBe("ses_test")
expect(Object.getPrototypeOf(result.admitted)).toBe(Object.prototype)
expect(Object.getPrototypeOf(result.admitted.payload)).toBe(Object.prototype)
expect(DateTime.toEpochMillis(result.admitted.timeCreated)).toBe(1_717_171_717_000)
expect(DateTime.toEpochMillis(result.admitted.time.created)).toBe(1_717_171_717_000)
expect(result.context).toEqual([])
expect(logQueries[0]).toEqual({ after: "0" })
expect(requests).toContainEqual({ method: "POST", url: "http://localhost:3000/api/session/ses_test/view" })
@@ -336,7 +336,7 @@ const admission = {
type: "user",
payload: { text: "Hello" },
delivery: "steer",
timeCreated: 1_717_171_717_000,
time: { created: 1_717_171_717_000 },
},
}
@@ -347,7 +347,7 @@ const compactionAdmission = {
delivery: "queue",
id: "msg_compaction",
sessionID: "ses_test",
timeCreated: 1_717_171_717_000,
time: { created: 1_717_171_717_000 },
},
}
+11 -13
View File
@@ -239,12 +239,10 @@ test("credential.activate uses the public HTTP contract", async () => {
},
})
await client.credential.activate({ credentialID: "cred_work", location: { directory: "/tmp/project" } })
await client.credential.activate({ credentialID: "cred_work" })
expect(request?.method).toBe("POST")
expect(request?.url).toBe(
"http://localhost:3000/api/credential/cred_work/activate?location%5Bdirectory%5D=%2Ftmp%2Fproject",
)
expect(request?.url).toBe("http://localhost:3000/api/credential/cred_work/activate")
})
test("integration connections optionally submit a form answer", async () => {
@@ -489,17 +487,17 @@ test("session instructions methods use the public HTTP contract", async () => {
expect(requests).toEqual([
{
method: "GET",
url: "http://localhost:3000/api/session/ses_test/instructions/entries",
url: "http://localhost:3000/api/experimental/session/ses_test/instructions/entries",
body: undefined,
},
{
method: "PUT",
url: "http://localhost:3000/api/session/ses_test/instructions/entries/review-notes",
url: "http://localhost:3000/api/experimental/session/ses_test/instructions/entries/review-notes",
body: { value: { text: "Check the diff", priority: 1 } },
},
{
method: "DELETE",
url: "http://localhost:3000/api/session/ses_test/instructions/entries/review-notes",
url: "http://localhost:3000/api/experimental/session/ses_test/instructions/entries/review-notes",
body: undefined,
},
])
@@ -511,7 +509,7 @@ test("session.inbox.list uses the public HTTP contract", async () => {
{
id: "msg_pending",
sessionID: "ses_test",
timeCreated: 1_717_171_717_000,
time: { created: 1_717_171_717_000 },
type: "user",
payload: { text: "Fix the failing tests" },
delivery: "steer",
@@ -861,7 +859,7 @@ test("session methods use the public HTTP contract", async () => {
const log = []
for await (const item of client.session.log({ sessionID: "ses_test", after: 0 })) log.push(item)
const interrupted = await client.session.interrupt({ sessionID: "ses_test", continue: true })
const message = await client.session.message({ sessionID: "ses_test", messageID: "msg_model" })
const message = await client.session.message.get({ sessionID: "ses_test", messageID: "msg_model" })
expect(page.cursor.next).toBe("next")
expect(page.data[0].time).toMatchObject({ idle: 1_717_171_717_002, viewed: 1_717_171_717_001 })
@@ -885,7 +883,7 @@ test("session methods use the public HTTP contract", async () => {
["POST", "http://localhost:3000/api/session/ses_test/generate"],
["POST", "http://localhost:3000/api/session/ses_test/synthetic"],
["POST", "http://localhost:3000/api/session/ses_test/compact"],
["POST", "http://localhost:3000/api/session/ses_test/wait"],
["POST", "http://localhost:3000/api/experimental/session/ses_test/wait"],
["GET", "http://localhost:3000/api/session/ses_test/context"],
["GET", "http://localhost:3000/api/experimental/session/ses_test/log?after=0"],
["POST", "http://localhost:3000/api/session/ses_test/interrupt?continue=true"],
@@ -971,7 +969,7 @@ const admission = {
type: "user",
data: { text: "Hello" },
delivery: "steer",
timeCreated: 1_717_171_717_000,
time: { created: 1_717_171_717_000 },
},
}
@@ -982,7 +980,7 @@ const syntheticAdmission = {
type: "synthetic",
data: { text: "Completed" },
delivery: "queue",
timeCreated: 1_717_171_717_000,
time: { created: 1_717_171_717_000 },
},
}
@@ -991,7 +989,7 @@ const compactionAdmission = {
type: "compaction",
id: "msg_compaction",
sessionID: "ses_test",
timeCreated: 1_717_171_717_000,
time: { created: 1_717_171_717_000 },
},
}
@@ -348,7 +348,7 @@ const event = { id: "evt_compact", created: 10, durable: { aggregateID: sessionI
const item = (id: string, timeCreated = 10): SessionInboxCompaction => ({
id,
sessionID,
timeCreated,
time: { created: timeCreated },
type: "compaction",
delivery: "steer",
payload: {},
+153 -7
View File
@@ -2,7 +2,7 @@ import { expect, test } from "bun:test"
import { getEventListeners } from "node:events"
import { createRoot } from "solid-js"
import { createData, type CreateDataInput } from "../src/solid"
import { OpenCode, type OpenCodeEvent, type Project, type SessionInfo } from "../src/promise"
import { OpenCode, type ModelInfo, type OpenCodeEvent, type Project, type SessionInfo } from "../src/promise"
const session = (viewed: number): SessionInfo => ({
id: "ses_refresh",
@@ -406,9 +406,7 @@ test("refreshes global credential events across every loaded location", async ()
}
listeners.forEach((listener) => listener({ name: updated.type, details: updated }))
await wait(() => requests.length === 2)
expect(
requests.map((url) => [url.pathname, url.searchParams.get("location[directory]")]),
).toEqual([
expect(requests.map((url) => [url.pathname, url.searchParams.get("location[directory]")])).toEqual([
["/api/integration", "/project"],
["/api/integration", "/other"],
])
@@ -423,9 +421,7 @@ test("refreshes global credential events across every loaded location", async ()
}
listeners.forEach((listener) => listener({ name: switched.type, details: switched }))
await wait(() => requests.length === 4)
expect(
requests.map((url) => [url.pathname, url.searchParams.get("location[directory]")]),
).toEqual(
expect(requests.map((url) => [url.pathname, url.searchParams.get("location[directory]")])).toEqual(
expect.arrayContaining([
["/api/model", "/project"],
["/api/provider", "/project"],
@@ -443,6 +439,140 @@ test("refreshes global credential events across every loaded location", async ()
}
})
for (const resource of ["provider", "model"] as const) {
test(`refreshes ${resource} values only at the location named by ${resource}.updated`, async () => {
const listeners = new Set<Parameters<CreateDataInput["event"]["listen"]>[0]>()
const requests: URL[] = []
const current = { name: "Before" }
const api = OpenCode.make({
baseUrl: "http://opencode.local",
fetch: async (input, init) => {
const request = input instanceof Request ? input : new Request(input, init)
const url = new URL(request.url)
requests.push(url)
const directory = url.searchParams.get("location[directory]") ?? "/project"
if (url.pathname !== "/api/provider" && url.pathname !== "/api/model")
throw new Error(`Unexpected request: ${request.url}`)
return Response.json({
location: { directory, project: { id: directory, directory, canonical: directory } },
data:
url.pathname === "/api/provider"
? [{ id: "company", name: current.name, activation: "enabled", package: "aisdk:@ai-sdk/openai" }]
: [modelInfo(current.name)],
})
},
})
const setup = createRoot((dispose) => ({
data: createData({
api: () => api,
directory: "/project",
event: {
on: () => () => {},
listen(handler) {
listeners.add(handler)
return () => listeners.delete(handler)
},
},
connection: { status: () => "connected" },
}),
dispose,
}))
const project = { directory: "/project" }
const other = { directory: "/other" }
const unaffected = resource === "provider" ? "model" : "provider"
try {
await Promise.all(
[project, other].flatMap((location) => [
setup.data.location.provider.sync(location),
setup.data.location.model.sync(location),
]),
)
const untouchedResource = setup.data.location[unaffected].list(project)
const untouchedLocation = setup.data.location[resource].list(other)
requests.length = 0
current.name = "After"
const event: OpenCodeEvent = {
id: `evt_${resource}_updated`,
created: 1,
type: `${resource}.updated`,
location: project,
data: {},
}
listeners.forEach((listener) => listener({ name: event.type, details: event }))
await wait(() => setup.data.location[resource].list(project)?.[0]?.name === "After")
expect(requests.map((url) => [url.pathname, url.searchParams.get("location[directory]")])).toEqual([
[`/api/${resource}`, "/project"],
])
expect(setup.data.location[unaffected].list(project)).toBe(untouchedResource)
expect(setup.data.location[resource].list(other)).toBe(untouchedLocation)
expect(setup.data.location[unaffected].list(project)?.[0]?.name).toBe("Before")
expect(setup.data.location[resource].list(other)?.[0]?.name).toBe("Before")
} finally {
setup.dispose()
}
})
}
test("revalidates model discovery when an update overtakes an in-flight model list", async () => {
const started = Promise.withResolvers<void>()
const release = Promise.withResolvers<void>()
const listeners = new Set<Parameters<CreateDataInput["event"]["listen"]>[0]>()
let requests = 0
const api = OpenCode.make({
baseUrl: "http://opencode.local",
fetch: async (input, init) => {
const request = input instanceof Request ? input : new Request(input, init)
if (new URL(request.url).pathname !== "/api/model") throw new Error(`Unexpected request: ${request.url}`)
const initial = ++requests === 1
if (initial) {
started.resolve()
await release.promise
}
return Response.json({
location: { directory: "/project" },
data: [modelInfo(initial ? "Before discovery" : "Discovered model")],
})
},
})
const setup = createRoot((dispose) => ({
data: createData({
api: () => api,
directory: "/project",
event: {
on: () => () => {},
listen(handler) {
listeners.add(handler)
return () => listeners.delete(handler)
},
},
connection: { status: () => "connected" },
}),
dispose,
}))
try {
const initial = setup.data.location.model.sync()
await started.promise
const event: OpenCodeEvent = {
id: "evt_models_discovered",
created: 1,
type: "model.updated",
location: { directory: "/project" },
data: {},
}
listeners.forEach((listener) => listener({ name: event.type, details: event }))
release.resolve()
await initial
await wait(() => setup.data.location.model.list()?.[0]?.name === "Discovered model")
expect(requests).toBe(2)
} finally {
release.resolve()
setup.dispose()
}
})
test("refreshes references for the location an update names", async () => {
const listeners = new Set<Parameters<CreateDataInput["event"]["listen"]>[0]>()
const requests: URL[] = []
@@ -867,6 +997,22 @@ test("projects background user shell metadata from durable shell data", () => {
}
})
function modelInfo(name: string): ModelInfo {
return {
id: "chat",
modelID: "chat",
providerID: "company",
name,
capabilities: { tools: true, input: ["text"], output: ["text"] },
variants: [],
time: { released: 0 },
cost: [],
status: "active",
enabled: true,
limit: { context: 8192, output: 1024 },
}
}
function activityFixture(read: () => Response | Promise<Response>) {
const listeners = new Set<Parameters<CreateDataInput["event"]["listen"]>[0]>()
const api = OpenCode.make({
+1 -1
View File
@@ -142,7 +142,7 @@ function fixture(read?: (url: URL) => Promise<Response | void>) {
type: "user",
delivery: "steer",
payload: { text: "pending" },
timeCreated: 1,
time: { created: 1 },
},
],
})
+54 -3
View File
@@ -20,12 +20,23 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] Top-level `await` and `return` through the program's implicit async-function scope.
- [x] Explicit `return`, final top-level expression as a REPL-style result, and `null` when no value is produced.
- [x] Program results use JSON-like boundaries, with `undefined` and non-finite numbers normalized to `null`. Tool
arguments follow JSON serialization semantics before their schema applies (see the tools section).
arguments follow JSON serialization semantics before their schema applies (see the tools section). Own
`__proto__` keys are dropped wherever a host object crosses to the host, so merging tool inputs or results
cannot replace a prototype; `JSON.stringify` still emits the key, like JS, since a string cannot pollute.
- [x] Values `JSON.stringify` would flatten to `{}` cross the host boundary in a useful form instead: a Set as an
array, a RegExp as `"/source/flags"`, a URLSearchParams as its query string. A Map still crosses as `{}`.
Functions, generators, promises, and extension handles are rejected with a hint. In-program `JSON.stringify`
keeps JS behavior for all of these.
- [x] Live Date, RegExp, Map, Set, URL, and URLSearchParams values inside CodeMode.
- [x] Tool calls through the host-provided `tools` tree only.
- [x] The global `search(...)` built-in: synchronous tool discovery that counts as an admitted tool call and is
shadowable by program declarations like other globals.
- [x] Cooperative timeout, an optional total tool-call limit, output bounding, and unrestricted tool-call concurrency.
- [x] The timeout fires between interpreter steps, so one built-in is bounded in what it may build: strings up to
2^24 characters (`repeat`, `pad*`, `concat`, `join`, `+`, template literals, `JSON.stringify`), arrays up to
10,000,000 elements (`Array(n)`, `length =`, `Array.from`, `split`, `matchAll`, `concat`, `flat`; below the JS
maximum of 2^32 - 1), and 10,000 pending promises at once. Exceeding one throws a `RangeError`. A single regular
expression match can still run long on a pathological pattern; the host regex engine has no interrupt hook.
- [ ] Strict-mode early errors: duplicate parameter names, `yield` as an identifier, and a trailing comma after a
rest parameter are accepted unless the program itself begins with `"use strict"`.
- [ ] Valid JavaScript rejected by TypeScript transpilation before interpretation, such as `in` inside a destructuring
@@ -100,6 +111,10 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] Function declarations, function expressions, and arrow functions.
- [x] Synchronous and `async` functions.
- [x] Closures, recursion, default parameters, rest parameters, and destructured parameters.
- [x] A call depth limit of 10000: deeper nesting throws a catchable `RangeError: Maximum call stack size exceeded`
at the overflowing call instead of running until the timeout. Callbacks invoked by built-ins count below the
call that invoked the built-in, and a resumed `await` starts from depth 0 as in JS, so long async chains such
as recursive pagination are unaffected.
- [x] Expression and block function bodies.
- [x] User callbacks for the supported Array, Map, Set, URLSearchParams, sort, string-replacement, and `Array.from`
mapper APIs, with one shared acceptance rule everywhere including promise reactions.
@@ -257,8 +272,9 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] `Object.is` for supported data values.
- [x] `Object.groupBy` over finite collections and custom synchronous iterators/generators, with string-key coercion
and plain-object results.
- [x] `Object.prototype` methods on values: `toString` (`"[object Array]"`), `toLocaleString`, `valueOf`,
`hasOwnProperty`, `isPrototypeOf`, and `propertyIsEnumerable`.
- [x] `Object.prototype` methods on values: `toString` (`"[object Array]"`, `"[object Map]"`, and the class name of
every built-in wrapper), `toLocaleString`, `valueOf`, `hasOwnProperty`, `isPrototypeOf`, and
`propertyIsEnumerable`.
## Arrays
@@ -417,6 +433,32 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [ ] `crypto.getRandomValues` and `crypto.subtle`, `TextEncoder`/`TextDecoder`, and `Blob`: these need a binary
value type, which the JSON-like data model does not have yet.
## Extensions
Host classes and functions a host opts in through `Extension.make({ name, globals })` and `CodeMode.make({ extensions })`.
Nothing is exposed unless a host provides it; extension calls are not tool calls.
- [x] Each global is a class or a function, exposed as-is: constructors with `new`, prototype methods, accessors,
and statics (including through an exposed subclass, so `new this()` works), plus inheritance
up to the nearest exposed ancestor. A global that shadows a built-in or another extension throws at `make`.
- [x] Instances of exposed classes stay on the host; the program holds a handle whose only members are the class's.
The same host instance is always the same handle within a run, so identity and `instanceof` hold. Handles
cannot cross the data boundary: returning, stringifying, throwing, or passing one to a tool fails.
- [x] Every value crossing in either direction is converted, never shared: plain objects and arrays are copied,
`Date`, `RegExp`, `URL`, `URLSearchParams`, `Map`, and `Set` become fresh copies with their contents converted,
errors cross as errors with their name and message, and a `__proto__` key is dropped. Functions, generators,
un-awaited promises, and symbols cannot be passed in; an instance of an unexposed class, a symbol, or a BigInt
cannot come out.
- [x] A host `Promise` becomes a program promise. Whatever host code returns, resolves, throws, or rejects with
crosses the same way, so `catch (e)` receives a copy of the thrown value (an `Error` of the matching type, or
plain data). A getter must be synchronous.
- [x] A prototype member runs only with a handle of its own class as `this`; a detached call, a plain object, or a
handle of another class throws `TypeError: Illegal invocation`. Program edits to an exposed prototype affect
that run only. Data properties on a class or prototype are not exposed, since a program write would change the
host class itself; expose one through an accessor.
- [ ] Program functions as arguments to extension code (callbacks such as `forEach`).
- [ ] Binary values (`Uint8Array`, `ArrayBuffer`) at the extension boundary; needs the binary value type above.
## Errors and diagnostics
- [x] `Error`, `TypeError`, `RangeError`, `SyntaxError`, `ReferenceError`, `EvalError`, and `URIError`, callable with
@@ -440,3 +482,12 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] Failures raised by the interpreter are `TypeError`s unless JavaScript names them otherwise (`RangeError`,
`ReferenceError`, `SyntaxError`, `URIError`), so `e instanceof TypeError` and `e.constructor === TypeError`
hold. Unsupported syntax reached at runtime is a `SyntaxError`; awaited tool failures stay plain `Error`.
Host errors escaping a built-in (`(1).toFixed(200)`) become the same-named program error at the call.
A failure raised inside a promise a built-in created (`Promise.all(1)`, `Promise.race([])`, a resolution cycle)
is located at the call that created the promise.
- [x] One failure is one error object: every `catch`, rejection handler, and `allSettled` reason for the same
failure sees the identical value, so `a === b` holds after awaiting the same rejected promise twice.
- [x] Rethrowing an interpreter failure keeps its diagnostic: `catch (e) { throw e }` still reports the original
kind and source location. Uncaught errors report as `name: message` whoever raised them, as
`Error.prototype.toString` would (`TypeError: Cannot read properties of null (reading 'foo').`,
`TypeError: bad input`); other thrown values report as `Uncaught: <value>`.
+14 -1
View File
@@ -1,5 +1,8 @@
import { Effect, Schema } from "effect"
import type { Extension } from "./extension.js"
import { executeProgram } from "./interpreter/execute.js"
import { extensionGlobals } from "./interpreter/extensions.js"
import { globalNames } from "./interpreter/globals.js"
import { type Services, type ToolDescription, ToolRuntime } from "./tool-runtime.js"
import type { Tools } from "./tools.js"
@@ -34,6 +37,8 @@ export type ResolvedExecutionLimits = {
export type Options<Provided extends Record<string, unknown> = {}> = ToolRuntime.ToolCallHooks<Services<Provided>> & {
/** Explicit tools exposed to the program as `tools`. */
tools?: Provided & Tools<Services<Provided>>
/** Host classes and functions exposed as globals; see `Extension.make`. */
extensions?: ReadonlyArray<Extension>
/** Resource limits enforced on each execution. */
limits?: ExecutionLimits
}
@@ -133,8 +138,16 @@ export const make = <const Provided extends Record<string, unknown> = {}>(
): Runtime<Services<Provided>> => {
const prepared = ToolRuntime.prepare((options.tools ?? {}) as Tools<Services<Provided>>)
const limits = resolveExecutionLimits(options.limits)
const extensions = options.extensions ?? []
const bound = new Set(globalNames)
for (const extension of extensions) {
for (const name of Object.keys(extension.globals)) {
if (bound.has(name)) throw new TypeError(`Extension "${extension.name}" global "${name}" is already defined.`)
bound.add(name)
}
}
return {
catalog: prepared.catalog,
execute: (code) => executeProgram(code, prepared, limits, options),
execute: (code) => executeProgram(code, prepared, limits, options, (host) => extensionGlobals(host, extensions)),
}
}
+66 -19
View File
@@ -13,6 +13,7 @@ import {
ProgramDate,
ProgramError,
ProgramGenerator,
ProgramHandle,
ProgramMap,
ProgramObject,
ProgramPromise,
@@ -20,9 +21,10 @@ import {
ProgramSet,
ProgramURL,
ProgramURLSearchParams,
type ProgramWrapper,
} from "./interpreter/objects.js"
const MAX_VALUE_DEPTH = 32
export const MAX_VALUE_DEPTH = 32
export class ToolRuntimeError extends Error {
constructor(
@@ -58,9 +60,18 @@ export const fromData = (protos: Prototypes, value: unknown, label: string): unk
* non-finite numbers become null, and array holes become null. `undefined` object properties are
* dropped ("json") or become null ("result", for program results where the consumer must never see
* undefined); a bare `undefined` follows the same rule.
*
* At the host boundary (tool arguments and program results) `__proto__` keys are dropped and values
* `JSON.stringify` would flatten to `{}` cross in a useful form instead: a Set as an array, a RegExp
* and URLSearchParams as their strings. `JSON.stringify` itself passes `boundary: false` to keep JS
* behavior.
*/
export const toData = (value: unknown, label: string, undefinedAs: "json" | "result" = "json"): unknown =>
copy(value, label, undefinedAs, 0, new Set())
export const toData = (
value: unknown,
label: string,
undefinedAs: "json" | "result" = "json",
boundary = true,
): unknown => copy(value, label, undefinedAs, 0, new Set(), undefined, boundary)
// "program" and "data" build program objects; "json" and "result" build ordinary objects for the host.
type Mode = "program" | "data" | "json" | "result"
@@ -72,8 +83,9 @@ const copy = (
depth: number,
seen: Set<object>,
protos?: Prototypes,
boundary = true,
): unknown => {
const next = (item: unknown) => copy(item, label, mode, depth + 1, seen, protos)
const next = (item: unknown) => copy(item, label, mode, depth + 1, seen, protos, boundary)
if (depth > MAX_VALUE_DEPTH) {
throw new ToolRuntimeError("InvalidDataValue", `${label} exceeds the maximum value depth of ${MAX_VALUE_DEPTH}.`)
}
@@ -92,30 +104,38 @@ const copy = (
if ((value instanceof Callable || value instanceof ProgramGenerator) && mode !== "program") {
throw new ToolRuntimeError("InvalidDataValue", `${label} must contain data only.`)
}
if (value instanceof ProgramHandle && mode !== "program") {
throw new ToolRuntimeError(
"InvalidDataValue",
`${label} contains a ${value.instance.constructor.name}, which only extension functions accept.`,
)
}
// Host-produced input never holds program objects; one arriving here would come back as a host object.
if (value instanceof ProgramObject && mode === "data") {
throw new ToolRuntimeError("InvalidDataValue", `${label} must be host data, not a program value.`)
}
if (protos !== undefined && mode === "program") {
if (value instanceof ProgramObject) return value
if (value instanceof Date) return new ProgramDate(protos.Date, value.getTime())
if (value instanceof RegExp) return new ProgramRegExp(protos.RegExp, value.source, value.flags)
if (value instanceof Map) {
const wrapped = new ProgramMap(protos.Map)
for (const [key, item] of value.entries()) wrapped.map.set(next(key), next(item))
return wrapped
}
if (value instanceof Set) {
const wrapped = new ProgramSet(protos.Set)
for (const item of value.values()) wrapped.set.add(next(item))
return wrapped
}
if (value instanceof URL) return new ProgramURL(protos.URL, protos.URLSearchParams, new URL(value.href))
if (value instanceof URLSearchParams)
return new ProgramURLSearchParams(protos.URLSearchParams, new URLSearchParams(value))
const wrapped = wrapHost(protos, value, next)
if (wrapped !== undefined) return wrapped
}
if (value instanceof ProgramDate) return Number.isFinite(value.time) ? new Date(value.time).toISOString() : null
if (value instanceof Date) return Number.isFinite(value.getTime()) ? value.toISOString() : null
if (value instanceof ProgramURL) return value.url.href
if (value instanceof URL) return value.href
if (boundary && protos === undefined) {
if (value instanceof ProgramRegExp) return String(value.regex)
if (value instanceof ProgramURLSearchParams) return value.params.toString()
if (value instanceof ProgramSet) {
if (seen.has(value)) throw new ToolRuntimeError("InvalidDataValue", `${label} contains a circular value.`)
seen.add(value)
const copied = Array.from(value.set, (item) => next(item) ?? null)
seen.delete(value)
return copied
}
}
// Remaining wrappers and their host counterparts serialize as empty objects, like JSON.stringify.
if (
isWrapper(value) ||
@@ -145,6 +165,7 @@ const copy = (
defineHost(copied, "message", next(get(value, "message")))
}
for (const [key, item] of entries(value)) {
if (boundary && key === "__proto__") continue
const copiedItem = next(item)
if (copiedItem === undefined && mode === "json") continue
defineHost(copied, key, copiedItem)
@@ -180,6 +201,7 @@ const copy = (
}
const copied: Record<string, unknown> = {}
for (const [key, item] of Object.entries(value)) {
if (boundary && key === "__proto__") continue
const copiedItem = next(item)
if (copiedItem === undefined && mode === "json") continue
defineHost(copied, key, copiedItem)
@@ -188,6 +210,31 @@ const copy = (
return copied
}
/** The built-in wrapper for a host Date, RegExp, Map, Set, URL, or URLSearchParams, copying contents through `next`. */
export const wrapHost = (
protos: Prototypes,
value: object,
next: (item: unknown) => unknown,
): ProgramWrapper | undefined => {
if (value instanceof Date) return new ProgramDate(protos.Date, value.getTime())
if (value instanceof RegExp) return new ProgramRegExp(protos.RegExp, value.source, value.flags)
if (value instanceof Map) {
const wrapped = new ProgramMap(protos.Map)
for (const [key, item] of value) wrapped.map.set(next(key), next(item))
return wrapped
}
if (value instanceof Set) {
const wrapped = new ProgramSet(protos.Set)
for (const item of value) wrapped.set.add(next(item))
return wrapped
}
if (value instanceof URL) return new ProgramURL(protos.URL, protos.URLSearchParams, new URL(value.href))
if (value instanceof URLSearchParams) {
return new ProgramURLSearchParams(protos.URLSearchParams, new URLSearchParams(value))
}
return undefined
}
// Own data property regardless of the target's prototype, so a "__proto__" key on a host object
// never reaches the Object.prototype setter.
const defineHost = (target: object, key: string, value: unknown): void => {
+21
View File
@@ -0,0 +1,21 @@
export * as Extension from "./extension.js"
/**
* Host classes and functions a program uses directly, like JavaScript. Values crossing in either direction are
* converted, never shared: plain data is copied, instances of the classes stay on the host behind program-side
* handles. Extension calls are not tool calls.
*/
export type Extension = {
readonly name: string
/** Each value is a class or a function; everything on a class is exposed, including statics and accessors. */
readonly globals: Readonly<Record<string, Function>>
}
export const make = (options: Extension): Extension => {
for (const [name, value] of Object.entries(options.globals)) {
if (typeof value !== "function") {
throw new TypeError(`Extension "${options.name}" global "${name}" must be a class or a function.`)
}
}
return { name: options.name, globals: { ...options.globals } }
}
+1
View File
@@ -1,4 +1,5 @@
export * as CodeMode from "./codemode.js"
export * as Extension from "./extension.js"
export * as Namespace from "./namespace.js"
export * as Tool from "./tool.js"
export * as OpenAPI from "./openapi/index.js"
+47 -29
View File
@@ -2,7 +2,7 @@ import { Effect } from "effect"
import type { Diagnostic } from "../codemode.js"
import { ToolError } from "../tool-error.js"
import { toData, ToolRuntimeError } from "../data.js"
import { type AstNode, formatLocation, InterpreterRuntimeError, ProgramThrow, sourceLocation } from "./model.js"
import { type AstNode, formatLocation, PendingThrow, ProgramThrow, sourceLocation, typeError } from "./model.js"
import { containsRuntimeReference } from "./references.js"
import { createErrorValue, type ErrorType, isErrorType } from "./intrinsics.js"
import { constructor, methods, prototypeFrom, receiver } from "./native.js"
@@ -20,10 +20,10 @@ import { type Runner } from "./runner.js"
import { coerceToString } from "../stdlib/value.js"
export const normalizeError = (error: unknown): Diagnostic => {
if (error instanceof InterpreterRuntimeError) {
if (error instanceof PendingThrow) {
return {
kind: error.kind,
message: `${error.message}${formatLocation(error.node)}`,
message: `${error.type}: ${error.message}${formatLocation(error.node)}`,
...(error.node?.loc ? { location: sourceLocation(error.node) } : {}),
...(error.suggestions ? { suggestions: error.suggestions } : {}),
}
@@ -43,14 +43,15 @@ export const normalizeError = (error: unknown): Diagnostic => {
if (error instanceof ProgramThrow) {
const value = error.value
if (value instanceof ProgramError) {
return value.host ? normalizeError(value.host) : { kind: "ExecutionFailure", message: errorToString(value) }
}
let message: string
if (containsRuntimeReference(value)) {
// Never expose runtime reference internals through thrown values.
message = "a non-data value"
} else if (typeof value === "string") {
message = value
} else if (value instanceof ProgramObject && typeof get(value, "message") === "string") {
message = get(value, "message") as string
} else {
try {
message = JSON.stringify(toData(value, "Thrown value")) ?? String(value)
@@ -81,14 +82,47 @@ export const normalizeError = (error: unknown): Diagnostic => {
}
}
export const caughtErrorValue = <R>(runner: Runner<R>, thrown: unknown): unknown => {
/**
* Gives a failure the source location of the expression that raised it, keeping the first one attached. Host errors
* that escape a built-in become the equivalent program error here.
*/
export const locate = (error: unknown, node?: AstNode): unknown => {
if (error instanceof PendingThrow) {
if (error.node === undefined && node) error.node = node
return error
}
if (error instanceof Error && !(error instanceof ToolError) && !(error instanceof ToolRuntimeError)) {
return new PendingThrow(isErrorType(error.name) ? error.name : "Error", error.message, node)
}
return error
}
/** The program value a handler receives for a failure; one failure always yields the same value. */
export const materialize = <R>(runner: Runner<R>, thrown: unknown): unknown => {
if (thrown instanceof ProgramThrow) return thrown.value
const prototypes = runner.prototypes
if (thrown instanceof InterpreterRuntimeError) return createErrorValue(prototypes[thrown.type], thrown.message)
if (thrown instanceof PendingThrow) {
if (thrown.value === undefined) {
thrown.value = createErrorValue(prototypes[thrown.type], thrown.message)
thrown.value.host = thrown
}
return thrown.value
}
const type = thrown instanceof Error && isErrorType(thrown.name) ? thrown.name : "Error"
return createErrorValue(prototypes[type], normalizeError(thrown).message)
}
/** Error.prototype.toString: `name: message`, omitting whichever side is empty. */
const errorToString = (self: ProgramObject): string => {
const name = get(self, "name")
const message = get(self, "message")
const shownName = name === undefined ? "Error" : coerceToString(name)
const shownMessage = message === undefined ? "" : coerceToString(message)
if (shownMessage === "") return shownName
if (shownName === "") return shownMessage
return `${shownName}: ${shownMessage}`
}
export const createAggregateErrorValue = <R>(
runner: Runner<R>,
errors: Array<unknown>,
@@ -104,13 +138,10 @@ const constructAggregateErrorValue = <R>(
runner: Runner<R>,
args: Array<unknown>,
proto: ProgramObject,
node: AstNode,
): Effect.Effect<ProgramError, unknown, R> =>
Effect.gen(function* () {
const cursor = yield* runner.syncIterator(args[0], node)
if (cursor === undefined) {
throw new InterpreterRuntimeError("new AggregateError(...) expects a synchronous iterable of errors.", node)
}
const cursor = yield* runner.syncIterator(args[0])
if (cursor === undefined) throw typeError("new AggregateError(...) expects a synchronous iterable of errors.")
const errors: Array<unknown> = []
while (true) {
const step = yield* cursor.next
@@ -125,34 +156,21 @@ const constructAggregateErrorValue = <R>(
export const errorGlobal = <R>(type: ErrorType, runner: Runner<R>) => {
const protos = runner.prototypes
const prototype = protos[type]
const construct = (args: Array<unknown>, newTarget: Callable, node: AstNode) => {
const construct = (args: Array<unknown>, newTarget: Callable) => {
const proto = prototypeFrom(newTarget, prototype)
return type === "AggregateError"
? constructAggregateErrorValue(runner, args, proto, node)
? constructAggregateErrorValue(runner, args, proto)
: Effect.sync(() => createErrorValue(proto, args[0] === undefined ? undefined : coerceToString(args[0])))
}
const ctor: NativeFunction<R> = constructor<R>(protos, prototype, {
name: type,
length: type === "AggregateError" ? 2 : 1,
call: (_, args, node) => construct(args, ctor, node),
call: (_, args) => construct(args, ctor),
construct,
})
if (type === "Error") {
methods(protos, prototype, [
[
"toString",
0,
(thisValue, _, node) => {
const self = receiver(ProgramObject, thisValue, "Error.prototype.toString", node)
const name = get(self, "name")
const message = get(self, "message")
const shownName = name === undefined ? "Error" : coerceToString(name)
const shownMessage = message === undefined ? "" : coerceToString(message)
if (shownMessage === "") return shownName
if (shownName === "") return shownMessage
return `${shownName}: ${shownMessage}`
},
],
["toString", 0, (thisValue) => errorToString(receiver(ProgramObject, thisValue, "Error.prototype.toString"))],
])
}
return ctor
+2 -2
View File
@@ -9,7 +9,7 @@ import { ToolRuntime } from "../tool-runtime.js"
import { normalizeError } from "./errors.js"
import { createPrototypes } from "./intrinsics.js"
import type { Host } from "./globals.js"
import { InterpreterRuntimeError } from "./model.js"
import { PendingThrow } from "./model.js"
import { PromiseRuntime } from "./promises.js"
import { Runtime } from "./runtime.js"
@@ -122,7 +122,7 @@ const parseProgram = (code: string): Program => {
const transpiled = transpile(`async function __codemode__() {\n${code}\n}`)
if (transpiled.error !== undefined) {
throw new InterpreterRuntimeError(`Failed to parse TypeScript: ${transpiled.error}`, undefined, "ParseError")
throw new PendingThrow("SyntaxError", `Failed to parse TypeScript: ${transpiled.error}`, undefined, "ParseError")
}
const bodyStart = transpiled.outputText.indexOf("{") + 1
@@ -0,0 +1,297 @@
import { Effect } from "effect"
import { MAX_VALUE_DEPTH, wrapHost } from "../data.js"
import type { Extension } from "../extension.js"
import { coerceToString } from "../stdlib/value.js"
import type { Host } from "./globals.js"
import { createErrorValue, isErrorType } from "./intrinsics.js"
import { ProgramThrow, typeError } from "./model.js"
import { constructor, fn } from "./native.js"
import {
Callable,
define,
defineAccessor,
entries,
get,
hidden,
type NativeFunction,
ProgramArray,
ProgramDate,
ProgramError,
ProgramGenerator,
ProgramHandle,
ProgramMap,
ProgramObject,
ProgramPromise,
ProgramRegExp,
ProgramSet,
ProgramURL,
ProgramURLSearchParams,
} from "./objects.js"
import { describeValue } from "./references.js"
type Class = Function & { readonly prototype: object }
const isClass = (value: unknown): value is Class =>
typeof value === "function" && typeof value.prototype === "object" && value.prototype !== null
// Own keys the native function already carries.
const ownFunctionKeys = new Set(["length", "name", "prototype"])
const ownPrototypeKeys = new Set(["constructor"])
/**
* The global bindings of one run's extensions. Everything crossing the boundary is converted: plain data is
* copied, built-in wrappers are copied, instances of exposed classes travel as handles, and a host Promise becomes
* a program promise. Prototypes, constructors, and handle identity are all per run.
*/
export const extensionGlobals = <R>(
host: Host<R>,
extensions: ReadonlyArray<Extension>,
): ReadonlyArray<readonly [string, unknown]> => {
const protos = host.runner.prototypes
const classes = new Set(extensions.flatMap((extension) => Object.values(extension.globals)).filter(isClass))
// Host prototype object → this run's program prototype, so an instance wraps as its most-derived exposed class.
const prototypes = new Map<object, ProgramObject>()
const exposed = new Map<Class, { ctor: NativeFunction<R>; proto: ProgramObject }>()
const classOf = new Map<unknown, Class>()
const handles = new WeakMap<object, ProgramHandle>()
const toHost = (value: unknown, label: string, depth = 0, seen = new Set<object>()): unknown => {
if (depth > MAX_VALUE_DEPTH) throw typeError(`${label} exceeds the maximum value depth of ${MAX_VALUE_DEPTH}.`)
if (value === null || typeof value !== "object") {
if (isPrimitive(value)) return value
throw typeError(`${label} contains ${describeValue(value)}, which cannot be passed to an extension.`)
}
if (value instanceof ProgramHandle) return value.instance
if (value instanceof ProgramDate) return new Date(value.time)
if (value instanceof ProgramRegExp) return new RegExp(value.regex.source, value.regex.flags)
if (value instanceof ProgramURL) return new URL(value.url.href)
if (value instanceof ProgramURLSearchParams) return new URLSearchParams(value.params)
const next = (item: unknown) => toHost(item, label, depth + 1, seen)
if (value instanceof ProgramMap) return new Map([...value.map].map(([key, item]) => [next(key), next(item)]))
if (value instanceof ProgramSet) return new Set([...value.set].map(next))
if (
!(value instanceof ProgramObject) ||
value instanceof Callable ||
value instanceof ProgramGenerator ||
value instanceof ProgramPromise
) {
throw typeError(`${label} contains ${describeValue(value)}, which cannot be passed to an extension.`)
}
if (value instanceof ProgramError) {
const name = coerceToString(get(value, "name"))
const message = get(value, "message")
const text = message === undefined ? "" : coerceToString(message)
return name === "AggregateError" ? new AggregateError([], text) : new (hostErrors[name] ?? Error)(text)
}
if (seen.has(value)) throw typeError(`${label} contains a circular value.`)
seen.add(value)
const copied =
value instanceof ProgramArray
? value.items.map(next)
: Object.fromEntries(
entries(value)
.filter(([key]) => key !== "__proto__")
.map(([key, item]) => [key, next(item)]),
)
seen.delete(value)
return copied
}
const fromHost = (value: unknown, label: string, depth = 0, seen = new Set<object>()): unknown => {
if (depth > MAX_VALUE_DEPTH) throw typeError(`${label} exceeds the maximum value depth of ${MAX_VALUE_DEPTH}.`)
if (isPrimitive(value)) return value
if (value !== null && typeof value === "object") {
const existing = handles.get(value)
if (existing !== undefined) return existing
const proto = handlePrototype(value)
if (proto !== undefined) {
const handle = new ProgramHandle(proto, value)
handles.set(value, handle)
return handle
}
if (value instanceof Error) {
return createErrorValue(protos[isErrorType(value.name) ? value.name : "Error"], value.message)
}
const next = (item: unknown) => fromHost(item, label, depth + 1, seen)
const wrapped = wrapHost(protos, value, next)
if (wrapped !== undefined) return wrapped
if (seen.has(value)) throw typeError(`${label} produced a circular value.`)
seen.add(value)
if (Array.isArray(value)) {
const copied = new ProgramArray(protos.Array, value.map(next))
seen.delete(value)
return copied
}
const prototype = Object.getPrototypeOf(value)
if (prototype === Object.prototype || prototype === null) {
const copied = new ProgramObject(protos.Object)
for (const [key, item] of Object.entries(value)) define(copied, key, next(item))
seen.delete(value)
return copied
}
}
throw typeError(`${label} produced ${describeHost(value)}, which the program cannot hold.`)
}
const handlePrototype = (instance: object): ProgramObject | undefined => {
for (let level = Object.getPrototypeOf(instance); level !== null; level = Object.getPrototypeOf(level)) {
const proto = prototypes.get(level)
if (proto !== undefined) return proto
}
return undefined
}
// Runs host code with already-converted inputs. Whatever it returns, resolves, throws, or rejects with crosses
// the same way, so the program catches what the author threw.
const invoke = (run: () => unknown, label: string): Effect.Effect<unknown, unknown, R> => {
const thrown = (reason: unknown) => new ProgramThrow(fromHost(reason, label))
let result: unknown
try {
result = run()
} catch (reason) {
return Effect.fail(thrown(reason))
}
if (!(result instanceof Promise)) return Effect.succeed(fromHost(result, label))
return host.promises.create(
Effect.map(Effect.tryPromise({ try: () => result, catch: thrown }), (settled) => fromHost(settled, label)),
)
}
const args = (values: Array<unknown>, label: string): Array<unknown> =>
values.map((value, index) => toHost(value, `Argument ${index + 1} to ${label}`))
// Own members of each level from `from` up to (excluding) `root`, child first, as JS resolves them.
const members = (
target: ProgramObject,
from: object,
root: object,
skip: ReadonlySet<string>,
label: string,
receiver: (thisValue: unknown, member: string) => unknown,
): void => {
for (let level: object | null = from; level !== null && level !== root; level = Object.getPrototypeOf(level)) {
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(level))) {
if (skip.has(key) || target.props.has(key)) continue
const name = `${label}.${key}`
if (typeof descriptor.value === "function") {
const method: Function = descriptor.value
const impl = (thisValue: unknown, values: Array<unknown>) => {
const self = receiver(thisValue, name)
const converted = args(values, name)
return invoke(() => method.apply(self, converted), name)
}
define(target, key, fn<R>(protos, key, method.length, impl), hidden)
continue
}
// Data properties stay host-side: a program write to one would change the host class itself.
if ("value" in descriptor) continue
const get = descriptor.get
const set = descriptor.set
defineAccessor(
target,
key,
get === undefined
? undefined
: (thisValue) => {
const value = get.call(receiver(thisValue, name))
if (value instanceof Promise)
throw typeError(`${name} returned a Promise; a getter must be synchronous.`)
return fromHost(value, name)
},
set === undefined
? undefined
: (thisValue, value) => {
set.call(receiver(thisValue, name), toHost(value, `${name} value`))
},
)
}
}
}
const expose = (cls: Class): { ctor: NativeFunction<R>; proto: ProgramObject } => {
const existing = exposed.get(cls)
if (existing !== undefined) return existing
const ancestor = exposedAncestor(cls)
const base = ancestor === undefined ? undefined : expose(ancestor)
const proto = new ProgramObject(base === undefined ? protos.Object : base.proto)
prototypes.set(cls.prototype, proto)
const name = cls.name
const ctor = constructor<R>(protos, proto, {
name,
length: cls.length,
call: (_, values) => {
const converted = args(values, name)
return invoke(() => cls.apply(undefined, converted), name)
},
construct: (values) => {
const label = `new ${name}`
const construct = cls as new (...values: Array<unknown>) => object
const converted = args(values, label)
return invoke(() => new construct(...converted), label)
},
})
if (base !== undefined) ctor.proto = base.ctor
const entry = { ctor, proto }
exposed.set(cls, entry)
classOf.set(ctor, cls)
// A static called through an exposed subclass sees that subclass as `this`, like JS.
members(ctor, cls, ancestor ?? Function.prototype, ownFunctionKeys, name, (thisValue) => {
const called = classOf.get(thisValue)
return called !== undefined && (called === cls || called.prototype instanceof cls) ? called : cls
})
members(
proto,
cls.prototype,
ancestor?.prototype ?? Object.prototype,
ownPrototypeKeys,
`${name}.prototype`,
(thisValue, member) => {
if (thisValue instanceof ProgramHandle && thisValue.instance instanceof cls) return thisValue.instance
throw typeError(`Illegal invocation: ${member} called on ${describeValue(thisValue)}.`)
},
)
return entry
}
const exposedAncestor = (cls: Class): Class | undefined => {
for (let level = Object.getPrototypeOf(cls); isClass(level); level = Object.getPrototypeOf(level)) {
if (classes.has(level)) return level
}
return undefined
}
return extensions.flatMap((extension) =>
Object.entries(extension.globals).map(([name, value]) => {
if (isClass(value)) return [name, expose(value).ctor] as const
const impl = (_: unknown, values: Array<unknown>) => {
const converted = args(values, name)
return invoke(() => value.apply(undefined, converted), name)
}
return [name, fn<R>(protos, name, value.length, impl)] as const
}),
)
}
const hostErrors: Record<string, ErrorConstructor | undefined> = {
TypeError,
RangeError,
SyntaxError,
ReferenceError,
EvalError,
URIError,
}
// The primitives the interpreter operates on; symbols and BigInts are not among them.
const isPrimitive = (value: unknown): boolean =>
value === null ||
value === undefined ||
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean"
const describeHost = (value: unknown): string => {
if (typeof value === "function") return "a function"
if (typeof value !== "object" || value === null) return `a ${typeof value}`
const name = (value as { constructor?: { name?: string } }).constructor?.name
return name === undefined || name === "" ? "an object" : `a ${name}`
}
@@ -1,6 +1,6 @@
import { Effect } from "effect"
import { fn, type Method, methods, receiver } from "./native.js"
import { type AstNode, AsyncIteratorSymbol, type GeneratorRequestKind, IteratorSymbol } from "./model.js"
import { AsyncIteratorSymbol, type GeneratorRequestKind, IteratorSymbol } from "./model.js"
import { define, hidden, ProgramGenerator } from "./objects.js"
import type { PromiseRuntime } from "./promises.js"
import type { Runner } from "./runner.js"
@@ -14,9 +14,9 @@ export const generatorGlobals = <R>(runner: Runner<R>, promises: PromiseRuntime<
const request = (kind: GeneratorRequestKind): Method => [
kind,
1,
(thisValue: unknown, args: Array<unknown>, node: AstNode) => {
const generator = receiver(ProgramGenerator, thisValue, `${label}.prototype.${kind}`, node)
const requested = generator.request(kind, args[0], node) as Effect.Effect<unknown, unknown, R>
(thisValue: unknown, args: Array<unknown>) => {
const generator = receiver(ProgramGenerator, thisValue, `${label}.prototype.${kind}`)
const requested = generator.request(kind, args[0]) as Effect.Effect<unknown, unknown, R>
return generator.asynchronous ? promises.create(requested) : requested
},
]
+52 -47
View File
@@ -16,7 +16,7 @@ import { ToolReference } from "../tool-runtime.js"
import { errorGlobal } from "./errors.js"
import { errorTypes } from "./intrinsics.js"
import { constants, constructor, native } from "./native.js"
import { type AstNode, AsyncIteratorSymbol, InterpreterRuntimeError, IteratorSymbol } from "./model.js"
import { AsyncIteratorSymbol, IteratorSymbol, typeError } from "./model.js"
import { generatorGlobals } from "./generators.js"
import { promiseGlobal, type PromiseRuntime } from "./promises.js"
import type { Runner } from "./runner.js"
@@ -32,27 +32,24 @@ export type Host<R> = {
// Function.prototype.constructor exists so `fn.constructor === Function` holds; dynamic code is unsupported.
const functionGlobal = <R>(runner: Runner<R>) => {
const reject = (_: unknown, __: Array<unknown>, node: AstNode) =>
const reject = () =>
Effect.sync(() => {
throw new InterpreterRuntimeError("The Function constructor is not supported; write the function inline.", node)
throw typeError("The Function constructor is not supported; write the function inline.")
})
return constructor<R>(runner.prototypes, runner.prototypes.Function, {
name: "Function",
length: 1,
call: reject,
construct: (args, _, node) => reject(undefined, args, node),
construct: reject,
})
}
const symbolGlobal = <R>(runner: Runner<R>) => {
const symbol = native<R>(runner.prototypes, {
name: "Symbol",
call: (_, __, node) =>
call: () =>
Effect.sync(() => {
throw new InterpreterRuntimeError(
"Symbol is not callable; only Symbol.asyncIterator and Symbol.iterator are available.",
node,
)
throw typeError("Symbol is not callable; only Symbol.asyncIterator and Symbol.iterator are available.")
}),
callback: false,
})
@@ -60,44 +57,52 @@ const symbolGlobal = <R>(runner: Runner<R>) => {
return symbol
}
type Factory = <R>(host: Host<R>) => unknown
// A table rather than a list so the names are known before any runtime exists.
const table: Record<string, Factory> = {
tools: () => new ToolReference([]),
search: (host) =>
native(host.runner.prototypes, { name: "search", call: (_, args) => host.search(args), callback: false }),
undefined: () => undefined,
NaN: () => NaN,
Infinity: () => Infinity,
Object: (host) => objectGlobal(host.runner, host.toolKeys),
Function: (host) => functionGlobal(host.runner),
Array: (host) => arrayGlobal(host.runner),
Math: (host) => mathGlobal(host.runner),
JSON: (host) => jsonGlobal(host.runner),
console: (host) => consoleGlobal(host.runner, host.logs),
Promise: (host) => promiseGlobal(host.runner, host.promises),
Symbol: (host) => symbolGlobal(host.runner),
Number: (host) => numberGlobal(host.runner),
String: (host) => stringGlobal(host.runner),
Boolean: (host) => booleanGlobal(host.runner),
parseInt: (host) => coercion(host.runner, "parseInt", 2),
parseFloat: (host) => coercion(host.runner, "parseFloat"),
isFinite: (host) => coercion(host.runner, "isFinite"),
isNaN: (host) => coercion(host.runner, "isNaN"),
Date: (host) => dateGlobal(host.runner),
RegExp: (host) => regexpGlobal(host.runner),
Map: (host) => mapGlobal(host.runner),
Set: (host) => setGlobal(host.runner),
URL: (host) => urlGlobal(host.runner),
URLSearchParams: (host) => urlSearchParamsGlobal(host.runner),
encodeURI: (host) => uriGlobal(host.runner, "encodeURI"),
encodeURIComponent: (host) => uriGlobal(host.runner, "encodeURIComponent"),
decodeURI: (host) => uriGlobal(host.runner, "decodeURI"),
decodeURIComponent: (host) => uriGlobal(host.runner, "decodeURIComponent"),
atob: (host) => base64Global(host.runner, "atob"),
btoa: (host) => base64Global(host.runner, "btoa"),
crypto: (host) => cryptoGlobal(host.runner),
...Object.fromEntries(errorTypes.map((type) => [type, <R>(host: Host<R>) => errorGlobal(type, host.runner)])),
}
/** Names bound in every program before extensions apply. */
export const globalNames: ReadonlySet<string> = new Set(Object.keys(table))
/** The immutable global bindings of every program, in declaration order. */
export const globals = <R>(host: Host<R>): ReadonlyArray<readonly [string, unknown]> => {
const runner = host.runner
generatorGlobals(runner, host.promises)
return [
["tools", new ToolReference([])],
["search", native<R>(runner.prototypes, { name: "search", call: (_, args) => host.search(args), callback: false })],
["undefined", undefined],
["NaN", NaN],
["Infinity", Infinity],
["Object", objectGlobal(runner, host.toolKeys)],
["Function", functionGlobal(runner)],
["Array", arrayGlobal(runner)],
["Math", mathGlobal(runner)],
["JSON", jsonGlobal(runner)],
["console", consoleGlobal(runner, host.logs)],
["Promise", promiseGlobal(runner, host.promises)],
["Symbol", symbolGlobal(runner)],
["Number", numberGlobal(runner)],
["String", stringGlobal(runner)],
["Boolean", booleanGlobal(runner)],
["parseInt", coercion(runner, "parseInt", 2)],
["parseFloat", coercion(runner, "parseFloat")],
["isFinite", coercion(runner, "isFinite")],
["isNaN", coercion(runner, "isNaN")],
["Date", dateGlobal(runner)],
["RegExp", regexpGlobal(runner)],
["Map", mapGlobal(runner)],
["Set", setGlobal(runner)],
["URL", urlGlobal(runner)],
["URLSearchParams", urlSearchParamsGlobal(runner)],
["encodeURI", uriGlobal(runner, "encodeURI")],
["encodeURIComponent", uriGlobal(runner, "encodeURIComponent")],
["decodeURI", uriGlobal(runner, "decodeURI")],
["decodeURIComponent", uriGlobal(runner, "decodeURIComponent")],
["atob", base64Global(runner, "atob")],
["btoa", base64Global(runner, "btoa")],
["crypto", cryptoGlobal(runner)],
...errorTypes.map((type) => [type, errorGlobal(type, runner)] as const),
]
generatorGlobals(host.runner, host.promises)
return Object.entries(table).map(([name, factory]) => [name, factory(host)] as const)
}
@@ -0,0 +1,19 @@
import { rangeError } from "./model.js"
// The timeout only fires between interpreter steps, so a single built-in must not be able to materialize an
// unbounded value. These bound what one call may build; programs cannot reach such sizes any other way.
/** Longest string a built-in or operator may produce. */
export const MAX_STRING_LENGTH = 1 << 24
/** Longest array a built-in may create or grow to. */
export const MAX_ARRAY_LENGTH = 10_000_000
/** Most promises that may be pending at once. */
export const MAX_PENDING_PROMISES = 10_000
export const checkStringLength = (length: number): void => {
if (length > MAX_STRING_LENGTH) throw rangeError("Invalid string length")
}
export const checkArrayLength = (length: number): void => {
if (length > MAX_ARRAY_LENGTH) throw rangeError("Invalid array length")
}
+24 -18
View File
@@ -1,10 +1,17 @@
import type { Node } from "acorn"
import { Context } from "effect"
import type { ErrorType } from "./intrinsics.js"
import type { DiagnosticKind } from "../codemode.js"
import type { ProgramError } from "./objects.js"
/** Any parsed node; the interpreter narrows on `type` and reads `loc` for diagnostics. */
export type AstNode = Node
/** The program call a built-in is running under: where to locate failures born inside it, and how deep the stack is there. */
export const CallSite = Context.Reference<{ readonly node?: AstNode; readonly depth: number }>("codemode/CallSite", {
defaultValue: () => ({ depth: 0 }),
})
export type Binding = {
mutable: boolean
value: unknown
@@ -33,32 +40,31 @@ export class GeneratorReturn {
export const OptionalShortCircuit: unique symbol = Symbol("codemode.optional-short-circuit")
export class InterpreterRuntimeError extends Error {
readonly node?: AstNode
/**
* A failure raised by the interpreter or a built-in. It travels as a defect and becomes one program Error object
* the first time a handler observes it, so every observer of the same failure sees the same value.
*/
export class PendingThrow {
node?: AstNode
value?: ProgramError
constructor(
message: string,
/** The JS error class a program sees when it catches this failure. */
readonly type: ErrorType,
readonly message: string,
node?: AstNode,
readonly kind: DiagnosticKind = "ExecutionFailure",
readonly suggestions?: ReadonlyArray<string>,
/** The JS error class a program sees when it catches this failure. */
readonly type: ErrorType = "TypeError",
) {
super(message)
this.name = "InterpreterRuntimeError"
if (node) this.node = node
}
}
/** Attaches a source location to a failure raised where none was known, such as inside a property accessor. */
export const locate = (error: unknown, node: AstNode): unknown =>
error instanceof InterpreterRuntimeError && error.node === undefined
? new InterpreterRuntimeError(error.message, node, error.kind, error.suggestions, error.type)
: error
const failure = (type: ErrorType) => (message: string, node?: AstNode) =>
new InterpreterRuntimeError(message, node, "ExecutionFailure", undefined, type)
const failure = (type: ErrorType, kind?: DiagnosticKind) => (message: string, node?: AstNode) =>
new PendingThrow(type, message, node, kind)
export const typeError = failure("TypeError")
export const invalidData = failure("TypeError", "InvalidDataValue")
export const rangeError = failure("RangeError")
export const referenceError = failure("ReferenceError")
export const syntaxError = failure("SyntaxError")
@@ -68,13 +74,13 @@ export const uriError = failure("URIError")
export const supportedSyntaxMessage =
"This is a restricted JavaScript-like language. Supported: plain and async functions, data literals, destructuring, standard control flow, await and Promise, and built-ins such as Array, Object, Math, JSON, Date, RegExp, Map, Set, and URL. Unsupported: classes, this, getters/setters, tagged templates, BigInt, and custom Symbols. Use plain functions and data objects instead."
export const unsupportedSyntax = (kind: string, node: AstNode): InterpreterRuntimeError =>
new InterpreterRuntimeError(
export const unsupportedSyntax = (kind: string, node: AstNode): PendingThrow =>
new PendingThrow(
"SyntaxError",
`Syntax '${kind}' is not supported. ${supportedSyntaxMessage}`,
node,
"UnsupportedSyntax",
[supportedSyntaxMessage],
"SyntaxError",
)
export const isRecord = (value: unknown): value is Record<string, unknown> =>
+13 -16
View File
@@ -1,19 +1,19 @@
import { Effect } from "effect"
import type { Prototypes } from "./intrinsics.js"
import { type AstNode, InterpreterRuntimeError } from "./model.js"
import { typeError } from "./model.js"
import { type Callable, define, frozen, hidden, NativeFunction, type NativeOptions, ProgramObject } from "./objects.js"
import { describeValue } from "./references.js"
/** A native function body: a plain value, a thrown `InterpreterRuntimeError`, or an Effect. */
export type Impl = (thisValue: unknown, args: Array<unknown>, node: AstNode) => unknown
/** A native function body: a plain value, a thrown `PendingThrow`, or an Effect. */
export type Impl = (thisValue: unknown, args: Array<unknown>) => unknown
// The dispatch in `Frame.invokeCallable` suspends every native call, so a synchronous throw here is a defect.
const lift =
<R>(impl: Impl) =>
(thisValue: unknown, args: Array<unknown>, node: AstNode): Effect.Effect<unknown, unknown, R> =>
Effect.suspend(() => {
const result = impl(thisValue, args, node)
return Effect.isEffect(result) ? (result as Effect.Effect<unknown, unknown, R>) : Effect.succeed(result)
})
(thisValue: unknown, args: Array<unknown>): Effect.Effect<unknown, unknown, R> => {
const result = impl(thisValue, args)
return Effect.isEffect(result) ? (result as Effect.Effect<unknown, unknown, R>) : Effect.succeed(result)
}
export const native = <R>(protos: Prototypes, options: NativeOptions<R>): NativeFunction<R> =>
new NativeFunction<R>(protos.Function, options)
@@ -44,12 +44,10 @@ export const constructor = <R>(
}
/** The `call` of a constructor that JS requires to be invoked with `new`. */
export const requiresNew =
(name: string) =>
(_: unknown, __: Array<unknown>, node: AstNode): Effect.Effect<never, unknown, never> =>
Effect.sync(() => {
throw new InterpreterRuntimeError(`Constructor ${name} requires 'new'.`, node)
})
export const requiresNew = (name: string) => (): Effect.Effect<never, unknown, never> =>
Effect.sync(() => {
throw typeError(`Constructor ${name} requires 'new'.`)
})
/** The instance prototype for `new` via `newTarget.prototype`, falling back to the built-in's own. */
export const prototypeFrom = (newTarget: Callable, fallback: ProgramObject): ProgramObject => {
@@ -62,8 +60,7 @@ export const receiver = <T extends ProgramObject>(
cls: abstract new (...args: never) => T,
thisValue: unknown,
method: string,
node?: AstNode,
): T => {
if (thisValue instanceof cls) return thisValue
throw new InterpreterRuntimeError(`${method} called on incompatible receiver ${describeValue(thisValue)}.`, node)
throw typeError(`${method} called on incompatible receiver ${describeValue(thisValue)}.`)
}
+53 -37
View File
@@ -1,6 +1,13 @@
import type { BlockStatement, Expression, Pattern } from "acorn"
import type { Effect, Fiber } from "effect"
import { type AstNode, AsyncIteratorSymbol, type Binding, type GeneratorRequestKind, IteratorSymbol } from "./model.js"
import { checkArrayLength } from "./limits.js"
import {
AsyncIteratorSymbol,
type Binding,
type GeneratorRequestKind,
IteratorSymbol,
type PendingThrow,
} from "./model.js"
/** Property attributes, as in a JS property descriptor. */
export type Attributes = {
@@ -42,7 +49,10 @@ export class ProgramArray extends ProgramObject {
}
/** An object with the [[ErrorData]] slot: what `Error.prototype.toString` and the host boundary recognize as an error. */
export class ProgramError extends ProgramObject {}
export class ProgramError extends ProgramObject {
/** The interpreter failure this error materialized from, so rethrowing it keeps the diagnostic kind and location. */
host?: PendingThrow
}
export abstract class Callable extends ProgramObject {
constructor(proto: ProgramObject, name: string, length: number) {
@@ -67,16 +77,8 @@ export class ProgramFunction extends Callable {
}
}
export type NativeCall<R> = (
thisValue: unknown,
args: Array<unknown>,
node: AstNode,
) => Effect.Effect<unknown, unknown, R>
export type NativeConstruct<R> = (
args: Array<unknown>,
newTarget: Callable,
node: AstNode,
) => Effect.Effect<unknown, unknown, R>
export type NativeCall<R> = (thisValue: unknown, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
export type NativeConstruct<R> = (args: Array<unknown>, newTarget: Callable) => Effect.Effect<unknown, unknown, R>
export type NativeOptions<R> = {
readonly name: string
@@ -114,17 +116,19 @@ export class ProgramGenerator extends ProgramObject {
constructor(
proto: ProgramObject,
readonly asynchronous: boolean,
readonly request: (
kind: GeneratorRequestKind,
value: unknown,
node: AstNode,
) => Effect.Effect<unknown, unknown, unknown>,
readonly request: (kind: GeneratorRequestKind, value: unknown) => Effect.Effect<unknown, unknown, unknown>,
) {
super(proto)
}
}
export class ProgramDate extends ProgramObject {
/** A built-in object wrapping a host value; data-like, but never plain data. `kind` is its JS class name. */
export abstract class ProgramWrapper extends ProgramObject {
abstract readonly kind: string
}
export class ProgramDate extends ProgramWrapper {
readonly kind = "Date"
constructor(
proto: ProgramObject,
public time: number,
@@ -133,7 +137,8 @@ export class ProgramDate extends ProgramObject {
}
}
export class ProgramRegExp extends ProgramObject {
export class ProgramRegExp extends ProgramWrapper {
readonly kind = "RegExp"
readonly regex: RegExp
constructor(proto: ProgramObject, pattern: string, flags: string) {
super(proto)
@@ -142,15 +147,18 @@ export class ProgramRegExp extends ProgramObject {
}
}
export class ProgramMap extends ProgramObject {
export class ProgramMap extends ProgramWrapper {
readonly kind = "Map"
readonly map = new Map<unknown, unknown>()
}
export class ProgramSet extends ProgramObject {
export class ProgramSet extends ProgramWrapper {
readonly kind = "Set"
readonly set = new Set<unknown>()
}
export class ProgramURLSearchParams extends ProgramObject {
export class ProgramURLSearchParams extends ProgramWrapper {
readonly kind = "URLSearchParams"
constructor(
proto: ProgramObject,
readonly params: URLSearchParams,
@@ -159,7 +167,8 @@ export class ProgramURLSearchParams extends ProgramObject {
}
}
export class ProgramURL extends ProgramObject {
export class ProgramURL extends ProgramWrapper {
readonly kind = "URL"
readonly searchParams: ProgramURLSearchParams
constructor(
proto: ProgramObject,
@@ -171,24 +180,25 @@ export class ProgramURL extends ProgramObject {
}
}
/** Built-in objects that wrap a host value; data-like, but never plain data. */
export const isWrapper = (
value: unknown,
): value is ProgramDate | ProgramRegExp | ProgramMap | ProgramSet | ProgramURL | ProgramURLSearchParams =>
value instanceof ProgramDate ||
value instanceof ProgramRegExp ||
value instanceof ProgramMap ||
value instanceof ProgramSet ||
value instanceof ProgramURL ||
value instanceof ProgramURLSearchParams
/** An instance of an extension class: the host object lives in a field no property path reaches. */
export class ProgramHandle extends ProgramObject {
constructor(
proto: ProgramObject,
readonly instance: object,
) {
super(proto)
}
}
const MAX_ARRAY_LENGTH = 4_294_967_295
export const isWrapper = (value: unknown): value is ProgramWrapper => value instanceof ProgramWrapper
const MAX_ARRAY_INDEX = 4_294_967_295
export const parseArrayIndex = (key: string | number): number | undefined => {
const property = String(key)
if (!/^(0|[1-9]\d*)$/.test(property)) return undefined
const index = Number(property)
return index < MAX_ARRAY_LENGTH ? index : undefined
return index < MAX_ARRAY_INDEX ? index : undefined
}
const canonical = (key: PropertyKey): string | symbol => (typeof key === "symbol" ? key : String(key))
@@ -250,7 +260,8 @@ const writeArray = (target: ProgramArray, name: string | symbol, value: unknown)
}
if (name !== "length") return undefined
const length = typeof value === "number" ? value : Number(value)
if (!Number.isInteger(length) || length < 0 || length > MAX_ARRAY_LENGTH) return false
if (!Number.isInteger(length) || length < 0) return false
checkArrayLength(length)
target.items.length = length
return true
}
@@ -290,7 +301,12 @@ export const define = (target: ProgramObject, key: PropertyKey, value: unknown,
target.props.set(name, { value, ...attrs })
}
export const defineAccessor = (target: ProgramObject, key: PropertyKey, get: Getter, set?: Setter): void => {
export const defineAccessor = (
target: ProgramObject,
key: PropertyKey,
get: Getter | undefined,
set?: Setter,
): void => {
target.props.set(canonical(key), { get, set, enumerable: false, configurable: true })
}
+44 -54
View File
@@ -1,6 +1,7 @@
import { Cause, Deferred, Effect, Exit, Fiber, Scope } from "effect"
import type { Diagnostic } from "../codemode.js"
import { type AstNode, InterpreterRuntimeError, ProgramThrow } from "./model.js"
import { MAX_PENDING_PROMISES } from "./limits.js"
import { CallSite, ProgramThrow, rangeError, typeError } from "./model.js"
import {
Callable,
define,
@@ -13,7 +14,7 @@ import {
record,
} from "./objects.js"
import { constructor, fn, methods, native, receiver, requiresNew } from "./native.js"
import { caughtErrorValue, createAggregateErrorValue, normalizeError } from "./errors.js"
import { createAggregateErrorValue, locate, materialize, normalizeError } from "./errors.js"
import { typeofValue } from "./references.js"
import { applyCollectionCallback, isSupportedCallback, type Runner } from "./runner.js"
@@ -49,10 +50,16 @@ export class PromiseRuntime<R> {
}
create(effect: Effect.Effect<unknown, unknown, R>): Effect.Effect<ProgramPromise, never, R> {
return Effect.suspend(() => {
return Effect.flatMap(CallSite, (site) => {
if (this.active.size >= MAX_PENDING_PROMISES) {
throw rangeError(
`Too many pending promises (limit ${MAX_PENDING_PROMISES}); await promises before creating more.`,
)
}
// Allocate before forking so reruns get distinct IDs and diagnostics retain creation order.
const id = this.nextID++
return Effect.map(Effect.forkIn(effect, this.scope, { startImmediately: true }), (fiber) => {
const body = Effect.catchDefect(effect, (defect) => Effect.die(locate(defect, site.node)))
return Effect.map(Effect.forkIn(body, this.scope, { startImmediately: true }), (fiber) => {
const promise = new ProgramPromise(this.proto, fiber)
this.active.add(promise)
this.ids.set(promise, id)
@@ -105,16 +112,14 @@ export class PromiseRuntime<R> {
}
}
export const selfResolutionError = (node?: AstNode): InterpreterRuntimeError =>
new InterpreterRuntimeError("Chaining cycle detected: a promise cannot resolve with itself.", node)
export const resolvePromiseValue = <R>(
runner: Runner<R>,
value: unknown,
node: AstNode,
own?: { promise?: ProgramPromise },
): Effect.Effect<unknown, unknown, R> => {
if (own?.promise !== undefined && value === own.promise) return Effect.fail(selfResolutionError(node))
if (own?.promise !== undefined && value === own.promise) {
return Effect.die(typeError("Chaining cycle detected: a promise cannot resolve with itself."))
}
if (value instanceof ProgramPromise) return runner.settlePromise(value)
if (!(value instanceof ProgramObject)) return Effect.succeed(value)
const then = get(value, "then")
@@ -128,12 +133,12 @@ export const resolvePromiseValue = <R>(
const reject = capability(runner, "reject", (reason) =>
Deferred.doneUnsafe(deferred, Exit.fail(new ProgramThrow(reason))),
)
const executed = yield* Effect.exit(runner.invokeCallable(then, value, [resolve, reject], node))
const executed = yield* Effect.exit(runner.invokeCallable(then, value, [resolve, reject]))
if (!Exit.isSuccess(executed)) {
if (Cause.hasInterruptsOnly(executed.cause)) return yield* Effect.failCause(executed.cause)
Deferred.doneUnsafe(deferred, Exit.fail(Cause.squash(executed.cause)))
}
return yield* resolvePromiseValue(runner, yield* Deferred.await(deferred), node, own)
return yield* resolvePromiseValue(runner, yield* Deferred.await(deferred), own)
})
}
@@ -141,10 +146,9 @@ export const resolvePromise = <R>(
runner: Runner<R>,
promises: PromiseRuntime<R>,
value: unknown,
node: AstNode,
): Effect.Effect<ProgramPromise, never, R> => {
if (value instanceof ProgramPromise) return Effect.succeed(value)
return promises.createWithSelf((self) => resolvePromiseValue(runner, value, node, self))
return promises.createWithSelf((self) => resolvePromiseValue(runner, value, self))
}
const promiseStatics = ["all", "allSettled", "race", "any", "resolve", "reject"] as const
@@ -154,10 +158,9 @@ const invokePromiseMethod = <R>(
promises: PromiseRuntime<R>,
name: (typeof promiseStatics)[number],
args: Array<unknown>,
node: AstNode,
): Effect.Effect<unknown, unknown, R> => {
if (name === "resolve") {
return resolvePromise(runner, promises, args[0], node)
return resolvePromise(runner, promises, args[0])
}
if (name === "reject") {
return promises.create(Effect.fail(new ProgramThrow(args[0])))
@@ -165,15 +168,13 @@ const invokePromiseMethod = <R>(
return promises.create(
Effect.gen(function* () {
const cursor = yield* runner.syncIterator(args[0], node)
if (cursor === undefined) {
throw new InterpreterRuntimeError(`Promise.${name} expects an array or other synchronous iterable.`, node)
}
const cursor = yield* runner.syncIterator(args[0])
if (cursor === undefined) throw typeError(`Promise.${name} expects an array or other synchronous iterable.`)
const items: Array<ProgramPromise> = []
while (true) {
const step = yield* cursor.next
if (step.done) break
const item = yield* resolvePromise(runner, promises, step.value, node)
const item = yield* resolvePromise(runner, promises, step.value)
promises.markObserved(item)
items.push(item)
}
@@ -201,7 +202,7 @@ const invokePromiseMethod = <R>(
outcomes.push(
record(runner.prototypes.Object, {
status: "rejected",
reason: caughtErrorValue(runner, Cause.squash(exit.cause)),
reason: materialize(runner, Cause.squash(exit.cause)),
}),
)
}
@@ -210,10 +211,7 @@ const invokePromiseMethod = <R>(
}
if (name === "race") {
if (items.length === 0) {
throw new InterpreterRuntimeError(
"Promise.race([]) would never settle; provide at least one promise or value.",
node,
)
throw typeError("Promise.race([]) would never settle; provide at least one promise or value.")
}
return yield* settleAfterTurn(Effect.flatten(Effect.raceAll(items.map((item) => promises.await(item)))))
}
@@ -221,7 +219,7 @@ const invokePromiseMethod = <R>(
Effect.flatMap(promises.await(item), (exit) => {
if (Exit.isSuccess(exit)) return Effect.fail(new PromiseAnyFulfilled(exit.value))
if (Cause.hasInterruptsOnly(exit.cause)) return Effect.failCause(exit.cause)
return Effect.succeed(caughtErrorValue(runner, Cause.squash(exit.cause)))
return Effect.succeed(materialize(runner, Cause.squash(exit.cause)))
}),
)
return yield* settleAfterTurn(
@@ -244,41 +242,36 @@ const instanceMethod = <R>(
name: "then" | "catch" | "finally",
thisValue: unknown,
args: Array<unknown>,
node: AstNode,
): Effect.Effect<ProgramPromise, unknown, R> => {
const method = `Promise.prototype.${name}`
const promise = receiver(ProgramPromise, thisValue, method, node)
const promise = receiver(ProgramPromise, thisValue, method)
promises.markObserved(promise)
if (name === "finally") {
return chainFinally(runner, promises, promise, reactionHandler(args[0], method, node), method, node)
return chainFinally(runner, promises, promise, reactionHandler(args[0], method), method)
}
const onFulfilled = name === "then" ? reactionHandler(args[0], method, node) : undefined
const onRejected = reactionHandler(name === "then" ? args[1] : args[0], method, node)
return chainReaction(runner, promises, promise, onFulfilled, onRejected, method, node)
const onFulfilled = name === "then" ? reactionHandler(args[0], method) : undefined
const onRejected = reactionHandler(name === "then" ? args[1] : args[0], method)
return chainReaction(runner, promises, promise, onFulfilled, onRejected, method)
}
const constructPromise = <R>(
runner: Runner<R>,
promises: PromiseRuntime<R>,
executor: unknown,
node: AstNode,
): Effect.Effect<ProgramPromise, unknown, R> => {
if (!(executor instanceof ProgramFunction)) {
throw new InterpreterRuntimeError(
"new Promise(...) expects an executor function (e.g. new Promise((resolve, reject) => { ... })).",
node,
)
throw typeError("new Promise(...) expects an executor function (e.g. new Promise((resolve, reject) => { ... })).")
}
return Effect.gen(function* () {
const deferred = Deferred.makeUnsafe<unknown, unknown>()
const promise = yield* promises.createWithSelf((self) =>
Effect.flatMap(Deferred.await(deferred), (value) => resolvePromiseValue(runner, value, node, self)),
Effect.flatMap(Deferred.await(deferred), (value) => resolvePromiseValue(runner, value, self)),
)
const resolve = capability(runner, "resolve", (value) => Deferred.doneUnsafe(deferred, Exit.succeed(value)))
const reject = capability(runner, "reject", (value) =>
Deferred.doneUnsafe(deferred, Exit.fail(new ProgramThrow(value))),
)
const executed = yield* Effect.exit(runner.invokeCallable(executor, undefined, [resolve, reject], node))
const executed = yield* Effect.exit(runner.invokeCallable(executor, undefined, [resolve, reject]))
if (!Exit.isSuccess(executed)) {
if (Cause.hasInterruptsOnly(executed.cause)) return yield* Effect.failCause(executed.cause)
Deferred.doneUnsafe(deferred, Exit.fail(Cause.squash(executed.cause)))
@@ -295,12 +288,11 @@ class PromiseAnyFulfilled {
constructor(readonly value: unknown) {}
}
const reactionHandler = (value: unknown, method: string, node: AstNode): Callable | undefined => {
const reactionHandler = (value: unknown, method: string): Callable | undefined => {
if (isSupportedCallback(value)) return value
if (typeofValue(value) === "function") {
throw new InterpreterRuntimeError(
throw typeError(
`${method} cannot use this callable as a handler; wrap it in an arrow function, e.g. (value) => tools.ns.tool(value).`,
node,
)
}
return undefined
@@ -325,16 +317,15 @@ const chainReaction = <R>(
onFulfilled: Callable | undefined,
onRejected: Callable | undefined,
method: string,
node: AstNode,
): Effect.Effect<ProgramPromise, never, R> => {
return promises.createWithSelf((self) =>
Effect.gen(function* () {
const exit = yield* reactionExit(promises, source)
const handler = Exit.isSuccess(exit) ? onFulfilled : onRejected
if (handler === undefined) return yield* exit
const input = Exit.isSuccess(exit) ? exit.value : caughtErrorValue(runner, Cause.squash(exit.cause))
const result = yield* applyCollectionCallback(runner, handler, method, node)([input])
return yield* resolvePromiseValue(runner, result, node, self)
const input = Exit.isSuccess(exit) ? exit.value : materialize(runner, Cause.squash(exit.cause))
const result = yield* applyCollectionCallback(runner, handler, method)([input])
return yield* resolvePromiseValue(runner, result, self)
}),
)
}
@@ -345,16 +336,15 @@ const chainFinally = <R>(
source: ProgramPromise,
cleanup: Callable | undefined,
method: string,
node: AstNode,
): Effect.Effect<ProgramPromise, never, R> =>
promises.create(
Effect.gen(function* () {
const exit = yield* reactionExit(promises, source)
if (cleanup !== undefined) {
const result = yield* applyCollectionCallback(runner, cleanup, method, node)([])
const result = yield* applyCollectionCallback(runner, cleanup, method)([])
const intermediate = yield* promises.create(
Effect.gen(function* () {
yield* runner.settlePromise(yield* resolvePromise(runner, promises, result, node))
yield* runner.settlePromise(yield* resolvePromise(runner, promises, result))
return yield* exit
}),
)
@@ -371,7 +361,7 @@ export const promiseGlobal = <R>(runner: Runner<R>, promises: PromiseRuntime<R>)
name: "Promise",
length: 1,
call: requiresNew("Promise"),
construct: (args, _, node) => constructPromise(runner, promises, args[0], node),
construct: (args) => constructPromise(runner, promises, args[0]),
})
// Combinators are not callbacks: `[p].map(Promise.resolve)` must ask for an arrow function.
for (const name of promiseStatics) {
@@ -381,16 +371,16 @@ export const promiseGlobal = <R>(runner: Runner<R>, promises: PromiseRuntime<R>)
native<R>(protos, {
name,
length: 1,
call: (_, args, node) => invokePromiseMethod(runner, promises, name, args, node),
call: (_, args) => invokePromiseMethod(runner, promises, name, args),
callback: false,
}),
hidden,
)
}
methods(protos, proto, [
["then", 2, (thisValue, args, node) => instanceMethod(runner, promises, "then", thisValue, args, node)],
["catch", 1, (thisValue, args, node) => instanceMethod(runner, promises, "catch", thisValue, args, node)],
["finally", 1, (thisValue, args, node) => instanceMethod(runner, promises, "finally", thisValue, args, node)],
["then", 2, (thisValue, args) => instanceMethod(runner, promises, "then", thisValue, args)],
["catch", 1, (thisValue, args) => instanceMethod(runner, promises, "catch", thisValue, args)],
["finally", 1, (thisValue, args) => instanceMethod(runner, promises, "finally", thisValue, args)],
])
return promise
}
@@ -1,26 +1,23 @@
import { ToolReference } from "../tool-runtime.js"
import { type AstNode, InterpreterRuntimeError } from "./model.js"
import { invalidData } from "./model.js"
import {
Callable,
getOwn,
isWrapper,
ownKeys,
ProgramArray,
ProgramDate,
ProgramGenerator,
ProgramMap,
ProgramHandle,
ProgramObject,
ProgramPromise,
ProgramRegExp,
ProgramSet,
ProgramURL,
ProgramURLSearchParams,
ProgramWrapper,
} from "./objects.js"
/** Values that cannot cross the data boundary. */
export const isRuntimeReference = (value: unknown): boolean =>
value instanceof Callable ||
value instanceof ProgramGenerator ||
value instanceof ProgramHandle ||
value instanceof ToolReference ||
value instanceof ProgramPromise ||
isWrapper(value)
@@ -66,11 +63,10 @@ export const rejectCircularInsertion = (
container: object,
value: unknown,
label: string,
node: AstNode,
seen = new Set<object>(),
): void => {
if (find(value, (current) => current === container, isRuntimeReference, seen)) {
throw new InterpreterRuntimeError(`${label} contains a circular value.`, node, "InvalidDataValue")
throw invalidData(`${label} contains a circular value.`)
}
}
@@ -79,13 +75,9 @@ export const describeValue = (value: unknown): string => {
if (value instanceof ProgramArray) return "an array"
if (value instanceof ProgramPromise) return "an un-awaited Promise"
if (value instanceof ToolReference) return "a tool reference"
if (value instanceof ProgramDate) return "a Date"
if (value instanceof ProgramRegExp) return "a RegExp"
if (value instanceof ProgramMap) return "a Map"
if (value instanceof ProgramSet) return "a Set"
if (value instanceof ProgramURL) return "a URL"
if (value instanceof ProgramURLSearchParams) return "a URLSearchParams"
if (value instanceof ProgramWrapper) return `a ${value.kind}`
if (value instanceof ProgramGenerator) return "a generator"
if (value instanceof ProgramHandle) return `a ${value.instance.constructor.name}`
if (isRuntimeReference(value)) return "a function"
if (typeof value === "object") return "a data object"
return `a ${typeof value}`
+11 -15
View File
@@ -1,7 +1,7 @@
import { Effect, Exit } from "effect"
import { coerceToNumber, coerceToString } from "../stdlib/value.js"
import type { Prototypes } from "./intrinsics.js"
import { type AstNode, InterpreterRuntimeError } from "./model.js"
import { typeError } from "./model.js"
import { Callable, get, NativeFunction, ProgramDate, ProgramObject, ProgramPromise } from "./objects.js"
import { typeofValue } from "./references.js"
@@ -16,10 +16,9 @@ export type Runner<R> = {
callable: unknown,
thisValue: unknown,
args: Array<unknown>,
node: AstNode,
) => Effect.Effect<unknown, unknown, R>
readonly settlePromise: (promise: ProgramPromise) => Effect.Effect<unknown, unknown, never>
readonly syncIterator: (value: unknown, node: AstNode) => Effect.Effect<IteratorCursor<R> | undefined, unknown, R>
readonly syncIterator: (value: unknown) => Effect.Effect<IteratorCursor<R> | undefined, unknown, R>
readonly prototypes: Prototypes
}
@@ -41,7 +40,6 @@ export const toPrimitive = <R>(
runner: Runner<R>,
value: unknown,
hint: "number" | "string" | "default",
node: AstNode,
): Effect.Effect<unknown, unknown, R> => {
if (!(value instanceof ProgramObject)) return Effect.succeed(value)
const asString = hint === "string" || (hint === "default" && value instanceof ProgramDate)
@@ -50,18 +48,18 @@ export const toPrimitive = <R>(
for (const method of order) {
const callable = get(value, method)
if (!(callable instanceof Callable)) continue
const result = yield* runner.invokeCallable(callable, value, [], node)
const result = yield* runner.invokeCallable(callable, value, [])
if (result === null || (typeof result !== "object" && typeof result !== "function")) return result
}
throw new InterpreterRuntimeError("Cannot convert object to primitive value.", node)
throw typeError("Cannot convert object to primitive value.")
})
}
export const toPrimitiveString = <R>(runner: Runner<R>, value: unknown, node: AstNode) =>
Effect.map(toPrimitive(runner, value, "string", node), coerceToString)
export const toPrimitiveString = <R>(runner: Runner<R>, value: unknown) =>
Effect.map(toPrimitive(runner, value, "string"), coerceToString)
export const toPrimitiveNumber = <R>(runner: Runner<R>, value: unknown, node: AstNode) =>
Effect.map(toPrimitive(runner, value, "number", node), coerceToNumber)
export const toPrimitiveNumber = <R>(runner: Runner<R>, value: unknown) =>
Effect.map(toPrimitive(runner, value, "number"), coerceToNumber)
// The single acceptance list for callbacks: collections, sort, string replacers,
// Array.from mappers, and promise reactions all admit exactly these callables.
@@ -74,16 +72,14 @@ export const applyCollectionCallback = <R>(
runner: Runner<R>,
callback: unknown,
name: string,
node: AstNode,
): ((args: Array<unknown>) => Effect.Effect<unknown, unknown, R>) => {
if (!isSupportedCallback(callback)) {
if (typeofValue(callback) === "function") {
throw new InterpreterRuntimeError(
throw typeError(
`${name} cannot use this callable as a callback; wrap it in an arrow function, e.g. (value) => tools.ns.tool(value).`,
node,
)
}
throw new InterpreterRuntimeError(`${name} expects a function callback.`, node)
throw typeError(`${name} expects a function callback.`)
}
return (callbackArgs) => runner.invokeCallable(callback, undefined, callbackArgs, node)
return (callbackArgs) => runner.invokeCallable(callback, undefined, callbackArgs)
}
+169 -141
View File
@@ -48,18 +48,20 @@ import {
type AstNode,
AsyncIteratorSymbol,
type Binding,
CallSite,
type GeneratorRequestKind,
GeneratorReturn,
InterpreterRuntimeError,
IteratorSymbol,
locate,
OptionalShortCircuit,
invalidData,
ProgramThrow,
rangeError,
type StatementResult,
typeError,
unsupportedSyntax,
} from "./model.js"
import { caughtErrorValue } from "./errors.js"
import { checkStringLength } from "./limits.js"
import { locate, materialize } from "./errors.js"
import type { Prototypes } from "./intrinsics.js"
import { globals, type Host } from "./globals.js"
import {
@@ -124,11 +126,11 @@ const calleeDescription = (callee: Expression | Super | undefined): string => {
// OrdinaryHasInstance: walk the left operand's chain looking for the constructor's `prototype`.
const instanceofValue = (lhs: unknown, rhs: unknown, node: AstNode): boolean => {
if (!(rhs instanceof Callable)) {
throw new InterpreterRuntimeError("The right-hand side of 'instanceof' is not callable.", node)
throw typeError("The right-hand side of 'instanceof' is not callable.", node)
}
const prototype = get(rhs, "prototype")
if (!(prototype instanceof ProgramObject)) {
throw new InterpreterRuntimeError("The right-hand side of 'instanceof' has no 'prototype' object.", node)
throw typeError("The right-hand side of 'instanceof' has no 'prototype' object.", node)
}
return hasPrototype(lhs, prototype)
}
@@ -207,7 +209,7 @@ const loopDeclaration = (left: VariableDeclaration | Pattern, statement: "for...
if (left.type !== "VariableDeclaration") return undefined
const declaration = left.declarations.length === 1 ? left.declarations[0] : undefined
if (declaration === undefined) {
throw new InterpreterRuntimeError(`${statement} supports one declared binding.`, left)
throw typeError(`${statement} supports one declared binding.`, left)
}
const kind = left.kind
return {
@@ -246,8 +248,6 @@ type GeneratorState = {
available?: Deferred.Deferred<void>
}
const promiseResolutionNode: AstNode = { type: "PromiseResolution", start: 0, end: 0 }
/** One program execution: the tool bridge, promise scheduler, captured logs, and the global scope built once. */
export class Runtime<R> {
readonly runner: Runner<R>
@@ -266,9 +266,9 @@ export class Runtime<R> {
// Calling back into the program never reads frame state, so any frame serves; the root is always alive.
this.root = new Frame(this, new ScopeStack([globalScope]))
this.runner = {
invokeCallable: (callable, thisValue, args, node) => this.root.invokeCallable(callable, thisValue, args, node),
invokeCallable: (callable, thisValue, args) => this.root.invokeCallable(callable, thisValue, args),
settlePromise: (promise) => this.root.settlePromise(promise),
syncIterator: (value, node) => this.root.syncIterator(value, node),
syncIterator: (value) => this.root.syncIterator(value),
prototypes,
}
for (const [name, value] of [...globals(this), ...extraGlobals(this)]) {
@@ -281,6 +281,8 @@ export class Runtime<R> {
}
}
const MAX_CALL_DEPTH = 10_000
/** One activation: the top-level program or a single function call, evaluating against its own scope chain. */
class Frame<R> {
private generatorState?: GeneratorState
@@ -289,6 +291,8 @@ class Frame<R> {
constructor(
private readonly runtime: Runtime<R>,
private scopes: ScopeStack,
/** Nested call depth; resets when an await resumes, since the continuation runs from the job queue. */
private depth = 0,
) {}
run(program: Program): Effect.Effect<unknown, unknown, R> {
@@ -313,12 +317,12 @@ class Frame<R> {
}
if (result.kind === "break" || result.kind === "continue") {
throw new InterpreterRuntimeError(`Unexpected '${result.kind}' outside of a loop.`, statement)
throw typeError(`Unexpected '${result.kind}' outside of a loop.`, statement)
}
}
// The implicit async body adopts returned promises before copy-out.
value = yield* resolvePromiseValue(self.runtime.runner, value, program)
value = yield* resolvePromiseValue(self.runtime.runner, value)
return value
}).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop())))
}
@@ -483,7 +487,7 @@ class Frame<R> {
return Effect.gen(function* () {
const discriminant = yield* self.evaluateExpression(node.discriminant)
if (containsOpaqueReference(discriminant)) {
throw new InterpreterRuntimeError("Switch discriminants must be data values.", node, "InvalidDataValue")
throw invalidData("Switch discriminants must be data values.", node)
}
self.scopes.push()
return yield* Effect.gen(function* () {
@@ -501,7 +505,7 @@ class Frame<R> {
}
const candidate = yield* self.evaluateExpression(test)
if (containsOpaqueReference(candidate)) {
throw new InterpreterRuntimeError("Switch case values must be data values.", test, "InvalidDataValue")
throw invalidData("Switch case values must be data values.", test)
}
if (candidate === discriminant) {
selected = index
@@ -624,7 +628,7 @@ class Frame<R> {
const iterator = yield* self.customIterator(right, node, awaiting)
const cursor = iterator === undefined ? yield* self.syncIterator(right, node) : undefined
if (iterator === undefined && cursor === undefined) {
throw new InterpreterRuntimeError(
throw invalidData(
`${awaiting ? "for await...of" : "for...of"} requires an array, string, Map, Set, or URLSearchParams, or custom iterator value.`,
node,
)
@@ -637,7 +641,7 @@ class Frame<R> {
: (cursor?.close ?? Effect.void)
if (left.type === "RestElement" || left.type === "AssignmentPattern") {
throw new InterpreterRuntimeError("Unsupported for...of binding.", left)
throw typeError("Unsupported for...of binding.", left)
}
const assignment = left.type === "VariableDeclaration" ? undefined : left
@@ -664,7 +668,7 @@ class Frame<R> {
while (true) {
const current = iterator
? yield* self.nextIteratorResult(iterator, node, awaiting)
: yield* cursor?.next ?? Effect.fail(new InterpreterRuntimeError("Iterator is unavailable.", node))
: yield* cursor?.next ?? Effect.die(typeError("Iterator is unavailable.", node))
const step = cursor && awaiting ? { done: current.done, value: yield* self.awaitValue(current.value) } : current
if (step.done) return { kind: "none" } satisfies StatementResult
const bodyExit = yield* Effect.exit(evaluateBody(step.value))
@@ -690,16 +694,19 @@ class Frame<R> {
)
}
private awaitValue(value: unknown, node: AstNode = promiseResolutionNode): Effect.Effect<unknown, unknown, R> {
return Effect.flatMap(resolvePromise(this.runtime.runner, this.runtime.promises, value, node), (promise) =>
this.settlePromise(promise),
private awaitValue(value: unknown): Effect.Effect<unknown, unknown, R> {
return Effect.flatMap(resolvePromise(this.runtime.runner, this.runtime.promises, value), (promise) =>
Effect.ensuring(
this.settlePromise(promise),
Effect.sync(() => (this.depth = 0)),
),
)
}
private awaitAsyncFromSyncValue(
iterator: CustomIterator,
value: unknown,
node: AstNode,
node: AstNode | undefined,
closeOnRejection: boolean,
): Effect.Effect<unknown, unknown, R> {
const self = this
@@ -713,7 +720,7 @@ class Frame<R> {
})
}
syncIterator(value: unknown, node: AstNode) {
syncIterator(value: unknown, node?: AstNode) {
const iterator =
value instanceof ProgramArray
? value.items[Symbol.iterator]()
@@ -750,7 +757,7 @@ class Frame<R> {
)
}
private customIterator(value: unknown, node: AstNode, allowAsync = true) {
private customIterator(value: unknown, node: AstNode | undefined, allowAsync = true) {
if (!(value instanceof ProgramObject)) return Effect.undefined
const asyncMethod = allowAsync ? get(value, AsyncIteratorSymbol) : undefined
const method = asyncMethod ?? get(value, IteratorSymbol)
@@ -769,7 +776,7 @@ class Frame<R> {
)
}
private nextIteratorResult(iterator: CustomIterator, node: AstNode, awaiting: boolean) {
private nextIteratorResult(iterator: CustomIterator, node: AstNode | undefined, awaiting: boolean) {
const self = this
return Effect.gen(function* () {
if (iterator.asynchronous) {
@@ -805,7 +812,11 @@ class Frame<R> {
})
}
private closeIterator(iterator: CustomIterator, node: AstNode, awaiting = true): Effect.Effect<void, unknown, R> {
private closeIterator(
iterator: CustomIterator,
node: AstNode | undefined,
awaiting = true,
): Effect.Effect<void, unknown, R> {
const close = get(iterator.iterator, "return")
if (close === undefined || close === null) return iterator.asynchronous || !awaiting ? Effect.void : Effect.yieldNow
const self = this
@@ -836,14 +847,14 @@ class Frame<R> {
})
}
private requireIteratorObject(value: unknown, context: string, node: AstNode): ProgramObject {
private requireIteratorObject(value: unknown, context: string, node?: AstNode): ProgramObject {
if (value instanceof ProgramObject) return value
throw new InterpreterRuntimeError(`${context} must be an object.`, node)
throw typeError(`${context} must be an object.`, node)
}
private requireIteratorMethod(value: unknown, context: string, node: AstNode): unknown {
private requireIteratorMethod(value: unknown, context: string, node?: AstNode): unknown {
if (typeofValue(value) === "function") return value
throw new InterpreterRuntimeError(`${context} must be a function.`, node)
throw typeError(`${context} must be a function.`, node)
}
// for...in over null/undefined iterates nothing, like JS.
@@ -869,7 +880,7 @@ class Frame<R> {
const keys = self.enumerableKeys(right, node.right)
if (left.type !== "Identifier" && left.type !== "VariableDeclaration") {
throw new InterpreterRuntimeError("Unsupported for...in binding.", left)
throw typeError("Unsupported for...in binding.", left)
}
const assignmentName = left.type === "Identifier" ? left.name : undefined
@@ -955,7 +966,7 @@ class Frame<R> {
return Effect.failCause(cause)
}
const caught = caughtErrorValue(self.runtime.runner, Cause.squash(cause))
const caught = materialize(self.runtime.runner, Cause.squash(cause))
const parameter = handler.param
self.scopes.push()
return Effect.gen(function* () {
@@ -991,7 +1002,7 @@ class Frame<R> {
return Effect.gen(function* () {
for (const declaration of node.declarations) {
if (declaration.type !== "VariableDeclarator") {
throw new InterpreterRuntimeError("Unsupported variable declaration shape.", declaration)
throw typeError("Unsupported variable declaration shape.", declaration)
}
const init = declaration.init
@@ -1033,10 +1044,9 @@ class Frame<R> {
if (pattern.type === "ObjectPattern") {
if (!(value instanceof ProgramObject)) {
throw new InterpreterRuntimeError(
throw typeError(
`Object destructuring requires a data object or array value, received ${describeValue(value)}.`,
pattern,
"InvalidDataValue",
)
}
@@ -1051,7 +1061,13 @@ class Frame<R> {
const key = yield* self.destructuringPropertyKey(property)
consumed.add(typeof key === "symbol" ? key : String(key))
yield* self.declarePattern(property.value, get(value, key), mutable, property, initialize)
yield* self.declarePattern(
property.value,
self.readProperty(value, key, property),
mutable,
property,
initialize,
)
}
return
}
@@ -1062,7 +1078,7 @@ class Frame<R> {
)
}
throw new InterpreterRuntimeError(`Unsupported binding pattern '${pattern.type}'.`, pattern)
throw typeError(`Unsupported binding pattern '${pattern.type}'.`, pattern)
})
}
@@ -1087,10 +1103,9 @@ class Frame<R> {
if (pattern.type === "ObjectPattern") {
if (!(value instanceof ProgramObject)) {
throw new InterpreterRuntimeError(
throw invalidData(
`Object destructuring requires a data object or array value, received ${describeValue(value)}.`,
pattern,
"InvalidDataValue",
)
}
@@ -1104,7 +1119,7 @@ class Frame<R> {
}
const key = yield* self.destructuringPropertyKey(property)
consumed.add(typeof key === "symbol" ? key : String(key))
yield* self.assignPattern(property.value, get(value, key), property)
yield* self.assignPattern(property.value, self.readProperty(value, key, property), property)
}
return
}
@@ -1115,7 +1130,7 @@ class Frame<R> {
)
}
throw new InterpreterRuntimeError(`Unsupported assignment pattern '${pattern.type}'.`, node)
throw typeError(`Unsupported assignment pattern '${pattern.type}'.`, node)
})
}
@@ -1134,7 +1149,7 @@ class Frame<R> {
return Effect.gen(function* () {
const cursor = yield* self.syncIterator(value, pattern)
if (cursor === undefined) {
throw new InterpreterRuntimeError("Array destructuring requires a supported iterable value.", pattern)
throw typeError("Array destructuring requires a supported iterable value.", pattern)
}
let done = false
for (const element of pattern.elements) {
@@ -1171,7 +1186,7 @@ class Frame<R> {
private destructuringPropertyKey(property: Property | AssignmentProperty): Effect.Effect<PropertyKey, unknown, R> {
if (property.type !== "Property" || property.kind !== "init") {
throw new InterpreterRuntimeError("Unsupported object destructuring property.", property)
throw typeError("Unsupported object destructuring property.", property)
}
const keyNode = property.key
if (property.computed) {
@@ -1186,8 +1201,7 @@ class Frame<R> {
switch (node.type) {
case "Literal": {
const regex = node.regex
if (regex)
return Effect.sync(() => constructRegExp(this.runtime.prototypes, [regex.pattern, regex.flags], node))
if (regex) return Effect.sync(() => constructRegExp(this.runtime.prototypes, [regex.pattern, regex.flags]))
return Effect.sync(() => toProgram(this.runtime.prototypes, node.value, "Literal"))
}
case "Identifier":
@@ -1233,7 +1247,7 @@ class Frame<R> {
return this.evaluateUpdateExpression(node)
case "AwaitExpression": {
// Await always suspends, including for plain values.
return Effect.flatMap(this.evaluateExpression(node.argument), (value) => this.awaitValue(value, node))
return Effect.flatMap(this.evaluateExpression(node.argument), (value) => this.awaitValue(value))
}
case "YieldExpression":
return this.evaluateYieldExpression(node)
@@ -1261,10 +1275,10 @@ class Frame<R> {
: callee instanceof NativeFunction
? `new ${name}(...) is not supported; call ${name}(...) without new instead.`
: `${name} is not a constructor.`
throw new InterpreterRuntimeError(message, node)
throw typeError(message, node)
}
const args = yield* self.evaluateCallArguments(node.arguments)
return yield* construct(args, callee as NativeFunction<R>, node)
return yield* self.native(() => construct(args, callee as NativeFunction<R>), node)
})
}
@@ -1292,7 +1306,7 @@ class Frame<R> {
return has(rhs, lhs !== null && typeof lhs === "object" ? coerceToString(lhs) : (lhs as PropertyKey))
}
if (containsOpaqueReference(lhs) || containsOpaqueReference(rhs)) {
throw new InterpreterRuntimeError("Binary operators require data values.", node, "InvalidDataValue")
throw invalidData("Binary operators require data values.", node)
}
// Null-prototype data needs explicit primitive coercion; identity and `in` retain raw objects.
// Dates use their default string hint for addition and loose equality, and epoch time elsewhere.
@@ -1306,8 +1320,11 @@ class Frame<R> {
const l = coerceOperand(lhs)
const r = coerceOperand(rhs)
switch (operator) {
case "+":
return (l as string) + (r as string)
case "+": {
const sum = (l as string) + (r as string)
if (typeof sum === "string") checkStringLength(sum.length)
return sum
}
case "-":
return (l as number) - (r as number)
case "*":
@@ -1344,11 +1361,11 @@ class Frame<R> {
return (l as number) >>> (r as number)
case "in":
if (!(rhs instanceof ProgramObject)) {
throw new InterpreterRuntimeError("The 'in' operator requires a data object on the right-hand side.", node)
throw typeError("The 'in' operator requires a data object on the right-hand side.", node)
}
return has(rhs, coerceOperand(lhs) as PropertyKey)
default:
throw new InterpreterRuntimeError(`Unsupported binary operator '${operator}'.`, node)
throw typeError(`Unsupported binary operator '${operator}'.`, node)
}
}
@@ -1359,7 +1376,7 @@ class Frame<R> {
if (operator === "||") return left ? Effect.succeed(left) : this.evaluateExpression(node.right)
if (operator === "??")
return left !== null && left !== undefined ? Effect.succeed(left) : this.evaluateExpression(node.right)
throw new InterpreterRuntimeError(`Unsupported logical operator '${operator}'.`, node)
throw typeError(`Unsupported logical operator '${operator}'.`, node)
})
}
@@ -1376,7 +1393,7 @@ class Frame<R> {
if (operator === "!") return !value
if (operator === "void") return undefined
if (containsOpaqueReference(value)) {
throw new InterpreterRuntimeError("Unary operators require data values.", node, "InvalidDataValue")
throw invalidData("Unary operators require data values.", node)
}
const operand =
value instanceof ProgramDate
@@ -1396,7 +1413,7 @@ class Frame<R> {
result = ~(operand as number)
break
default:
throw new InterpreterRuntimeError(`Unsupported unary operator '${operator}'.`, node)
throw typeError(`Unsupported unary operator '${operator}'.`, node)
}
return toProgram(this.runtime.prototypes, result, "Unary expression result")
})
@@ -1443,7 +1460,7 @@ class Frame<R> {
}),
)
}
throw new InterpreterRuntimeError("Assignment target must be an Identifier or MemberExpression.", left)
throw typeError("Assignment target must be an Identifier or MemberExpression.", left)
})
}
@@ -1475,7 +1492,7 @@ class Frame<R> {
: Effect.succeed({ write: false, next: current, result: current }),
)
}
throw new InterpreterRuntimeError("Assignment target must be an Identifier or MemberExpression.", left)
throw typeError("Assignment target must be an Identifier or MemberExpression.", left)
}
private evaluateUpdateExpression(node: UpdateExpression): Effect.Effect<unknown, unknown, R> {
@@ -1486,14 +1503,14 @@ class Frame<R> {
const increment = operator === "++" ? 1 : operator === "--" ? -1 : undefined
if (increment === undefined) {
throw new InterpreterRuntimeError(`Unsupported update operator '${operator}'.`, node)
throw typeError(`Unsupported update operator '${operator}'.`, node)
}
// CodeMode numeric coercion, not host Number(): null-prototype data objects would make
// the host throw during ToPrimitive, and opaque runtime references must reject clearly.
const operand = (current: unknown): number => {
if (containsOpaqueReference(current)) {
throw new InterpreterRuntimeError(`'${operator}' requires a data value.`, argument, "InvalidDataValue")
throw invalidData(`'${operator}' requires a data value.`, argument)
}
return coerceToNumber(current)
}
@@ -1516,7 +1533,7 @@ class Frame<R> {
})
}
throw new InterpreterRuntimeError("Update target must be an Identifier or MemberExpression.", argument)
throw typeError("Update target must be an Identifier or MemberExpression.", argument)
}
// EvaluateCall: a member callee supplies its base object as `this`; anything else calls with undefined.
@@ -1552,23 +1569,34 @@ class Frame<R> {
callable: unknown,
thisValue: unknown,
args: Array<unknown>,
node: AstNode,
node?: AstNode,
callee?: Expression,
): Effect.Effect<unknown, unknown, R> {
const self = this
return Effect.gen(function* () {
if (callable instanceof ToolReference) {
if (callable.path.length === 0) {
throw new InterpreterRuntimeError("The tools root is not callable.", callee ?? node)
throw typeError("The tools root is not callable.", callee ?? node)
}
return yield* self.createToolCallPromise(callable.path, args)
}
if (callable instanceof ProgramFunction) return yield* self.invokeFunction(callable, args)
if (callable instanceof NativeFunction) return yield* (callable as NativeFunction<R>).call(thisValue, args, node)
throw new InterpreterRuntimeError(`${calleeDescription(callee)} is not a function.`, callee ?? node)
if (callable instanceof ProgramFunction) return yield* self.invokeFunction(callable, args, node)
if (callable instanceof NativeFunction) {
return yield* self.native(() => (callable as NativeFunction<R>).call(thisValue, args), node)
}
throw typeError(`${calleeDescription(callee)} is not a function.`, callee ?? node)
})
}
// Built-ins throw without a location, synchronously or inside their Effect; the call site supplies it.
private native(body: () => Effect.Effect<unknown, unknown, R>, node?: AstNode): Effect.Effect<unknown, unknown, R> {
return Effect.provideService(
Effect.catchDefect(Effect.suspend(body), (defect) => Effect.die(locate(defect, node))),
CallSite,
{ node, depth: this.depth },
)
}
private evaluateCallArguments(
argNodes: ReadonlyArray<Expression | SpreadElement>,
): Effect.Effect<Array<unknown>, unknown, R> {
@@ -1579,8 +1607,7 @@ class Frame<R> {
if (argNode.type === "SpreadElement") {
const spread = yield* self.evaluateExpression(argNode.argument)
const cursor = yield* self.syncIterator(spread, argNode)
if (cursor === undefined)
throw new InterpreterRuntimeError("Spread arguments require a synchronous iterable.", argNode)
if (cursor === undefined) throw typeError("Spread arguments require a synchronous iterable.", argNode)
while (true) {
const step = yield* cursor.next
if (step.done) break
@@ -1594,45 +1621,50 @@ class Frame<R> {
})
}
invokeFunction(fn: ProgramFunction, args: Array<unknown>): Effect.Effect<unknown, unknown, R> {
// A callback invoked by a built-in runs below the call that invoked the built-in, so the deeper of the two counts.
invokeFunction(fn: ProgramFunction, args: Array<unknown>, node?: AstNode): Effect.Effect<unknown, unknown, R> {
const self = this
const invocation = new Frame(this.runtime, new ScopeStack([...fn.capturedScopes, new Map()]))
const run = Effect.gen(function* () {
// Seed all parameters first so defaults cannot fall through to same-named outer bindings.
const paramScope = invocation.scopes.current()
for (const parameter of fn.parameters) {
for (const name of collectPatternNames(parameter)) {
paramScope.set(name, { mutable: true, value: undefined, initialized: false })
return Effect.flatMap(CallSite, (site) => {
const depth = Math.max(self.depth, site.depth) + 1
if (depth > MAX_CALL_DEPTH) throw rangeError("Maximum call stack size exceeded", node)
const invocation = new Frame(this.runtime, new ScopeStack([...fn.capturedScopes, new Map()]), depth)
const run = Effect.gen(function* () {
// Seed all parameters first so defaults cannot fall through to same-named outer bindings.
const paramScope = invocation.scopes.current()
for (const parameter of fn.parameters) {
for (const name of collectPatternNames(parameter)) {
paramScope.set(name, { mutable: true, value: undefined, initialized: false })
}
}
}
for (const [index, parameter] of fn.parameters.entries()) {
if (parameter.type === "RestElement") {
yield* invocation.declarePattern(
parameter.argument,
new ProgramArray(self.runtime.prototypes.Array, args.slice(index)),
true,
parameter,
true,
)
break
for (const [index, parameter] of fn.parameters.entries()) {
if (parameter.type === "RestElement") {
yield* invocation.declarePattern(
parameter.argument,
new ProgramArray(self.runtime.prototypes.Array, args.slice(index)),
true,
parameter,
true,
)
break
}
yield* invocation.declarePattern(parameter, args[index], true, parameter, true)
}
yield* invocation.declarePattern(parameter, args[index], true, parameter, true)
}
if (fn.body.type === "BlockStatement") {
invocation.scopes.push()
invocation.hoistVars(fn.body.body, paramScope)
const result = yield* invocation.evaluateStatement(fn.body)
return result.kind === "return" ? result.value : undefined
}
if (fn.body.type === "BlockStatement") {
invocation.scopes.push()
invocation.hoistVars(fn.body.body, paramScope)
const result = yield* invocation.evaluateStatement(fn.body)
return result.kind === "return" ? result.value : undefined
}
return yield* invocation.evaluateExpression(fn.body)
return yield* invocation.evaluateExpression(fn.body)
})
if (fn.generator) return Effect.succeed(this.createGenerator(invocation, run, fn.async))
if (!fn.async) return run
return this.runtime.promises.createWithSelf((self) =>
Effect.flatMap(run, (value) => resolvePromiseValue(invocation.runtime.runner, value, self)),
)
})
if (fn.generator) return Effect.succeed(this.createGenerator(invocation, run, fn.async))
if (!fn.async) return run
return this.runtime.promises.createWithSelf((self) =>
Effect.flatMap(run, (value) => resolvePromiseValue(invocation.runtime.runner, value, fn.body, self)),
)
}
private createGenerator(
@@ -1645,11 +1677,9 @@ class Frame<R> {
invocation.generatorAsync = asynchronous
const protos = this.runtime.prototypes
const result = (value: unknown, done: boolean) => record(protos.Object, { value, done })
const request = (kind: GeneratorRequestKind, value: unknown, node: AstNode) => {
const request = (kind: GeneratorRequestKind, value: unknown) => {
const request = { kind, value, response: Deferred.makeUnsafe<unknown, unknown>() }
if (!asynchronous && state.active) {
return Effect.fail(new InterpreterRuntimeError("Generator is already running.", node))
}
if (!asynchronous && state.active) return Effect.die(typeError("Generator is already running."))
if (asynchronous && (state.completed || (!state.started && kind !== "next"))) {
state.started = true
state.completed = true
@@ -1769,7 +1799,7 @@ class Frame<R> {
const argument = node.argument
const self = this
return Effect.gen(function* () {
if (!self.generatorState) throw new InterpreterRuntimeError("yield is only valid inside a generator.", node)
if (!self.generatorState) throw typeError("yield is only valid inside a generator.", node)
if (node.delegate) {
const value = argument ? yield* self.evaluateExpression(argument) : undefined
return yield* self.delegateYield(value, node)
@@ -1782,7 +1812,7 @@ class Frame<R> {
private suspendGenerator(value: unknown, node: AstNode): Effect.Effect<unknown, unknown, R> {
const state = this.generatorState
if (!state?.active) throw new InterpreterRuntimeError("Generator has no active request.", node)
if (!state?.active) throw typeError("Generator has no active request.", node)
Deferred.doneUnsafe(
state.active.response,
Exit.succeed(record(this.runtime.prototypes.Object, { value, done: false })),
@@ -1809,7 +1839,7 @@ class Frame<R> {
value instanceof ProgramURLSearchParams
) {
const cursor = yield* self.syncIterator(value, node)
if (!cursor) throw new InterpreterRuntimeError("Built-in iterator is unavailable.", node)
if (!cursor) throw typeError("Built-in iterator is unavailable.", node)
while (true) {
const step = yield* cursor.next
if (step.done) return undefined
@@ -1824,14 +1854,14 @@ class Frame<R> {
}
if (error instanceof ProgramThrow) {
yield* cursor.close
throw new InterpreterRuntimeError("The delegated iterator does not provide a throw() method.", node)
throw typeError("The delegated iterator does not provide a throw() method.", node)
}
return yield* Effect.failCause(resumed.cause)
}
}
const iterator = yield* self.customIterator(value, node, self.generatorAsync)
if (!iterator) throw new InterpreterRuntimeError("yield* requires a compatible iterable value.", node)
if (!iterator) throw typeError("yield* requires a compatible iterable value.", node)
let kind: GeneratorRequestKind = "next"
let input: unknown = undefined
while (true) {
@@ -1839,7 +1869,7 @@ class Frame<R> {
if (method === undefined || method === null) {
if (kind === "return") return yield* Effect.fail(new GeneratorReturn(input))
yield* self.closeIterator(iterator, node, self.generatorAsync)
throw new InterpreterRuntimeError("The delegated iterator does not provide a throw() method.", node)
throw typeError("The delegated iterator does not provide a throw() method.", node)
}
const called = yield* self.invokeCallable(
self.requireIteratorMethod(method, `Iterator ${kind}`, node),
@@ -1891,7 +1921,7 @@ class Frame<R> {
}
if (property.kind !== "init") {
throw new InterpreterRuntimeError("Only init object properties are supported.", property)
throw typeError("Only init object properties are supported.", property)
}
const keyNode = property.key
@@ -1905,7 +1935,7 @@ class Frame<R> {
} else if (keyNode.type === "Literal") {
key = self.toPropertyKey(keyNode.value, keyNode)
} else {
throw new InterpreterRuntimeError("Unsupported object property key shape.", keyNode)
throw typeError("Unsupported object property key shape.", keyNode)
}
const name =
@@ -1935,8 +1965,7 @@ class Frame<R> {
if (element.type === "SpreadElement") {
const spread = yield* self.evaluateExpression(element.argument)
const cursor = yield* self.syncIterator(spread, element)
if (cursor === undefined)
throw new InterpreterRuntimeError("Array spread requires a synchronous iterable.", element)
if (cursor === undefined) throw typeError("Array spread requires a synchronous iterable.", element)
while (true) {
const step = yield* cursor.next
if (step.done) break
@@ -1962,13 +1991,15 @@ class Frame<R> {
const quasi = quasis[index]!
// acorn only omits `cooked` for invalid escapes in tagged templates, which are unsupported.
if (typeof quasi.value.cooked !== "string") {
throw new InterpreterRuntimeError("Invalid template literal quasi.", quasi)
throw typeError("Invalid template literal quasi.", quasi)
}
output += quasi.value.cooked
checkStringLength(output.length)
if (index < expressions.length) {
const raw = yield* self.evaluateExpression(expressions[index])
output += coerceToString(toProgram(self.runtime.prototypes, raw, "Template interpolation"))
checkStringLength(output.length)
}
}
@@ -1984,7 +2015,7 @@ class Frame<R> {
private applyCompoundAssignment(operator: string, current: unknown, incoming: unknown, node: AstNode): unknown {
if (!compoundOperators.has(operator)) {
throw new InterpreterRuntimeError(`Unsupported assignment operator '${operator}'.`, node)
throw typeError(`Unsupported assignment operator '${operator}'.`, node)
}
return this.applyBinaryOperator(operator.slice(0, -1), current, incoming, node)
}
@@ -2010,7 +2041,7 @@ class Frame<R> {
if (objectValue instanceof ToolReference) {
if (typeof key !== "string") {
throw new InterpreterRuntimeError("Tool paths must use string property names.", propertyNode)
throw typeError("Tool paths must use string property names.", propertyNode)
}
return new ToolReference([...objectValue.path, key])
}
@@ -2029,26 +2060,27 @@ class Frame<R> {
if (typeof objectValue === "boolean") return { target: protos.Boolean, key, receiver: objectValue }
if (objectValue === null || objectValue === undefined) {
throw new InterpreterRuntimeError(
`Cannot read properties of ${objectValue} (reading '${String(key)}').`,
objectNode,
)
throw typeError(`Cannot read properties of ${objectValue} (reading '${String(key)}').`, objectNode)
}
throw new InterpreterRuntimeError("Cannot access a property on a non-object value.", objectNode)
throw typeError("Cannot access a property on a non-object value.", objectNode)
})
}
private readReference(reference: MemberReference, node: MemberExpression): unknown {
// Reject unknown promise properties so a missing await cannot hide.
if (reference.target instanceof ProgramPromise && !has(reference.target, reference.key)) {
throw new InterpreterRuntimeError(
throw invalidData(
"This value is an un-awaited Promise; await it first - e.g. `const result = await tools.ns.tool(...)`.",
node.object,
"InvalidDataValue",
)
}
return this.readProperty(reference.target, reference.key, node, reference.receiver)
}
// Accessors throw without a location; the member or pattern that read them supplies it.
private readProperty(target: ProgramObject, key: PropertyKey, node: AstNode, receiver: unknown = target): unknown {
try {
return get(reference.target, reference.key, reference.receiver)
return get(target, key, receiver)
} catch (error) {
throw locate(error, node)
}
@@ -2070,15 +2102,15 @@ class Frame<R> {
private evaluateDeleteExpression(argument: Expression): Effect.Effect<boolean, unknown, R> {
const target = argument.type === "ChainExpression" ? argument.expression : argument
if (target.type !== "MemberExpression") {
throw new InterpreterRuntimeError("Only data fields may be deleted.", argument)
throw typeError("Only data fields may be deleted.", argument)
}
return Effect.map(this.getMemberReference(target), (reference) => {
if (reference === OptionalShortCircuit) return true
if (reference instanceof ToolReference || "value" in reference || reference.receiver !== reference.target) {
throw new InterpreterRuntimeError("Only data fields may be deleted.", target, "InvalidDataValue")
throw invalidData("Only data fields may be deleted.", target)
}
if (remove(reference.target, reference.key)) return true
throw new InterpreterRuntimeError(`Cannot delete property '${String(reference.key)}'.`, target)
throw typeError(`Cannot delete property '${String(reference.key)}'.`, target)
})
}
@@ -2091,10 +2123,10 @@ class Frame<R> {
return Effect.gen(function* () {
const reference = yield* self.getMemberReference(node)
if (reference === OptionalShortCircuit || reference instanceof ToolReference || "value" in reference) {
throw new InterpreterRuntimeError("Only data fields may be assigned.", node)
throw typeError("Only data fields may be assigned.", node)
}
if (reference.receiver !== reference.target) {
throw new InterpreterRuntimeError(
throw typeError(
`Cannot create property '${String(reference.key)}' on ${typeof reference.receiver} '${String(reference.receiver)}'.`,
node,
)
@@ -2107,14 +2139,13 @@ class Frame<R> {
}
private assignToReference(target: ProgramObject, key: PropertyKey, next: unknown, node: AstNode): void {
rejectCircularInsertion(
target,
next,
target instanceof ProgramArray ? "Array assignment result" : "Object assignment result",
node,
)
const written = (() => {
try {
rejectCircularInsertion(
target,
next,
target instanceof ProgramArray ? "Array assignment result" : "Object assignment result",
)
return set(target, key, next)
} catch (error) {
throw locate(error, node)
@@ -2122,7 +2153,7 @@ class Frame<R> {
})()
if (written) return
if (target instanceof ProgramArray && key === "length") throw rangeError("Invalid array length", node)
throw new InterpreterRuntimeError(`Cannot assign to read only property '${String(key)}'.`, node)
throw typeError(`Cannot assign to read only property '${String(key)}'.`, node)
}
private toPropertyKey(value: unknown, node: AstNode): PropertyKey {
@@ -2131,9 +2162,6 @@ class Frame<R> {
}
if (value === AsyncIteratorSymbol || value === IteratorSymbol) return value
throw new InterpreterRuntimeError(
"Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator.",
node,
)
throw typeError("Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator.", node)
}
}
+6 -6
View File
@@ -1,4 +1,4 @@
import { type AstNode, type Binding, InterpreterRuntimeError, referenceError } from "./model.js"
import { type AstNode, type Binding, referenceError, typeError } from "./model.js"
export class ScopeStack {
private readonly scopes: Array<Map<string, Binding>>
@@ -10,7 +10,7 @@ export class ScopeStack {
reserve(name: string, mutable: boolean, node: AstNode): void {
const scope = this.current()
if (scope.has(name)) {
throw new InterpreterRuntimeError(`Identifier '${name}' has already been declared.`, node)
throw typeError(`Identifier '${name}' has already been declared.`, node)
}
scope.set(name, { mutable, value: undefined, initialized: false })
}
@@ -18,7 +18,7 @@ export class ScopeStack {
initialize(name: string, value: unknown, node: AstNode): void {
const binding = this.current().get(name)
if (!binding || binding.initialized !== false) {
throw new InterpreterRuntimeError(`Identifier '${name}' has not been reserved for initialization.`, node)
throw typeError(`Identifier '${name}' has not been reserved for initialization.`, node)
}
binding.value = value
binding.initialized = true
@@ -27,7 +27,7 @@ export class ScopeStack {
declare(name: string, value: unknown, mutable: boolean, node: AstNode): void {
const scope = this.current()
if (scope.has(name)) {
throw new InterpreterRuntimeError(`Identifier '${name}' has already been declared.`, node)
throw typeError(`Identifier '${name}' has already been declared.`, node)
}
scope.set(name, { mutable, value, initialized: true })
}
@@ -58,7 +58,7 @@ export class ScopeStack {
}
if (!binding.mutable) {
throw new InterpreterRuntimeError(`Cannot assign to constant '${name}'.`, node)
throw typeError(`Cannot assign to constant '${name}'.`, node)
}
binding.value = value
@@ -82,7 +82,7 @@ export class ScopeStack {
const scope = this.scopes[this.scopes.length - 1]
if (!scope) {
throw new InterpreterRuntimeError("Interpreter scope stack is empty.")
throw typeError("Interpreter scope stack is empty.")
}
return scope
+103 -109
View File
@@ -1,43 +1,37 @@
import { Effect } from "effect"
import { constructor, type Method, methods, prototypeFrom, receiver } from "../interpreter/native.js"
import { type AstNode, InterpreterRuntimeError, rangeError } from "../interpreter/model.js"
import { checkArrayLength, checkStringLength, MAX_ARRAY_LENGTH } from "../interpreter/limits.js"
import { invalidData, rangeError, typeError } from "../interpreter/model.js"
import { get, ProgramArray, ProgramGenerator, ProgramObject } from "../interpreter/objects.js"
import { describeValue, rejectCircularInsertion } from "../interpreter/references.js"
import { applyCollectionCallback, preserveConsumerError, type Runner } from "../interpreter/runner.js"
import { compareText } from "../tool-runtime.js"
import { coerceToNumber, coerceToString } from "./value.js"
const MAX_LENGTH = 4_294_967_295
const arrayLikeSource = (
source: unknown,
node: AstNode,
): { readonly length: number; readonly source: ProgramObject } => {
const arrayLikeSource = (source: unknown): { readonly length: number; readonly source: ProgramObject } => {
if (source instanceof ProgramObject && typeof get(source, "length") === "number") {
const length = get(source, "length") as number
const normalized = Number.isNaN(length) || length <= 0 ? 0 : Math.trunc(length)
if (normalized > MAX_LENGTH) throw new RangeError("Invalid array length")
checkArrayLength(normalized)
return { length: normalized, source }
}
throw new InterpreterRuntimeError(
throw invalidData(
`Array.from expects an array, string, Map, Set, or array-like value, received ${describeValue(source)}.`,
node,
"InvalidDataValue",
)
}
const arrayFrom = <R>(runner: Runner<R>, args: Array<unknown>, node: AstNode): Effect.Effect<unknown, unknown, R> => {
const arrayFrom = <R>(runner: Runner<R>, args: Array<unknown>): Effect.Effect<unknown, unknown, R> => {
const source = args[0]
const proto = runner.prototypes.Array
const apply =
args.length < 2 || args[1] === undefined ? undefined : applyCollectionCallback(runner, args[1], "Array.from", node)
args.length < 2 || args[1] === undefined ? undefined : applyCollectionCallback(runner, args[1], "Array.from")
return Effect.gen(function* () {
const cursor = yield* runner.syncIterator(source, node)
const cursor = yield* runner.syncIterator(source)
if (cursor === undefined) {
if (source instanceof ProgramGenerator) {
throw new InterpreterRuntimeError("Array.from expects a synchronous iterable or array-like value.", node)
throw typeError("Array.from expects a synchronous iterable or array-like value.")
}
const arrayLike = arrayLikeSource(source, node)
const arrayLike = arrayLikeSource(source)
const values: Array<unknown> = []
for (let index = 0; index < arrayLike.length; index += 1) {
const item = get(arrayLike.source, index)
@@ -61,12 +55,11 @@ export const sortArray = <R>(
target: Array<unknown>,
comparator: unknown,
name: string,
node: AstNode,
): Effect.Effect<Array<unknown>, unknown, R> => {
if (comparator === undefined) {
return Effect.sync(() => [...target].sort((a, b) => compareText(coerceToString(a), coerceToString(b))))
}
const apply = applyCollectionCallback(runner, comparator, name, node)
const apply = applyCollectionCallback(runner, comparator, name)
const mergeSort = (items: Array<unknown>): Effect.Effect<Array<unknown>, unknown, R> => {
if (items.length <= 1) return Effect.succeed(items)
const midpoint = Math.floor(items.length / 2)
@@ -95,32 +88,31 @@ export const arrayGlobal = <R>(runner: Runner<R>) => {
const protos = runner.prototypes
const proto = protos.Array
const wrap = (items: Array<unknown>) => new ProgramArray(proto, items)
const construct = (args: Array<unknown>, into: ProgramObject, node: AstNode): ProgramArray => {
const construct = (args: Array<unknown>, into: ProgramObject): ProgramArray => {
if (args.length !== 1) return new ProgramArray(into, [...args])
const first = args[0]
if (typeof first !== "number") return new ProgramArray(into, [first])
if (!Number.isInteger(first) || first < 0 || first > MAX_LENGTH) throw rangeError("Invalid array length.", node)
if (!Number.isInteger(first) || first < 0 || first > MAX_ARRAY_LENGTH) throw rangeError("Invalid array length.")
// Sparse like JS: Array(3) has holes, and combinator loops already skip them.
return new ProgramArray(into, new Array(first))
}
const array = constructor<R>(protos, proto, {
name: "Array",
length: 1,
call: (_, args, node) => Effect.sync(() => construct(args, proto, node)),
construct: (args, newTarget, node) => Effect.sync(() => construct(args, prototypeFrom(newTarget, proto), node)),
call: (_, args) => Effect.sync(() => construct(args, proto)),
construct: (args, newTarget) => Effect.sync(() => construct(args, prototypeFrom(newTarget, proto))),
})
methods(protos, array, [
["isArray", 1, (_, args) => args[0] instanceof ProgramArray],
["of", 0, (_, args) => wrap([...args])],
["from", 1, (_, args, node) => arrayFrom(runner, args, node)],
["from", 1, (_, args) => arrayFrom(runner, args)],
])
const self = (thisValue: unknown, name: string, node: AstNode) =>
receiver(ProgramArray, thisValue, `Array.prototype.${name}`, node)
const optNumber = (name: string, value: unknown, label: string, node: AstNode): number | undefined => {
const self = (thisValue: unknown, name: string) => receiver(ProgramArray, thisValue, `Array.prototype.${name}`)
const optNumber = (name: string, value: unknown, label: string): number | undefined => {
if (value === undefined) return undefined
if (typeof value !== "number") {
throw new InterpreterRuntimeError(`Array.${name} expects ${label} to be a number.`, node)
throw typeError(`Array.${name} expects ${label} to be a number.`)
}
return value
}
@@ -133,14 +125,13 @@ export const arrayGlobal = <R>(runner: Runner<R>) => {
receiver: ProgramArray,
apply: (args: Array<unknown>) => Effect.Effect<unknown, unknown, R>,
args: Array<unknown>,
node: AstNode,
) => Effect.Effect<unknown, unknown, R>,
): Method => [
name,
length,
(thisValue, args, node) => {
const target = self(thisValue, name, node)
return body(target.items, target, applyCollectionCallback(runner, args[0], `Array.${name}`, node), args, node)
(thisValue, args) => {
const target = self(thisValue, name)
return body(target.items, target, applyCollectionCallback(runner, args[0], `Array.${name}`), args)
},
]
@@ -148,89 +139,92 @@ export const arrayGlobal = <R>(runner: Runner<R>) => {
[
"join",
1,
(thisValue, args, node) => {
const target = self(thisValue, "join", node).items
(thisValue, args) => {
const target = self(thisValue, "join").items
if (args.length > 1 || (args.length === 1 && typeof args[0] !== "string")) {
throw new InterpreterRuntimeError("Array.join expects zero arguments or one string separator.", node)
throw typeError("Array.join expects zero arguments or one string separator.")
}
return target.map((item) => coerceToString(item ?? "")).join(args.length === 0 ? "," : (args[0] as string))
const joined = target
.map((item) => coerceToString(item ?? ""))
.join(args.length === 0 ? "," : (args[0] as string))
checkStringLength(joined.length)
return joined
},
],
[
"toString",
0,
(thisValue, _, node) =>
self(thisValue, "toString", node)
(thisValue) =>
self(thisValue, "toString")
.items.map((item) => coerceToString(item ?? ""))
.join(","),
],
[
"includes",
1,
(thisValue, args, node) => {
const target = self(thisValue, "includes", node).items
(thisValue, args) => {
const target = self(thisValue, "includes").items
if (args.length === 0 || args.length > 2) {
throw new InterpreterRuntimeError("Array.includes expects a value and optional start index.", node)
throw typeError("Array.includes expects a value and optional start index.")
}
return target.includes(args[0], optNumber("includes", args[1], "start index", node))
return target.includes(args[0], optNumber("includes", args[1], "start index"))
},
],
[
"indexOf",
1,
(thisValue, args, node) =>
self(thisValue, "indexOf", node).items.indexOf(args[0], optNumber("indexOf", args[1], "start index", node)),
(thisValue, args) =>
self(thisValue, "indexOf").items.indexOf(args[0], optNumber("indexOf", args[1], "start index")),
],
[
"lastIndexOf",
1,
(thisValue, args, node) => {
const target = self(thisValue, "lastIndexOf", node).items
(thisValue, args) => {
const target = self(thisValue, "lastIndexOf").items
return args[1] === undefined
? target.lastIndexOf(args[0])
: target.lastIndexOf(args[0], optNumber("lastIndexOf", args[1], "start index", node))
: target.lastIndexOf(args[0], optNumber("lastIndexOf", args[1], "start index"))
},
],
[
"at",
1,
(thisValue, args, node) => self(thisValue, "at", node).items.at(optNumber("at", args[0], "index", node) ?? 0),
],
["at", 1, (thisValue, args) => self(thisValue, "at").items.at(optNumber("at", args[0], "index") ?? 0)],
[
"slice",
2,
(thisValue, args, node) =>
(thisValue, args) =>
wrap(
self(thisValue, "slice", node).items.slice(
optNumber("slice", args[0], "start", node),
optNumber("slice", args[1], "end", node),
self(thisValue, "slice").items.slice(
optNumber("slice", args[0], "start"),
optNumber("slice", args[1], "end"),
),
),
],
[
"concat",
1,
(thisValue, args, node) =>
wrap(
self(thisValue, "concat", node).items.concat(
...args.map((item) => (item instanceof ProgramArray ? item.items : item)),
),
),
(thisValue, args) => {
const joined = self(thisValue, "concat").items.concat(
...args.map((item) => (item instanceof ProgramArray ? item.items : item)),
)
checkArrayLength(joined.length)
return wrap(joined)
},
],
[
"flat",
0,
(thisValue, args, node) => {
(thisValue, args) => {
const flatten = (items: Array<unknown>, depth: number): Array<unknown> =>
items.flatMap((item) => (item instanceof ProgramArray && depth > 0 ? flatten(item.items, depth - 1) : [item]))
return wrap(flatten(self(thisValue, "flat", node).items, optNumber("flat", args[0], "depth", node) ?? 1))
const flattened = flatten(self(thisValue, "flat").items, optNumber("flat", args[0], "depth") ?? 1)
checkArrayLength(flattened.length)
return wrap(flattened)
},
],
[
"reverse",
0,
(thisValue, _, node) => {
const target = self(thisValue, "reverse", node)
(thisValue) => {
const target = self(thisValue, "reverse")
target.items.reverse()
return target
},
@@ -238,13 +232,13 @@ export const arrayGlobal = <R>(runner: Runner<R>) => {
[
"sort",
1,
(thisValue, args, node) => {
const target = self(thisValue, "sort", node)
(thisValue, args) => {
const target = self(thisValue, "sort")
const items = target.items
const length = items.length
const holeCount = Array.from({ length }, (_, index) => Object.hasOwn(items, index)).filter((o) => !o).length
const itemCount = length - holeCount
return Effect.map(sortArray(runner, items, args[0], "Array.sort", node), (sorted) => {
return Effect.map(sortArray(runner, items, args[0], "Array.sort"), (sorted) => {
sorted.slice(0, itemCount).forEach((item, index) => {
items[index] = item
})
@@ -258,18 +252,18 @@ export const arrayGlobal = <R>(runner: Runner<R>) => {
[
"toSorted",
1,
(thisValue, args, node) =>
Effect.map(sortArray(runner, self(thisValue, "toSorted", node).items, args[0], "Array.toSorted", node), wrap),
(thisValue, args) =>
Effect.map(sortArray(runner, self(thisValue, "toSorted").items, args[0], "Array.toSorted"), wrap),
],
["toReversed", 0, (thisValue, _, node) => wrap([...self(thisValue, "toReversed", node).items].reverse())],
["toReversed", 0, (thisValue) => wrap([...self(thisValue, "toReversed").items].reverse())],
[
"with",
2,
(thisValue, args, node) => {
const target = self(thisValue, "with", node).items
const index = optNumber("with", args[0], "index", node) ?? 0
(thisValue, args) => {
const target = self(thisValue, "with").items
const index = optNumber("with", args[0], "index") ?? 0
const resolved = index < 0 ? target.length + index : index
if (resolved < 0 || resolved >= target.length) throw rangeError("Array.with index is out of range.", node)
if (resolved < 0 || resolved >= target.length) throw rangeError("Array.with index is out of range.")
const copied = [...target]
copied[resolved] = args[1]
return wrap(copied)
@@ -278,80 +272,80 @@ export const arrayGlobal = <R>(runner: Runner<R>) => {
[
"push",
1,
(thisValue, args, node) => {
const target = self(thisValue, "push", node)
(thisValue, args) => {
const target = self(thisValue, "push")
// Validate all insertions before mutating to avoid partial cyclic updates.
for (const item of args) rejectCircularInsertion(target, item, "Array.push result", node)
for (const item of args) rejectCircularInsertion(target, item, "Array.push result")
return target.items.push(...args)
},
],
[
"unshift",
1,
(thisValue, args, node) => {
const target = self(thisValue, "unshift", node)
for (const item of args) rejectCircularInsertion(target, item, "Array.unshift result", node)
(thisValue, args) => {
const target = self(thisValue, "unshift")
for (const item of args) rejectCircularInsertion(target, item, "Array.unshift result")
return target.items.unshift(...args)
},
],
["pop", 0, (thisValue, _, node) => self(thisValue, "pop", node).items.pop()],
["shift", 0, (thisValue, _, node) => self(thisValue, "shift", node).items.shift()],
["pop", 0, (thisValue) => self(thisValue, "pop").items.pop()],
["shift", 0, (thisValue) => self(thisValue, "shift").items.shift()],
[
"splice",
2,
(thisValue, args, node) => {
const target = self(thisValue, "splice", node)
(thisValue, args) => {
const target = self(thisValue, "splice")
if (args.length === 0) return wrap(target.items.splice(0, 0))
const start = optNumber("splice", args[0], "start", node) ?? 0
const start = optNumber("splice", args[0], "start") ?? 0
if (args.length === 1) return wrap(target.items.splice(start))
const deleteCount = optNumber("splice", args[1], "delete count", node) ?? 0
const deleteCount = optNumber("splice", args[1], "delete count") ?? 0
const inserted = args.slice(2)
for (const item of inserted) rejectCircularInsertion(target, item, "Array.splice result", node)
for (const item of inserted) rejectCircularInsertion(target, item, "Array.splice result")
return wrap(target.items.splice(start, deleteCount, ...inserted))
},
],
[
"toSpliced",
2,
(thisValue, args, node) => {
const copied = [...self(thisValue, "toSpliced", node).items]
(thisValue, args) => {
const copied = [...self(thisValue, "toSpliced").items]
if (args.length === 0) return wrap(copied)
const start = optNumber("toSpliced", args[0], "start", node) ?? 0
const start = optNumber("toSpliced", args[0], "start") ?? 0
if (args.length === 1) copied.splice(start)
else copied.splice(start, optNumber("toSpliced", args[1], "delete count", node) ?? 0, ...args.slice(2))
else copied.splice(start, optNumber("toSpliced", args[1], "delete count") ?? 0, ...args.slice(2))
return wrap(copied)
},
],
[
"fill",
1,
(thisValue, args, node) => {
const target = self(thisValue, "fill", node)
rejectCircularInsertion(target, args[0], "Array.fill result", node)
target.items.fill(args[0], optNumber("fill", args[1], "start", node), optNumber("fill", args[2], "end", node))
(thisValue, args) => {
const target = self(thisValue, "fill")
rejectCircularInsertion(target, args[0], "Array.fill result")
target.items.fill(args[0], optNumber("fill", args[1], "start"), optNumber("fill", args[2], "end"))
return target
},
],
[
"copyWithin",
2,
(thisValue, args, node) => {
const target = self(thisValue, "copyWithin", node)
(thisValue, args) => {
const target = self(thisValue, "copyWithin")
target.items.copyWithin(
optNumber("copyWithin", args[0], "target index", node) ?? 0,
optNumber("copyWithin", args[1], "start", node) ?? 0,
optNumber("copyWithin", args[2], "end", node),
optNumber("copyWithin", args[0], "target index") ?? 0,
optNumber("copyWithin", args[1], "start") ?? 0,
optNumber("copyWithin", args[2], "end"),
)
return target
},
],
["keys", 0, (thisValue, _, node) => wrap(Array.from(self(thisValue, "keys", node).items.keys()))],
["values", 0, (thisValue, _, node) => wrap([...self(thisValue, "values", node).items])],
["keys", 0, (thisValue) => wrap(Array.from(self(thisValue, "keys").items.keys()))],
["values", 0, (thisValue) => wrap([...self(thisValue, "values").items])],
[
"entries",
0,
(thisValue, _, node) =>
wrap(Array.from(self(thisValue, "entries", node).items.entries(), ([index, item]) => wrap([index, item]))),
(thisValue) =>
wrap(Array.from(self(thisValue, "entries").items.entries(), ([index, item]) => wrap([index, item]))),
],
iterate("map", 1, (target, receiver, apply) =>
Effect.gen(function* () {
@@ -455,7 +449,7 @@ export const arrayGlobal = <R>(runner: Runner<R>) => {
return undefined
}),
),
iterate("reduce", 1, (target, receiver, apply, args, node) =>
iterate("reduce", 1, (target, receiver, apply, args) =>
Effect.gen(function* () {
const length = target.length
let start = 0
@@ -463,7 +457,7 @@ export const arrayGlobal = <R>(runner: Runner<R>) => {
if (args.length < 2) {
while (start < length && !(start in target)) start += 1
if (start === length) {
throw new InterpreterRuntimeError("Array.reduce of an empty array with no initial value.", node)
throw typeError("Array.reduce of an empty array with no initial value.")
}
accumulator = target[start]
start += 1
@@ -475,14 +469,14 @@ export const arrayGlobal = <R>(runner: Runner<R>) => {
return accumulator
}),
),
iterate("reduceRight", 1, (target, receiver, apply, args, node) =>
iterate("reduceRight", 1, (target, receiver, apply, args) =>
Effect.gen(function* () {
let start = target.length - 1
let accumulator = args[1]
if (args.length < 2) {
while (start >= 0 && !(start in target)) start -= 1
if (start < 0) {
throw new InterpreterRuntimeError("Array.reduceRight of an empty array with no initial value.", node)
throw typeError("Array.reduceRight of an empty array with no initial value.")
}
accumulator = target[start]
start -= 1

Some files were not shown because too many files have changed in this diff Show More