mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-15 13:26:20 +00:00
Compare commits
45
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f93e2826c | ||
|
|
00a2fc97f7 | ||
|
|
a54f2c12a8 | ||
|
|
648d01eec7 | ||
|
|
cdee29a821 | ||
|
|
2b855fccb2 | ||
|
|
4fa6ca00e5 | ||
|
|
76594882b4 | ||
|
|
75f046212f | ||
|
|
6bc60caab9 | ||
|
|
3468a092c9 | ||
|
|
60b88daddf | ||
|
|
8cf4e6b9b4 | ||
|
|
fee337c462 | ||
|
|
8378f6caa9 | ||
|
|
94804423f4 | ||
|
|
c62d2e8336 | ||
|
|
f64ce6aad4 | ||
|
|
dac296f963 | ||
|
|
55e1b3eb17 | ||
|
|
f1149efee7 | ||
|
|
0ee9f61e3d | ||
|
|
41df55ac15 | ||
|
|
8fc49da6eb | ||
|
|
cbe8823671 | ||
|
|
6e43875123 | ||
|
|
ce380fba29 | ||
|
|
3045156fa6 | ||
|
|
0a3dc16961 | ||
|
|
1d4d9b5fa5 | ||
|
|
752d980770 | ||
|
|
07225172df | ||
|
|
223b0271e6 | ||
|
|
93a37958a7 | ||
|
|
40f4778296 | ||
|
|
e9bb6d490b | ||
|
|
1e697962bd | ||
|
|
4418df5d62 | ||
|
|
052be04466 | ||
|
|
cd3a64b225 | ||
|
|
48c9a0a8de | ||
|
|
48875190ef | ||
|
|
476432de1e | ||
|
|
a71bb4d38c | ||
|
|
5d3019e5a1 |
@@ -9,6 +9,7 @@ on:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: typecheck
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
|
||||
@@ -32,6 +32,7 @@ target
|
||||
# Local dev files
|
||||
opencode-dev
|
||||
UPCOMING_CHANGELOG.md
|
||||
RELEASE_REVIEW.md
|
||||
logs/
|
||||
*.bun-build
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
+61
-61
@@ -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 |
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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)] })
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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: {},
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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" } },
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -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 })
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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,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,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,
|
||||
|
||||
@@ -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"]) => ({
|
||||
|
||||
@@ -8,7 +8,6 @@ export { OpenCode } from "./client.js"
|
||||
export type {
|
||||
AgentApi,
|
||||
AppApi,
|
||||
CatalogApi,
|
||||
CommandApi,
|
||||
ConfigApi,
|
||||
EventApi,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ export * from "./generated/index.js"
|
||||
export { OpenCode } from "./client.js"
|
||||
export type {
|
||||
AgentApi,
|
||||
CatalogApi,
|
||||
CommandApi,
|
||||
ConfigApi,
|
||||
EventApi,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {},
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -142,7 +142,7 @@ function fixture(read?: (url: URL) => Promise<Response | void>) {
|
||||
type: "user",
|
||||
delivery: "steer",
|
||||
payload: { text: "pending" },
|
||||
timeCreated: 1,
|
||||
time: { created: 1 },
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
@@ -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>`.
|
||||
|
||||
@@ -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)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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> =>
|
||||
|
||||
@@ -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)}.`)
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user