@ai-ecoverse/slicc-agent 4.3.5 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +8 -9
  2. package/dist/browser/guard.js +1 -1
  3. package/dist/browser/guard.js.map +2 -2
  4. package/dist/client.js +89 -84
  5. package/dist/client.js.map +3 -3
  6. package/dist/codemode/index.js +8 -12
  7. package/dist/codemode/index.js.map +2 -2
  8. package/dist/host.js +69 -117
  9. package/dist/host.js.map +3 -3
  10. package/dist/images/index.js +2 -2
  11. package/dist/images/index.js.map +2 -2
  12. package/dist/index.js +4 -13
  13. package/dist/index.js.map +2 -2
  14. package/dist/licks/extension.js +2 -2
  15. package/dist/licks/extension.js.map +2 -2
  16. package/dist/mcp/tools.js +3 -3
  17. package/dist/mcp/tools.js.map +2 -2
  18. package/dist/memory/extract.js +3 -1
  19. package/dist/memory/extract.js.map +2 -2
  20. package/dist/memory/prompt.js +2 -2
  21. package/dist/memory/prompt.js.map +2 -2
  22. package/dist/mismatch.js +9 -4
  23. package/dist/mismatch.js.map +2 -2
  24. package/dist/patches.js +1 -22
  25. package/dist/patches.js.map +2 -2
  26. package/dist/readonly.js +12 -38
  27. package/dist/readonly.js.map +2 -2
  28. package/dist/scoops/extension.js +1 -1
  29. package/dist/scoops/extension.js.map +2 -2
  30. package/dist/secrets/extension.js +13 -5
  31. package/dist/secrets/extension.js.map +2 -2
  32. package/dist/services.js +111 -13
  33. package/dist/services.js.map +2 -2
  34. package/dist/serving.js +174 -0
  35. package/dist/serving.js.map +7 -0
  36. package/dist/settings.js +4 -18
  37. package/dist/settings.js.map +2 -2
  38. package/dist/spectrum/agent-port.js +5 -1
  39. package/dist/spectrum/agent-port.js.map +2 -2
  40. package/dist/spectrum/index.js +11 -2
  41. package/dist/spectrum/index.js.map +2 -2
  42. package/dist/spectrum/notices-port.js +17 -2
  43. package/dist/spectrum/notices-port.js.map +2 -2
  44. package/dist/types/host.d.ts +15 -3
  45. package/dist/types/index.d.ts +2 -4
  46. package/dist/types/mismatch.d.ts +2 -2
  47. package/dist/types/patches.d.ts +0 -10
  48. package/dist/types/readonly.d.ts +6 -4
  49. package/dist/types/services.d.ts +13 -13
  50. package/dist/types/serving.d.ts +37 -0
  51. package/dist/types/settings.d.ts +0 -1
  52. package/dist/types/spectrum/agent-port.d.ts +2 -0
  53. package/dist/types/spectrum/index.d.ts +1 -1
  54. package/dist/types/spectrum/notices-port.d.ts +3 -1
  55. package/dist/types/wire.d.ts +10 -8
  56. package/dist/wire.js +70 -77
  57. package/dist/wire.js.map +2 -2
  58. package/package.json +11 -12
  59. package/dist/listener.js +0 -73
  60. package/dist/listener.js.map +0 -7
  61. package/dist/types/listener.d.ts +0 -11
package/README.md CHANGED
@@ -28,15 +28,15 @@ await agent.prompt('What is in /home?');
28
28
 
29
29
  - `openAgent({ models, model, storage?, registry?, settings? })` opens a durable Harness (in memory unless `storage` is given) and its root conversation, and resumes work a previous worker left unfinished.
30
30
  - `openOpfsSqliteStorage({ directory?, file? })` keeps the session in wasm SQLite ([`@sqlite.org/sqlite-wasm`](https://www.npmjs.com/package/@sqlite.org/sqlite-wasm)) in an `opfs-sahpool` pool, by default `/.slicc/agent/` in OPFS. It needs a dedicated worker. Only one worker holds a pool at a time: the opener takes a Web Lock per directory and waits until every file of the pool can be opened before sqlite-wasm installs it, because a failed install deletes the pool's directory. `openMemorySqliteStorage()` keeps the same database in memory.
31
- - `hostAgent(agent)` serves the agent with pi's protocol: a pi-server with one session, `agent`, whose Chord services are `slicc.agent.control` (send with `whenBusy` `steer`, `followUp` or `reject`, wait, withdraw, abort, compact) and `slicc.agent.transcript` (the conversation's durable view as replicated state, plus `deliveries`: how each user entry was delivered, `run`, `steer` or `follow-up`, derived by the host from where each send it accepted was placed: after a tool result it steered, queued first it's a follow-up or a late steer per its `whenBusy`, placed at once it started a run). `serveConnections(self, host)` hands it every `MessagePort` the page sends.
31
+ - `hostAgent(agent)` serves the agent from one Chord host named `agent` (`serveAgent` in [`src/host.ts`](src/host.ts)). Its remote contracts, in [`src/services.ts`](src/services.ts), are `slicc.agent.control` (send with `whenBusy` `steer`, `followUp` or `reject`, wait, withdraw, abort, compact) and `slicc.agent.transcript` (the conversation's durable view as replicated state, plus `deliveries`: how each user entry was delivered, `run`, `steer` or `follow-up`, derived by the host from where each send it accepted was placed: after a tool result it steered, queued first it's a follow-up or a late steer per its `whenBusy`, placed at once it started a run), `slicc.agent.settings`, `slicc.agent.remote` (remote control) and `slicc.agent.pages` (Panels). `serveConnections(self, host)` hands it every `MessagePort` the page sends, and each port becomes one Chord link whose principal is a seat: `owner` (a page of the owner, with its own Panels), `control` (a follower with Control) or `view` (a follower without), plus the link's number, so every link gets its own objects. Each factory picks a variant per seat. The contracts take their methods' arguments as one JSON array, and the client in [`src/serving.ts`](src/serving.ts) turns them back into the `AgentControl`, `AgentSettings`, `AgentRemote` and `AgentPages` interfaces and their replicated states, so callers don't see Chord.
32
32
  - `@ai-ecoverse/slicc-agent/page` has what the page needs (`startAgent`, `connectAgent`) without the agent's runtime: no pi-durable, pi-ai, providers or SQLite, which load only in the worker. `@ai-ecoverse/slicc-agent/spectrum` is just as light, and a test keeps both that way.
33
33
  - `startAgent({ worker })` takes the Web Lock `slicc-agent`, so one tab at a time owns the agent worker, and waits while another tab holds it. `connect()` returns an `AgentConnection` (`control`, `transcript`, `prompt()`, and `webhook(name, { id?, headers?, body })`, which hands a webhook delivery, such as one from the tray hub, to the licks without a chord `Context`), `restart()` replaces the worker (durable resumes from SQLite), and `release()` stops it and lets the next tab take over. `port()` returns a raw `MessagePort` into the worker instead, for another tab: hand it over (through a SharedWorker, say), and that tab calls `connectAgent(port)`. A caller that already holds the lock passes `held: true`. A connection's `closed` resolves when its port ends (with the error, if any), and from then on its states keep their last values.
34
34
  - **Followers over a link** (PR 18): `port({ readOnly: true })` and `connect({ readOnly: true })` give a view-only connection, and `port({ pages: false })` a full one that never takes `panel` requests (see Panels).
35
35
  - **What a view-only connection can do:** it sees the transcript, the agents, settings state and remote-control `watch`, and may call `wait` and `changesOpen`.
36
- - **What it can't:** every other `AgentControl` method, settings action and remote-control `prompt` or `abort` is refused with `EACCES` ("this follower can watch, but not send, steer or abort"). Send and compact answer `accepted: false` with that code. The worker serves view-only ports from a second pi-server whose session gets these narrowed services.
36
+ - **What it can't:** every other `AgentControl` method, settings action and remote-control `prompt` or `abort` is refused with `EACCES` ("this follower can watch, but not send, steer or abort"). Send and compact answer `accepted: false` with that code. The `view` seat's variants don't have those methods at all (`view` for control and settings, `watch` for remote control, `none` for Panels); the follower's client answers the missing ones with the refusals ([`src/readonly.ts`](src/readonly.ts)), so the worker never runs them. A `watch` stream is readable only over the link that opened it.
37
37
  - **Carrying the connection over a byte stream:** `linkEndpoint({ write, close })` is a `PortEndpoint` that `connectAgent` accepts. Feed it the stream's bytes with `receive(bytes)` and its end with `end(error?)`. On the owner's side, `bridgePort(port, { write, close })` does the same for the `MessagePort` from `port()`.
38
- - **The bytes:** each `PortChannel` message becomes a frame: a `u32` big-endian length, then a tag (0 data, 1 hello with the server id, 2 close), then the payload. A frame is at most 16 MiB, and a malformed one ends the link.
39
- - **A newer owner:** agent 5.0 greets with `slicc-agent-link/2 <server id>` and speaks Chord frames instead of pi-protocol. A follower that gets such a hello closes the port at once and gets a connection whose `mismatch` is "That SLICC runs a newer version of the agent than this one, so this tab can't follow it. Update this SLICC, then reload this page." Its states are empty, every call rejects with `EPROTO` and that sentence, and `closed` stays pending, so a follower that retries on close doesn't loop. The spectrum adapter shows the sentence as a notice in the follower's chat.
38
+ - **The bytes:** the owner first posts a hello, `slicc-agent-link/2 <server id>`, then each Chord frame travels as its JSON text in UTF-8 (`PortLink` in [`src/wire.ts`](src/wire.ts)), and `{ close: true }` ends the port. On a byte stream each message becomes a frame: a `u32` big-endian length, then a tag (0 data, 1 hello, 2 close), then the payload. A frame is at most 16 MiB, and a malformed one ends the link.
39
+ - **Another agent version:** a follower whose owner greets with another link protocol (an agent before 5.0 greets with its bare server id, which reads as protocol 1), or whose contracts don't cover the follower's (`contract_mismatch`), closes the link at once and gets a connection whose `mismatch` is a sentence such as "That SLICC runs an older version of the agent than this one, so this tab can't follow it. Update that SLICC, then reload this page." Its states are empty, every call rejects with `EPROTO` and that sentence, and `closed` stays pending, so a follower that retries on close doesn't loop. The spectrum adapter puts the tab in watch-only mode (`AgentAdapter.readOnly()` is "Watch only."), so it has no composer and no cone actions, and `notices` lists a banner with the sentence that can't be dismissed (`agent-mismatch`, spectrum 1.75 on); nothing reaches the owner. A 4.x follower from 4.3.2 on reads the 5.x hello the same way and shows "That SLICC runs a newer version of the agent than this one, so this tab can't follow it. Update this SLICC, then reload this page."; an older one only sees the link close.
40
40
 
41
41
  ### Bash and files on slicc-kernel
42
42
 
@@ -181,7 +181,7 @@ A send's `Outgoing.attachments` reach the model. In the page, `send()` reads eac
181
181
  - for a model whose `input` lacks `image`, an image is saved the same way and the text adds "This model can't see images, so the image was saved as a file instead." (pi-ai alone would put `(image omitted: model does not support images)` in its place, and the image would be lost to the tools.)
182
182
  - an attachment with only a `path` is mentioned by its path.
183
183
 
184
- Images are limited to 5 MiB, other files to 8 MiB, and one message's attachments to 10 MiB together, because one pi protocol frame holds 16 MiB and providers refuse bigger images anyway. The page first shrinks an image over the limit (to 1600 px, as image reads do). Anything still too big isn't sent, and a notice says why, for example "photo.png is larger than 5 MiB, so it can't be sent.". Text and inlined files go through the vault's scrubbing like any send; images can't be scrubbed as text. The replicated transcript and views don't carry the full images: the host swaps every image part over 48 KB, in entries and in the inbox, for a JPEG thumbnail of at most 256 px and 48 KB (made once per image in the worker with `OffscreenCanvas`) plus `full: { mimeType, size }`, so a conversation with many images still attaches in one 16 MiB frame. The model still gets the full image from storage. The transcript maps the parts back to spectrum's user message `attachments`, with the thumbnail as `url` and the id `ref:<entry>:<message>:<part>`. When such an attachment is sent again, by Retry or from the composer after a rewind, the adapter fetches the full image with `AgentControl.image({ entry, message, part })`. A remote `prompt()` takes the same `attachments`.
184
+ Images are limited to 5 MiB, other files to 8 MiB, and one message's attachments to 10 MiB together, because one link frame holds 16 MiB (10 MiB is about 13.4 MiB of base64 in one `send`) and providers refuse bigger images anyway. The page first shrinks an image over the limit (to 1600 px, as image reads do). Anything still too big isn't sent, and a notice says why, for example "photo.png is larger than 5 MiB, so it can't be sent.". Text and inlined files go through the vault's scrubbing like any send; images can't be scrubbed as text. The replicated transcript and views don't carry the full images: the host swaps every image part over 48 KB, in entries and in the inbox, for a JPEG thumbnail of at most 256 px and 48 KB (made once per image in the worker with `OffscreenCanvas`) plus `full: { mimeType, size }`, so a conversation with many images still attaches in one 16 MiB frame. The model still gets the full image from storage. The transcript maps the parts back to spectrum's user message `attachments`, with the thumbnail as `url` and the id `ref:<entry>:<message>:<part>`. When such an attachment is sent again, by Retry or from the composer after a rewind, the adapter fetches the full image with `AgentControl.image({ entry, message, part })`. A remote `prompt()` takes the same `attachments`.
185
185
 
186
186
  ### Licks
187
187
 
@@ -636,16 +636,15 @@ The agent never holds a secret's value. Values live in the vault (`vault.sliccy.
636
636
 
637
637
  Fixes to pi stay in this repository. [`patches/patches.json`](patches/patches.json) lists each one with its `kind`, `package`, `patchedVersion`, `reason`, `removeWhen` and `verify` command, and an optional `marker` (a file in the package and a string it must contain):
638
638
 
639
- - `monkeypatch`: code that runs in users' browsers is patched at runtime. pi-mcp parses SSE with Node's `Buffer.byteLength`, which browsers don't have. `patchSse()` in [`src/mcp/sse.ts`](src/mcp/sse.ts) replaces `StreamableHttpTransport.prototype.consumeSse` with the same parser measuring bytes with `TextEncoder`, before any server connects. `applyPatches()` in [`src/patches.ts`](src/patches.ts) applies them once before pi is used: `PortListener.start()` calls it, so `hostAgent` and a `PortListener` used directly with pi's `Server` both get them. Each one checks that the code it replaces still looks as expected and throws, naming its manifest entry, if not. Today there is one: pi-server's `Server.accept` calls `unref()` on its handshake timer, which browsers don't have, so the patch gives numeric timer ids a no-op `unref`.
639
+ - `monkeypatch`: code that runs in users' browsers is patched at runtime. pi-mcp parses SSE with Node's `Buffer.byteLength`, which browsers don't have. `patchSse()` in [`src/mcp/sse.ts`](src/mcp/sse.ts) replaces `StreamableHttpTransport.prototype.consumeSse` with the same parser measuring bytes with `TextEncoder`, before any server connects. It checks that the code it replaces still looks as expected and throws, naming its manifest entry (named in [`src/patches.ts`](src/patches.ts)), if not. It's the only one: pi-server's handshake-timer patch went with pi-server in 5.0.
640
640
  - `patch-package`: repository-only fixes go in `patches/<package>+<version>.patch`, applied by a `postinstall` that runs [patch-package](https://github.com/ds300/patch-package). There are none yet.
641
641
  - `environment`: something the host must provide.
642
- - pi-server imports `randomUUID` from `node:crypto`. slicc-bios serves a stub backed by `globalThis.crypto`, and the integration bundles alias it to `test/integration/shims/node-crypto.js`.
643
642
  - pi-codemode runs QuickJS in a `node:worker_threads` Worker. slicc-bios must serve a `node:worker_threads` stub that wraps a module Web Worker; inside the worker, the stub gives `parentPort` and `workerData` from the first message. `test/integration/shims/node-worker-threads.js` is that stub, and the integration bundles alias to it.
644
643
  - pi-mcp's index re-exports its stdio transport, which imports `node:child_process`, `node:process` and the CommonJS `cross-spawn` at the top. SLICC never constructs it, so slicc-bios's `node:*` stubs and CommonJS wrapping only have to let them load; `cross-spawn`, `which` and `isexe` also read the free globals `process` and `global` when they load. The MCP runtime is imported lazily, so if it fails to load, the agent still starts, and a configured `mcp.json` gets a lick saying MCP servers can't start. The integration bundles alias all three to `test/integration/shims/node-only.js`.
645
- - `environment`, pruned from seven's install: slicc-bios's `/opt/agent` (its `src/packages/agent`) removes `@google/genai` and `esbuild` with pnpm overrides in `pnpm-workspace.yaml` (#22).
644
+ - `environment`, pruned from seven's install: slicc-bios's `/opt/agent` (its `src/packages/agent`) removes `@google/genai` with a pnpm override in `pnpm-workspace.yaml` (#22). Its `esbuild` override is moot since chord 1.2.0, which depends on `typebox` instead.
646
645
  - A real Bedrock turn loads 28 of 95 installed packages and never those two trees. Adobe's APIs reach only `@anthropic-ai/sdk`, `openai` and `partial-json`, so those stay.
647
646
  - The prune takes the install from 99 to 58 packages and `node_modules` from 143 to 92 MB.
648
- - The markers keep pi-ai's two Google APIs (Gemini and Vertex) lazy and chord's esbuild Node-only. Seven offers only the Bedrock and Adobe providers: if a Google provider is ever added, the bios override has to go first, and a failing marker is the reminder.
647
+ - The markers keep pi-ai's two Google APIs (Gemini and Vertex) lazy. Seven offers only the Bedrock and Adobe providers: if a Google provider is ever added, the bios override has to go first, and a failing marker is the reminder.
649
648
 
650
649
  `npm run lint:patches` (part of `npm run lint`) fails when an installed version differs from `patchedVersion`, a marker is gone, a monkeypatch isn't named in `src/patches.ts`, a patch file and its entry don't match, or `renovate.json` doesn't route the package to the `patched dependencies` group with automerge off. That group covers every `@earendil-works/*` package, so a pi bump is always a reviewed PR: rerun each entry's `verify`, then move `patchedVersion` forward or drop the entry.
651
650
 
@@ -75,7 +75,7 @@ function browserGuard(origins, tools) {
75
75
  ...inner,
76
76
  execute(args, api, context) {
77
77
  if (!refused(commandOf(args), matcher)) return inner.execute(args, api, context);
78
- return Promise.resolve({ content: [{ type: "text", text: REFUSAL }], isError: true });
78
+ return Promise.resolve({ output: [{ type: "text", text: REFUSAL }], isError: true });
79
79
  }
80
80
  }))
81
81
  ]
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/browser/guard.ts"],
4
- "sourcesContent": ["import {\n defineExtension,\n type Extension,\n hook,\n type ToolRegistration,\n ToolTask,\n wrapTool,\n} from '@earendil-works/pi-durable';\nimport { FACADE_PORT, kernelHost, ownKernelServer } from '../kernel/loopback.ts';\nimport { PROTECTED_RULES, type ProtectedMatcher, protectedMatcher } from './origins.ts';\nimport { words } from './shell.ts';\n\nexport const BROWSER_GUARD = 'slicc-browser-guard';\nexport const REFUSAL = \"SLICC's own pages can't be opened or driven from the browser tools.\";\nexport { FACADE_PORT, ownKernelServer };\n\nconst BROWSER = new Set(['playwright-cli', 'curlwright']);\nconst WEB = new Set(['http:', 'https:', 'ws:', 'wss:']);\nconst INNER = new Set(PROTECTED_RULES.innerSchemes.map((scheme) => `${scheme}:`));\nconst SCHEMED = /^[a-z][a-z0-9+.-]*:/i;\nconst EMBEDDED = /[a-z][a-z0-9+.-]*:\\/\\/[^\\s'\"`<>(){}\\\\]+/gi;\nconst RELATIVE = /(?<![:\\w/])\\/\\/[^\\s'\"`<>(){}\\\\/]+/g;\nconst PREFIX = /^(?:[A-Za-z_][A-Za-z0-9_]*|-{1,2}[A-Za-z0-9-]+)=/;\n\nfunction parse(url: string): URL | undefined {\n try {\n return new URL(url);\n } catch {\n return undefined;\n }\n}\n\nexport function foreignKernel(url: string): boolean {\n let parsed = parse(url);\n for (let depth = 0; parsed && INNER.has(parsed.protocol) && depth < 8; depth++)\n parsed = parse(parsed.href.slice(parsed.protocol.length));\n if (!parsed || !WEB.has(parsed.protocol)) return false;\n const host = parsed.hostname.replace(/\\.+$/, '');\n return kernelHost(host) && !ownKernelServer(host);\n}\n\nexport function forbidden(url: string, matcher: ProtectedMatcher): boolean {\n return matcher.url(url) || foreignKernel(url);\n}\n\nexport function candidates(raw: string): string[] {\n const word = raw.replace(/[\\t\\n\\r]/g, '').replace(/^[\\0- ]+|[\\0- ]+$/g, '');\n const value = word.replace(PREFIX, '');\n const found = [\n word,\n value,\n ...(value.match(EMBEDDED) ?? []),\n ...(value.match(RELATIVE) ?? []).map((relative) => `https:${relative}`),\n ];\n if (!/^[/.~-]/.test(value) && !/^[a-z][a-z0-9+.-]*:\\/\\//i.test(value))\n found.push(`https://${value}`);\n if (value.startsWith('//')) found.push(`https:${value}`);\n return found.filter((each) => SCHEMED.test(each));\n}\n\nfunction browserCommand(word: string): boolean {\n return BROWSER.has(word.slice(word.lastIndexOf('/') + 1));\n}\n\nexport function refused(command: string, matcher: ProtectedMatcher): boolean {\n const all = words(command);\n if (!all.some(browserCommand)) return false;\n return all.some((word) => candidates(word).some((url) => forbidden(url, matcher)));\n}\n\nfunction commandOf(args: unknown): string {\n const command = (args as { command?: unknown } | undefined)?.command;\n return typeof command === 'string' ? command : '';\n}\n\nexport function browserGuard(\n origins: readonly string[],\n tools: readonly ToolRegistration[]\n): Extension {\n const bash = tools.find((tool) => tool.name === 'bash') as ToolRegistration;\n const matcher = protectedMatcher(origins);\n return defineExtension({\n name: BROWSER_GUARD,\n hooks: [\n hook(ToolTask, {\n beforeTool: (call) =>\n call.name === bash.name && refused(commandOf(call.arguments), matcher)\n ? { block: REFUSAL }\n : undefined,\n }),\n ],\n wraps: [\n wrapTool(bash, (inner) => ({\n ...inner,\n execute(args, api, context) {\n if (!refused(commandOf(args), matcher)) return inner.execute(args, api, context);\n return Promise.resolve({ content: [{ type: 'text', text: REFUSAL }], isError: true });\n },\n })),\n ],\n });\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import {\n defineExtension,\n type Extension,\n hook,\n type ToolRegistration,\n ToolTask,\n wrapTool,\n} from '@earendil-works/pi-durable';\nimport { FACADE_PORT, kernelHost, ownKernelServer } from '../kernel/loopback.ts';\nimport { PROTECTED_RULES, type ProtectedMatcher, protectedMatcher } from './origins.ts';\nimport { words } from './shell.ts';\n\nexport const BROWSER_GUARD = 'slicc-browser-guard';\nexport const REFUSAL = \"SLICC's own pages can't be opened or driven from the browser tools.\";\nexport { FACADE_PORT, ownKernelServer };\n\nconst BROWSER = new Set(['playwright-cli', 'curlwright']);\nconst WEB = new Set(['http:', 'https:', 'ws:', 'wss:']);\nconst INNER = new Set(PROTECTED_RULES.innerSchemes.map((scheme) => `${scheme}:`));\nconst SCHEMED = /^[a-z][a-z0-9+.-]*:/i;\nconst EMBEDDED = /[a-z][a-z0-9+.-]*:\\/\\/[^\\s'\"`<>(){}\\\\]+/gi;\nconst RELATIVE = /(?<![:\\w/])\\/\\/[^\\s'\"`<>(){}\\\\/]+/g;\nconst PREFIX = /^(?:[A-Za-z_][A-Za-z0-9_]*|-{1,2}[A-Za-z0-9-]+)=/;\n\nfunction parse(url: string): URL | undefined {\n try {\n return new URL(url);\n } catch {\n return undefined;\n }\n}\n\nexport function foreignKernel(url: string): boolean {\n let parsed = parse(url);\n for (let depth = 0; parsed && INNER.has(parsed.protocol) && depth < 8; depth++)\n parsed = parse(parsed.href.slice(parsed.protocol.length));\n if (!parsed || !WEB.has(parsed.protocol)) return false;\n const host = parsed.hostname.replace(/\\.+$/, '');\n return kernelHost(host) && !ownKernelServer(host);\n}\n\nexport function forbidden(url: string, matcher: ProtectedMatcher): boolean {\n return matcher.url(url) || foreignKernel(url);\n}\n\nexport function candidates(raw: string): string[] {\n const word = raw.replace(/[\\t\\n\\r]/g, '').replace(/^[\\0- ]+|[\\0- ]+$/g, '');\n const value = word.replace(PREFIX, '');\n const found = [\n word,\n value,\n ...(value.match(EMBEDDED) ?? []),\n ...(value.match(RELATIVE) ?? []).map((relative) => `https:${relative}`),\n ];\n if (!/^[/.~-]/.test(value) && !/^[a-z][a-z0-9+.-]*:\\/\\//i.test(value))\n found.push(`https://${value}`);\n if (value.startsWith('//')) found.push(`https:${value}`);\n return found.filter((each) => SCHEMED.test(each));\n}\n\nfunction browserCommand(word: string): boolean {\n return BROWSER.has(word.slice(word.lastIndexOf('/') + 1));\n}\n\nexport function refused(command: string, matcher: ProtectedMatcher): boolean {\n const all = words(command);\n if (!all.some(browserCommand)) return false;\n return all.some((word) => candidates(word).some((url) => forbidden(url, matcher)));\n}\n\nfunction commandOf(args: unknown): string {\n const command = (args as { command?: unknown } | undefined)?.command;\n return typeof command === 'string' ? command : '';\n}\n\nexport function browserGuard(\n origins: readonly string[],\n tools: readonly ToolRegistration[]\n): Extension {\n const bash = tools.find((tool) => tool.name === 'bash') as ToolRegistration;\n const matcher = protectedMatcher(origins);\n return defineExtension({\n name: BROWSER_GUARD,\n hooks: [\n hook(ToolTask, {\n beforeTool: (call) =>\n call.name === bash.name && refused(commandOf(call.arguments), matcher)\n ? { block: REFUSAL }\n : undefined,\n }),\n ],\n wraps: [\n wrapTool(bash, (inner) => ({\n ...inner,\n execute(args, api, context) {\n if (!refused(commandOf(args), matcher)) return inner.execute(args, api, context);\n return Promise.resolve({ output: [{ type: 'text', text: REFUSAL }], isError: true });\n },\n })),\n ],\n });\n}\n"],
5
+ "mappings": "AAAA;AAAA,EACE;AAAA,EAEA;AAAA,EAEA;AAAA,EACA;AAAA,OACK;AACP,SAAS,aAAa,YAAY,uBAAuB;AACzD,SAAS,iBAAwC,wBAAwB;AACzE,SAAS,aAAa;AAEf,MAAM,gBAAgB;AACtB,MAAM,UAAU;AAGvB,MAAM,UAAU,oBAAI,IAAI,CAAC,kBAAkB,YAAY,CAAC;AACxD,MAAM,MAAM,oBAAI,IAAI,CAAC,SAAS,UAAU,OAAO,MAAM,CAAC;AACtD,MAAM,QAAQ,IAAI,IAAI,gBAAgB,aAAa,IAAI,CAAC,WAAW,GAAG,MAAM,GAAG,CAAC;AAChF,MAAM,UAAU;AAChB,MAAM,WAAW;AACjB,MAAM,WAAW;AACjB,MAAM,SAAS;AAEf,SAAS,MAAM,KAA8B;AAC3C,MAAI;AACF,WAAO,IAAI,IAAI,GAAG;AAAA,EACpB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,cAAc,KAAsB;AAClD,MAAI,SAAS,MAAM,GAAG;AACtB,WAAS,QAAQ,GAAG,UAAU,MAAM,IAAI,OAAO,QAAQ,KAAK,QAAQ,GAAG;AACrE,aAAS,MAAM,OAAO,KAAK,MAAM,OAAO,SAAS,MAAM,CAAC;AAC1D,MAAI,CAAC,UAAU,CAAC,IAAI,IAAI,OAAO,QAAQ,EAAG,QAAO;AACjD,QAAM,OAAO,OAAO,SAAS,QAAQ,QAAQ,EAAE;AAC/C,SAAO,WAAW,IAAI,KAAK,CAAC,gBAAgB,IAAI;AAClD;AAEO,SAAS,UAAU,KAAa,SAAoC;AACzE,SAAO,QAAQ,IAAI,GAAG,KAAK,cAAc,GAAG;AAC9C;AAEO,SAAS,WAAW,KAAuB;AAChD,QAAM,OAAO,IAAI,QAAQ,aAAa,EAAE,EAAE,QAAQ,sBAAsB,EAAE;AAC1E,QAAM,QAAQ,KAAK,QAAQ,QAAQ,EAAE;AACrC,QAAM,QAAQ;AAAA,IACZ;AAAA,IACA;AAAA,IACA,GAAI,MAAM,MAAM,QAAQ,KAAK,CAAC;AAAA,IAC9B,IAAI,MAAM,MAAM,QAAQ,KAAK,CAAC,GAAG,IAAI,CAAC,aAAa,SAAS,QAAQ,EAAE;AAAA,EACxE;AACA,MAAI,CAAC,UAAU,KAAK,KAAK,KAAK,CAAC,2BAA2B,KAAK,KAAK;AAClE,UAAM,KAAK,WAAW,KAAK,EAAE;AAC/B,MAAI,MAAM,WAAW,IAAI,EAAG,OAAM,KAAK,SAAS,KAAK,EAAE;AACvD,SAAO,MAAM,OAAO,CAAC,SAAS,QAAQ,KAAK,IAAI,CAAC;AAClD;AAEA,SAAS,eAAe,MAAuB;AAC7C,SAAO,QAAQ,IAAI,KAAK,MAAM,KAAK,YAAY,GAAG,IAAI,CAAC,CAAC;AAC1D;AAEO,SAAS,QAAQ,SAAiB,SAAoC;AAC3E,QAAM,MAAM,MAAM,OAAO;AACzB,MAAI,CAAC,IAAI,KAAK,cAAc,EAAG,QAAO;AACtC,SAAO,IAAI,KAAK,CAAC,SAAS,WAAW,IAAI,EAAE,KAAK,CAAC,QAAQ,UAAU,KAAK,OAAO,CAAC,CAAC;AACnF;AAEA,SAAS,UAAU,MAAuB;AACxC,QAAM,UAAW,MAA4C;AAC7D,SAAO,OAAO,YAAY,WAAW,UAAU;AACjD;AAEO,SAAS,aACd,SACA,OACW;AACX,QAAM,OAAO,MAAM,KAAK,CAAC,SAAS,KAAK,SAAS,MAAM;AACtD,QAAM,UAAU,iBAAiB,OAAO;AACxC,SAAO,gBAAgB;AAAA,IACrB,MAAM;AAAA,IACN,OAAO;AAAA,MACL,KAAK,UAAU;AAAA,QACb,YAAY,CAAC,SACX,KAAK,SAAS,KAAK,QAAQ,QAAQ,UAAU,KAAK,SAAS,GAAG,OAAO,IACjE,EAAE,OAAO,QAAQ,IACjB;AAAA,MACR,CAAC;AAAA,IACH;AAAA,IACA,OAAO;AAAA,MACL,SAAS,MAAM,CAAC,WAAW;AAAA,QACzB,GAAG;AAAA,QACH,QAAQ,MAAM,KAAK,SAAS;AAC1B,cAAI,CAAC,QAAQ,UAAU,IAAI,GAAG,OAAO,EAAG,QAAO,MAAM,QAAQ,MAAM,KAAK,OAAO;AAC/E,iBAAO,QAAQ,QAAQ,EAAE,QAAQ,CAAC,EAAE,MAAM,QAAQ,MAAM,QAAQ,CAAC,GAAG,SAAS,KAAK,CAAC;AAAA,QACrF;AAAA,MACF,EAAE;AAAA,IACJ;AAAA,EACF,CAAC;AACH;",
6
6
  "names": []
7
7
  }
package/dist/client.js CHANGED
@@ -1,19 +1,24 @@
1
1
  import {
2
- createRemoteServiceBinding
2
+ createHost,
3
+ isChordError
3
4
  } from "@earendil-works/chord";
4
5
  import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
5
- import { Client, createClientServiceTransport } from "@earendil-works/pi-client";
6
6
  import { mismatched } from "./mismatch.js";
7
+ import { controlAnswer, pageAnswer, remoteAnswer, settingsAnswer } from "./readonly.js";
7
8
  import {
8
- AGENT_SESSION,
9
9
  AgentControl,
10
10
  AgentPagesService,
11
11
  AgentRemoteService,
12
- AgentSessions,
13
12
  AgentSettings,
14
- AgentTranscript
13
+ AgentTranscript,
14
+ CONTROL_METHODS,
15
+ PAGE_METHODS,
16
+ REMOTE_METHODS,
17
+ SETTINGS_METHODS,
18
+ TRANSCRIPT_STATES
15
19
  } from "./services.js";
16
- import { greeted, LINK_PROTOCOL, PortChannel } from "./wire.js";
20
+ import { AGENT_HOST, bound, kept, replicated } from "./serving.js";
21
+ import { greeted, LINK_PROTOCOL, PortLink } from "./wire.js";
17
22
  function lasting(state) {
18
23
  let last;
19
24
  let frozen = false;
@@ -47,90 +52,91 @@ function lastingSettings(settings, hold) {
47
52
  dismissMoved: (context) => settings.dismissMoved(context)
48
53
  };
49
54
  }
50
- function attached(client) {
51
- return new Promise((resolve) => {
52
- const stop = client.onAttachmentChange(() => {
53
- stop();
54
- resolve();
55
- });
56
- });
57
- }
55
+ const MISMATCHES = /* @__PURE__ */ new Set(["contract_mismatch", "upgrade_required"]);
56
+ const ended = (link) => link.ended.then((reason) => {
57
+ throw reason ?? new Error("the agent closed the link");
58
+ });
58
59
  async function connectAgent(endpoint, options = {}) {
59
60
  const context = options.context ?? BACKGROUND_CONTEXT;
60
- const channel = new PortChannel(endpoint);
61
- const hello = await channel.hello;
62
- const peer = greeted(hello);
61
+ const link = new PortLink(endpoint);
62
+ const peer = greeted(await link.hello);
63
63
  if (peer.protocol !== LINK_PROTOCOL) {
64
- endpoint.postMessage({ close: true });
65
- endpoint.close?.();
66
- return mismatched(peer.serverId);
64
+ link.close("link protocol mismatch");
65
+ return mismatched(peer.serverId, peer.protocol);
67
66
  }
68
- const serverId = hello;
69
- const client = await Client.connect({ serverId, transportFactory: channel.transport });
70
- const server = createRemoteServiceBinding({
71
- services: [AgentSessions],
72
- transport: createClientServiceTransport(client, () => ({ serverId })),
73
- bound: true
74
- });
75
- const ready = attached(client);
76
- await server.use(AgentSessions).attach(AGENT_SESSION, context);
77
- await ready;
78
- const session = createRemoteServiceBinding({
79
- services: [AgentControl, AgentTranscript, AgentSettings, AgentRemoteService, AgentPagesService],
80
- transport: createClientServiceTransport(client, () => client.attachment),
81
- bound: true
82
- });
83
- const control = session.use(AgentControl);
84
- const {
85
- state: transcript,
86
- deliveries,
87
- agents,
88
- views,
89
- commands,
90
- privateRevision,
91
- sprinkles,
92
- memories,
93
- memoryScopes,
94
- frozen,
95
- changes,
96
- monitor,
97
- vault,
98
- labels
99
- } = session.use(AgentTranscript);
100
- const settings = session.use(AgentSettings);
101
- const remote = session.use(AgentRemoteService);
102
- const pages = session.use(AgentPagesService);
103
- await session.ready(context);
67
+ const host = createHost({ name: `follower:${crypto.randomUUID()}`, kind: "client" });
68
+ host.connect(link);
69
+ const get = (contract) => host.get(contract, {}, { at: AGENT_HOST });
70
+ const handles = [
71
+ AgentControl,
72
+ AgentTranscript,
73
+ AgentSettings,
74
+ AgentRemoteService,
75
+ AgentPagesService
76
+ ].map(get);
77
+ const [controlHandle, transcriptHandle, settingsHandle, remoteHandle, pagesHandle] = handles;
78
+ const release = () => {
79
+ for (const handle of handles) handle.release();
80
+ };
81
+ try {
82
+ await Promise.race([Promise.all(handles.map((handle) => handle.ready)), ended(link)]);
83
+ } catch (error) {
84
+ release();
85
+ link.close("handshake failed");
86
+ if (isChordError(error) && MISMATCHES.has(error.code))
87
+ return mismatched(peer.serverId, peer.protocol);
88
+ throw error;
89
+ }
90
+ const root = (run, rootOptions) => host.root(run, rootOptions);
91
+ const control = bound(controlHandle, root, CONTROL_METHODS, controlAnswer);
92
+ const members = Object.fromEntries(
93
+ TRANSCRIPT_STATES.map((name) => [name, transcriptHandle[name]])
94
+ );
95
+ const settingsState = settingsHandle.state;
96
+ const keeping = [...Object.values(members), settingsState].map(kept);
97
+ await Promise.race([Promise.all(keeping.map((keep) => keep.ready)), ended(link)]);
98
+ const settings = {
99
+ ...bound(settingsHandle, root, SETTINGS_METHODS, settingsAnswer),
100
+ state: replicated(settingsState)
101
+ };
102
+ const remote = bound(remoteHandle, root, REMOTE_METHODS, remoteAnswer);
103
+ const pages = bound(pagesHandle, root, PAGE_METHODS, pageAnswer);
104
+ const state = (name) => replicated(members[name]);
104
105
  const held = [];
105
- const hold = (state) => {
106
- const kept = lasting(state);
107
- held.push(kept);
108
- return kept.state;
106
+ const hold = (state2) => {
107
+ const kept2 = lasting(state2);
108
+ held.push(kept2);
109
+ return kept2.state;
109
110
  };
110
- const freeze = () => {
111
- for (const kept of held) kept.freeze();
111
+ let open = true;
112
+ const finish = () => {
113
+ if (!open) return;
114
+ open = false;
115
+ for (const kept2 of held) kept2.freeze();
116
+ for (const keep of keeping) keep.stop();
117
+ release();
112
118
  };
113
- const closed = channel.ended.then((reason) => {
114
- freeze();
119
+ const closed = link.ended.then((reason) => {
120
+ finish();
115
121
  return reason;
116
122
  });
117
123
  return {
118
- serverId,
124
+ serverId: peer.serverId,
119
125
  control,
120
- transcript: hold(transcript),
121
- deliveries: hold(deliveries),
122
- agents: hold(agents),
123
- views: hold(views),
124
- commands: hold(commands),
125
- privateRevision: hold(privateRevision),
126
- sprinkles: hold(sprinkles),
127
- memories: hold(memories),
128
- memoryScopes: hold(memoryScopes),
129
- frozen: hold(frozen),
130
- changes: hold(changes),
131
- monitor: hold(monitor),
132
- vault: hold(vault),
133
- labels: hold(labels),
126
+ transcript: hold(state("state")),
127
+ deliveries: hold(state("deliveries")),
128
+ agents: hold(state("agents")),
129
+ views: hold(state("views")),
130
+ commands: hold(state("commands")),
131
+ privateRevision: hold(state("privateRevision")),
132
+ sprinkles: hold(state("sprinkles")),
133
+ memories: hold(state("memories")),
134
+ memoryScopes: hold(state("memoryScopes")),
135
+ frozen: hold(state("frozen")),
136
+ changes: hold(state("changes")),
137
+ monitor: hold(state("monitor")),
138
+ vault: hold(state("vault")),
139
+ labels: hold(state("labels")),
134
140
  settings: lastingSettings(settings, hold),
135
141
  remote,
136
142
  pages,
@@ -150,11 +156,10 @@ async function connectAgent(endpoint, options = {}) {
150
156
  return settled.text;
151
157
  },
152
158
  async close() {
153
- freeze();
154
- await server.use(AgentSessions).detach(context);
155
- await session.dispose(context);
156
- await server.dispose(context);
157
- await client.dispose();
159
+ finish();
160
+ link.close("closed");
161
+ await closed;
162
+ await host.close();
158
163
  }
159
164
  };
160
165
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/client.ts"],
4
- "sourcesContent": ["import {\n type Context,\n createRemoteServiceBinding,\n type ReplicatedState,\n} from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport { Client, createClientServiceTransport } from '@earendil-works/pi-client';\nimport type { ConversationView } from '@earendil-works/pi-durable';\nimport type { ChangesView } from './changes/git.ts';\nimport type { FrozenCone } from './freezer/index.ts';\nimport type { MemoryEntry, MemoryScope } from './memory/format.ts';\nimport { mismatched } from './mismatch.ts';\nimport type { AgentRemote } from './remote/host.ts';\nimport type { AgentPages } from './remote/pages.ts';\nimport type { VaultState } from './secrets/vault.ts';\nimport {\n AGENT_SESSION,\n AgentControl,\n AgentPagesService,\n AgentRemoteService,\n AgentSessions,\n AgentSettings,\n type AgentsSummary,\n AgentTranscript,\n type Command,\n type Delivered,\n type MonitorFeed,\n type PrivateRevision,\n type SendMode,\n} from './services.ts';\nimport type { Sprinkle } from './sprinkles/kind.ts';\nimport { greeted, LINK_PROTOCOL, PortChannel, type PortEndpoint } from './wire.ts';\n\nexport interface AgentConnection {\n readonly serverId: string;\n readonly control: AgentControl;\n readonly transcript: ReplicatedState<ConversationView>;\n readonly deliveries: ReplicatedState<Record<string, Delivered>>;\n readonly agents?: ReplicatedState<AgentsSummary>;\n readonly views?: ReplicatedState<Record<string, ConversationView>>;\n readonly commands?: ReplicatedState<Command[]>;\n readonly privateRevision?: ReplicatedState<PrivateRevision>;\n readonly sprinkles?: ReplicatedState<Sprinkle[]>;\n readonly memories?: ReplicatedState<MemoryEntry[]>;\n readonly memoryScopes?: ReplicatedState<MemoryScope[]>;\n readonly frozen?: ReplicatedState<FrozenCone[]>;\n readonly changes?: ReplicatedState<ChangesView>;\n readonly monitor?: ReplicatedState<MonitorFeed>;\n readonly vault?: ReplicatedState<VaultState>;\n readonly labels?: ReplicatedState<Record<string, string>>;\n readonly settings: AgentSettings;\n readonly remote?: AgentRemote;\n readonly pages?: AgentPages;\n readonly closed: Promise<Error | undefined>;\n readonly mismatch?: string;\n prompt(text: string, whenBusy?: SendMode): Promise<string>;\n webhook(\n name: string,\n delivery: { id?: string | null; headers?: Record<string, string>; body: string }\n ): Promise<{ delivered: boolean }>;\n close(): Promise<void>;\n}\n\nexport interface Lasting<T> {\n readonly state: ReplicatedState<T>;\n freeze(): void;\n}\n\nexport function lasting<T>(state: ReplicatedState<T>): Lasting<T> {\n let last: T | undefined;\n let frozen = false;\n const read = () => {\n if (frozen) return last;\n try {\n last = state.value;\n } catch {}\n return last;\n };\n return {\n state: {\n get value() {\n return read();\n },\n subscribe: (listener) => (frozen ? () => undefined : state.subscribe(listener)),\n },\n freeze() {\n read();\n frozen = true;\n },\n };\n}\n\nfunction lastingSettings(\n settings: AgentSettings,\n hold: <T>(state: ReplicatedState<T>) => ReplicatedState<T>\n): AgentSettings {\n return {\n state: hold(settings.state),\n connect: (providerId, secret, fields, context) =>\n settings.connect(providerId, secret, fields, context),\n disconnect: (providerId, context) => settings.disconnect(providerId, context),\n signIn: (providerId, context) => settings.signIn(providerId, context),\n dismissMoved: (context) => settings.dismissMoved(context),\n };\n}\n\nfunction attached(client: Client): Promise<void> {\n return new Promise((resolve) => {\n const stop = client.onAttachmentChange(() => {\n stop();\n resolve();\n });\n });\n}\n\nexport async function connectAgent(\n endpoint: PortEndpoint,\n options: { context?: Context } = {}\n): Promise<AgentConnection> {\n const context = options.context ?? BACKGROUND_CONTEXT;\n const channel = new PortChannel(endpoint);\n const hello = await channel.hello;\n const peer = greeted(hello);\n if (peer.protocol !== LINK_PROTOCOL) {\n endpoint.postMessage({ close: true });\n endpoint.close?.();\n return mismatched(peer.serverId);\n }\n const serverId = hello;\n const client = await Client.connect({ serverId, transportFactory: channel.transport });\n const server = createRemoteServiceBinding({\n services: [AgentSessions],\n transport: createClientServiceTransport(client, () => ({ serverId })),\n bound: true,\n });\n const ready = attached(client);\n await server.use(AgentSessions).attach(AGENT_SESSION, context);\n await ready;\n const session = createRemoteServiceBinding({\n services: [AgentControl, AgentTranscript, AgentSettings, AgentRemoteService, AgentPagesService],\n transport: createClientServiceTransport(client, () => client.attachment),\n bound: true,\n });\n const control = session.use(AgentControl);\n const {\n state: transcript,\n deliveries,\n agents,\n views,\n commands,\n privateRevision,\n sprinkles,\n memories,\n memoryScopes,\n frozen,\n changes,\n monitor,\n vault,\n labels,\n } = session.use(AgentTranscript);\n const settings = session.use(AgentSettings);\n const remote = session.use(AgentRemoteService);\n const pages = session.use(AgentPagesService);\n await session.ready(context);\n const held: Lasting<unknown>[] = [];\n const hold = <T>(state: ReplicatedState<T>): ReplicatedState<T> => {\n const kept = lasting(state);\n held.push(kept as Lasting<unknown>);\n return kept.state;\n };\n const freeze = () => {\n for (const kept of held) kept.freeze();\n };\n const closed = channel.ended.then((reason) => {\n freeze();\n return reason;\n });\n return {\n serverId,\n control,\n transcript: hold(transcript),\n deliveries: hold(deliveries),\n agents: hold(agents),\n views: hold(views),\n commands: hold(commands),\n privateRevision: hold(privateRevision),\n sprinkles: hold(sprinkles),\n memories: hold(memories),\n memoryScopes: hold(memoryScopes),\n frozen: hold(frozen),\n changes: hold(changes),\n monitor: hold(monitor),\n vault: hold(vault),\n labels: hold(labels),\n settings: lastingSettings(settings, hold),\n remote,\n pages,\n closed,\n webhook(name, delivery) {\n return control.webhook(\n name,\n { id: delivery.id ?? null, headers: delivery.headers ?? {}, body: delivery.body },\n context\n );\n },\n async prompt(text, whenBusy = 'followUp') {\n const sent = await control.send({ text, whenBusy, requestId: null }, context);\n if (!sent.accepted) throw new Error(`${sent.error.code}: ${sent.error.message}`);\n const settled = await control.wait(sent.submissionId, context);\n if (settled.status !== 'done') throw new Error(`unanswered: ${settled.reason}`);\n return settled.text;\n },\n async close() {\n freeze();\n await server.use(AgentSessions).detach(context);\n await session.dispose(context);\n await server.dispose(context);\n await client.dispose();\n },\n };\n}\n"],
5
- "mappings": "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",
6
- "names": []
4
+ "sourcesContent": ["import {\n type Context,\n createHost,\n isChordError,\n type Readable,\n type ReplicatedState,\n} from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { ConversationView } from '@earendil-works/pi-durable';\nimport type { ChangesView } from './changes/git.ts';\nimport type { FrozenCone } from './freezer/index.ts';\nimport type { MemoryEntry, MemoryScope } from './memory/format.ts';\nimport { mismatched } from './mismatch.ts';\nimport { controlAnswer, pageAnswer, remoteAnswer, settingsAnswer } from './readonly.ts';\nimport type { AgentRemote } from './remote/host.ts';\nimport type { AgentPages } from './remote/pages.ts';\nimport type { VaultState } from './secrets/vault.ts';\nimport {\n AgentControl,\n AgentPagesService,\n AgentRemoteService,\n AgentSettings,\n type AgentsSummary,\n AgentTranscript,\n CONTROL_METHODS,\n type Command,\n type Delivered,\n type MonitorFeed,\n PAGE_METHODS,\n type PrivateRevision,\n REMOTE_METHODS,\n SETTINGS_METHODS,\n type SendMode,\n type SettingsState,\n TRANSCRIPT_STATES,\n} from './services.ts';\nimport { AGENT_HOST, bound, kept, type Remote, type Root, replicated } from './serving.ts';\nimport type { Sprinkle } from './sprinkles/kind.ts';\nimport { greeted, LINK_PROTOCOL, type PortEndpoint, PortLink } from './wire.ts';\n\nexport interface AgentConnection {\n readonly serverId: string;\n readonly control: AgentControl;\n readonly transcript: ReplicatedState<ConversationView>;\n readonly deliveries: ReplicatedState<Record<string, Delivered>>;\n readonly agents?: ReplicatedState<AgentsSummary>;\n readonly views?: ReplicatedState<Record<string, ConversationView>>;\n readonly commands?: ReplicatedState<Command[]>;\n readonly privateRevision?: ReplicatedState<PrivateRevision>;\n readonly sprinkles?: ReplicatedState<Sprinkle[]>;\n readonly memories?: ReplicatedState<MemoryEntry[]>;\n readonly memoryScopes?: ReplicatedState<MemoryScope[]>;\n readonly frozen?: ReplicatedState<FrozenCone[]>;\n readonly changes?: ReplicatedState<ChangesView>;\n readonly monitor?: ReplicatedState<MonitorFeed>;\n readonly vault?: ReplicatedState<VaultState>;\n readonly labels?: ReplicatedState<Record<string, string>>;\n readonly settings: AgentSettings;\n readonly remote?: AgentRemote;\n readonly pages?: AgentPages;\n readonly closed: Promise<Error | undefined>;\n readonly mismatch?: string;\n prompt(text: string, whenBusy?: SendMode): Promise<string>;\n webhook(\n name: string,\n delivery: { id?: string | null; headers?: Record<string, string>; body: string }\n ): Promise<{ delivered: boolean }>;\n close(): Promise<void>;\n}\n\nexport interface Lasting<T> {\n readonly state: ReplicatedState<T>;\n freeze(): void;\n}\n\nexport function lasting<T>(state: ReplicatedState<T>): Lasting<T> {\n let last: T | undefined;\n let frozen = false;\n const read = () => {\n if (frozen) return last;\n try {\n last = state.value;\n } catch {}\n return last;\n };\n return {\n state: {\n get value() {\n return read();\n },\n subscribe: (listener) => (frozen ? () => undefined : state.subscribe(listener)),\n },\n freeze() {\n read();\n frozen = true;\n },\n };\n}\n\nfunction lastingSettings(\n settings: AgentSettings,\n hold: <T>(state: ReplicatedState<T>) => ReplicatedState<T>\n): AgentSettings {\n return {\n state: hold(settings.state),\n connect: (providerId, secret, fields, context) =>\n settings.connect(providerId, secret, fields, context),\n disconnect: (providerId, context) => settings.disconnect(providerId, context),\n signIn: (providerId, context) => settings.signIn(providerId, context),\n dismissMoved: (context) => settings.dismissMoved(context),\n };\n}\n\nconst MISMATCHES = new Set(['contract_mismatch', 'upgrade_required']);\n\nconst ended = (link: PortLink): Promise<never> =>\n link.ended.then((reason) => {\n throw reason ?? new Error('the agent closed the link');\n });\n\nexport async function connectAgent(\n endpoint: PortEndpoint,\n options: { context?: Context } = {}\n): Promise<AgentConnection> {\n const context = options.context ?? BACKGROUND_CONTEXT;\n const link = new PortLink(endpoint);\n const peer = greeted(await link.hello);\n if (peer.protocol !== LINK_PROTOCOL) {\n link.close('link protocol mismatch');\n return mismatched(peer.serverId, peer.protocol);\n }\n const host = createHost({ name: `follower:${crypto.randomUUID()}`, kind: 'client' });\n host.connect(link);\n const get = (contract: Parameters<typeof host.get>[0]) =>\n host.get(contract, {}, { at: AGENT_HOST }) as unknown as Remote;\n const handles = [\n AgentControl,\n AgentTranscript,\n AgentSettings,\n AgentRemoteService,\n AgentPagesService,\n ].map(get);\n const [controlHandle, transcriptHandle, settingsHandle, remoteHandle, pagesHandle] = handles as [\n Remote,\n Remote,\n Remote,\n Remote,\n Remote,\n ];\n const release = () => {\n for (const handle of handles) handle.release();\n };\n try {\n await Promise.race([Promise.all(handles.map((handle) => handle.ready)), ended(link)]);\n } catch (error) {\n release();\n link.close('handshake failed');\n if (isChordError(error) && MISMATCHES.has(error.code))\n return mismatched(peer.serverId, peer.protocol);\n throw error;\n }\n const root: Root = (run, rootOptions) => host.root(run, rootOptions);\n const control = bound<AgentControl>(controlHandle, root, CONTROL_METHODS, controlAnswer);\n const members = Object.fromEntries(\n TRANSCRIPT_STATES.map((name) => [name, transcriptHandle[name] as Readable<unknown>])\n ) as Record<(typeof TRANSCRIPT_STATES)[number], Readable<unknown>>;\n const settingsState = settingsHandle.state as Readable<unknown>;\n const keeping = [...Object.values(members), settingsState].map(kept);\n await Promise.race([Promise.all(keeping.map((keep) => keep.ready)), ended(link)]);\n const settings: AgentSettings = {\n ...bound<Omit<AgentSettings, 'state'>>(settingsHandle, root, SETTINGS_METHODS, settingsAnswer),\n state: replicated(settingsState as Readable<SettingsState | undefined>),\n };\n const remote = bound<AgentRemote>(remoteHandle, root, REMOTE_METHODS, remoteAnswer);\n const pages = bound<AgentPages>(pagesHandle, root, PAGE_METHODS, pageAnswer);\n const state = <K extends keyof typeof members>(name: K) =>\n replicated(members[name] as Readable<never>);\n const held: Lasting<unknown>[] = [];\n const hold = <T>(state: ReplicatedState<T>): ReplicatedState<T> => {\n const kept = lasting(state);\n held.push(kept as Lasting<unknown>);\n return kept.state;\n };\n let open = true;\n const finish = () => {\n if (!open) return;\n open = false;\n for (const kept of held) kept.freeze();\n for (const keep of keeping) keep.stop();\n release();\n };\n const closed = link.ended.then((reason) => {\n finish();\n return reason;\n });\n return {\n serverId: peer.serverId,\n control,\n transcript: hold<ConversationView>(state('state')),\n deliveries: hold<Record<string, Delivered>>(state('deliveries')),\n agents: hold<AgentsSummary>(state('agents')),\n views: hold<Record<string, ConversationView>>(state('views')),\n commands: hold<Command[]>(state('commands')),\n privateRevision: hold<PrivateRevision>(state('privateRevision')),\n sprinkles: hold<Sprinkle[]>(state('sprinkles')),\n memories: hold<MemoryEntry[]>(state('memories')),\n memoryScopes: hold<MemoryScope[]>(state('memoryScopes')),\n frozen: hold<FrozenCone[]>(state('frozen')),\n changes: hold<ChangesView>(state('changes')),\n monitor: hold<MonitorFeed>(state('monitor')),\n vault: hold<VaultState>(state('vault')),\n labels: hold<Record<string, string>>(state('labels')),\n settings: lastingSettings(settings, hold),\n remote,\n pages,\n closed,\n webhook(name, delivery) {\n return control.webhook(\n name,\n { id: delivery.id ?? null, headers: delivery.headers ?? {}, body: delivery.body },\n context\n );\n },\n async prompt(text, whenBusy = 'followUp') {\n const sent = await control.send({ text, whenBusy, requestId: null }, context);\n if (!sent.accepted) throw new Error(`${sent.error.code}: ${sent.error.message}`);\n const settled = await control.wait(sent.submissionId, context);\n if (settled.status !== 'done') throw new Error(`unanswered: ${settled.reason}`);\n return settled.text;\n },\n async close() {\n finish();\n link.close('closed');\n await closed;\n await host.close();\n },\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["state", "kept"]
7
7
  }
@@ -76,21 +76,17 @@ async function callNested(tool, args, api, context, id) {
76
76
  output: (chunk) => {
77
77
  output += typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
78
78
  },
79
+ retainedOutput: () => ({ text: output, truncated: false }),
79
80
  diagnostic: () => void 0,
80
81
  details: async () => void 0
81
82
  };
82
83
  const bash = tool.name === "bash";
83
- try {
84
- const result = await tool.execute(valid, nested, context);
85
- const text = result.content ? textOf(result.content) : output;
86
- if (result.isError) throw new Error(text || `Tool "${tool.name}" failed`);
87
- return bash ? { output: text, exit_code: 0 } : text;
88
- } catch (error) {
89
- const message = error instanceof Error ? error.message : String(error);
90
- const exit = /exited with code (\d+)/.exec(message);
91
- if (bash && exit) return { output, exit_code: Number(exit[1]) };
92
- throw new Error(message);
93
- }
84
+ const result = await tool.execute(valid, nested, context);
85
+ const text = result.output ? textOf(result.output) : output;
86
+ const exit = result.structuredOutput?.exitCode;
87
+ if (bash && typeof exit === "number") return { output: text, exit_code: exit };
88
+ if (result.isError) throw new Error(text || `Tool "${tool.name}" failed`);
89
+ return bash ? { output: text, exit_code: 0 } : text;
94
90
  }
95
91
  function valueText(value) {
96
92
  return typeof value === "string" ? value : JSON.stringify(value) ?? String(value);
@@ -229,7 +225,7 @@ Wall time ${seconds} seconds
229
225
  Output:
230
226
  `;
231
227
  return {
232
- content: [{ type: "text", text: header }, ...joinText([...cut.items, ...note])],
228
+ output: [{ type: "text", text: header }, ...joinText([...cut.items, ...note])],
233
229
  ...result.ok ? {} : { isError: true }
234
230
  };
235
231
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/codemode/index.ts"],
4
- "sourcesContent": ["import type { Context, JsonValue } from '@earendil-works/chord';\nimport { withAbortSignal } from '@earendil-works/chord/context';\nimport { Type } from '@earendil-works/pi-ai';\nimport { validateToolArguments } from '@earendil-works/pi-ai/utils/validation';\nimport {\n type CodemodeError,\n type CodemodeJsonSchema,\n type CodemodeOutputItem,\n type CodemodeResult,\n CodemodeSandbox,\n type CodemodeSandboxOptions,\n type CodemodeTool,\n loadQuickJSWasm,\n parseCodemodeSource,\n renderDeclarations,\n} from '@earendil-works/pi-codemode';\nimport {\n type Agent,\n defineDoc,\n defineExtension,\n defineTool,\n type Extension,\n type ToolExecutionApi,\n type ToolRegistration,\n} from '@earendil-works/pi-durable';\nimport { scrubItems } from '../secrets/scrub.ts';\n\nexport const CODEMODE = 'codemode';\nexport const DEFAULT_MAX_OUTPUT_TOKENS = 10_000;\nexport const MEMORY_LIMIT_BYTES = 256 * 1024 * 1024;\nconst CHARS_PER_TOKEN = 4;\n\nexport const CodemodeDoc = defineDoc<{ store: Record<string, JsonValue> }>({\n kind: 'slicc.codemode',\n version: 1,\n scope: 'conversation',\n history: 'rewindable',\n fork: 'asOf',\n initial: () => ({ store: {} }),\n});\n\nexport interface Sandbox {\n execute(\n code: string,\n options: { signal?: AbortSignal; store?: Record<string, unknown> }\n ): Promise<CodemodeResult>;\n close(): Promise<void>;\n}\n\nexport type SandboxFactory = (options: CodemodeSandboxOptions) => Sandbox;\n\nexport interface CodemodeExtra {\n tools: CodemodeTool[];\n globals: CodemodeTool[];\n}\n\nexport interface CodemodeOptions {\n sandbox: SandboxFactory;\n declared: readonly ToolRegistration[];\n extra?: (api: ToolExecutionApi, agent: Agent, context: Context) => Promise<CodemodeExtra>;\n}\n\nconst INTRO = `Run JavaScript that calls other tools. The input is raw JavaScript (not JSON, no code fence), run as an async function body in a QuickJS sandbox: top-level \\`await\\` and \\`return\\` work. No Node, file system, network, or timers.\n- \\`await tools.<name>({ ...args })\\` resolves to a string, or an object if the tool's declaration says so, and rejects with an Error on failure. Calls still running when the script ends are cancelled.\n- Optional first line: \\`// @options: {\"max_output_tokens\": 10000, \"timeout_ms\": 60000}\\``;\n\nconst GLOBALS = [\n 'Globals:',\n '- `text(value)`, `image(dataUrlOrImageBlock)`, `console.log(...)`, and top-level `return` add output; `exit()` ends the script. With several text items, each starts with a `==> text N/M <==` line, and `console` lines follow the other output in one `<console_output>` block.',\n '- `store(key, value)` and `load(key)` keep JSON values across codemode calls in this conversation.',\n '- `ALL_TOOLS` lists every tool scripts can call.',\n].join('\\n');\n\nconst DISCOVERY =\n '- `await searchTools(query, { limit?, namespace? })`, `await describeTool(name)`, `await describeNamespace(name)`: find unlisted tools, such as MCP tools.';\n\nconst BASH_OUTPUT: CodemodeJsonSchema = {\n type: 'object',\n properties: { output: { type: 'string' }, exit_code: { type: 'number' } },\n required: ['output', 'exit_code'],\n};\n\nfunction scriptTool(tool: ToolRegistration): Omit<CodemodeTool, 'execute'> {\n return {\n name: tool.name,\n description: tool.description,\n inputSchema: tool.parameters as CodemodeJsonSchema,\n ...(tool.name === 'bash' ? { outputSchema: BASH_OUTPUT } : {}),\n };\n}\n\nexport function codemodeDescription(\n declared: readonly ToolRegistration[],\n discovery = false\n): string {\n const tools = declared\n .filter((tool) => tool.name !== CODEMODE)\n .map((tool) => ({ ...scriptTool(tool), execute: () => undefined }));\n const sections = [INTRO, discovery ? `${GLOBALS}\\n${DISCOVERY}` : GLOBALS];\n if (tools.length) sections.push(`Nested tools:\\n${renderDeclarations({ tools })}`);\n return sections.join('\\n\\n');\n}\n\nfunction textOf(content: readonly { type: string; text?: string }[] | undefined): string {\n return (content ?? [])\n .filter((part) => part.type === 'text')\n .map((part) => part.text)\n .join('\\n');\n}\n\nexport async function callNested(\n tool: ToolRegistration,\n args: unknown,\n api: ToolExecutionApi,\n context: Context,\n id: string\n): Promise<unknown> {\n const prepared = tool.prepareArguments ? tool.prepareArguments(args) : args;\n const valid = validateToolArguments(tool, {\n type: 'toolCall',\n id,\n name: tool.name,\n arguments: prepared as never,\n });\n const decoder = new TextDecoder();\n let output = '';\n const nested: ToolExecutionApi = {\n ...api,\n callId: id,\n outputWindow: undefined,\n output: (chunk) => {\n output += typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });\n },\n diagnostic: () => undefined,\n details: async () => undefined,\n };\n const bash = tool.name === 'bash';\n try {\n const result = await tool.execute(valid, nested, context);\n const text = result.content ? textOf(result.content) : output;\n if (result.isError) throw new Error(text || `Tool \"${tool.name}\" failed`);\n return bash ? { output: text, exit_code: 0 } : text;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const exit = /exited with code (\\d+)/.exec(message);\n if (bash && exit) return { output, exit_code: Number(exit[1]) };\n throw new Error(message);\n }\n}\n\nfunction valueText(value: unknown): string {\n return typeof value === 'string' ? value : (JSON.stringify(value) ?? String(value));\n}\n\nexport function formatOutput(output: readonly CodemodeOutputItem[]): CodemodeOutputItem[] {\n const total = output.filter((item) => item.type === 'text' && !item.console).length;\n const items: CodemodeOutputItem[] = [];\n const lines: string[] = [];\n let index = 0;\n for (const item of output) {\n if (item.type === 'image') items.push(item);\n else if (item.console) lines.push(item.text);\n else {\n index++;\n items.push({\n type: 'text',\n text: total > 1 ? `==> text ${index}/${total} <==\\n${item.text}` : item.text,\n });\n }\n }\n if (lines.length)\n items.push({ type: 'text', text: `<console_output>\\n${lines.join('\\n')}\\n</console_output>` });\n return items;\n}\n\nexport function joinText(items: readonly CodemodeOutputItem[]): CodemodeOutputItem[] {\n const joined: CodemodeOutputItem[] = [];\n for (const item of items) {\n const last = joined.at(-1);\n if (item.type === 'text' && last?.type === 'text') {\n const separator = last.text === '' || last.text.endsWith('\\n') ? '' : '\\n';\n joined[joined.length - 1] = { type: 'text', text: `${last.text}${separator}${item.text}` };\n } else joined.push(item);\n }\n return joined;\n}\n\nexport function formatError(error: CodemodeError, calls: CodemodeResult['calls']): string {\n const heads: Record<string, string> = {\n timeout: `Script timed out: ${error.message}`,\n aborted: `Script aborted: ${error.message}`,\n sandbox: `Script sandbox failed: ${error.message}`,\n };\n const head =\n error.kind === 'script'\n ? (error.stack ?? `${error.name ?? 'Error'}: ${error.message}`)\n : heads[error.kind];\n const summary = calls.length\n ? `Tool calls made before the failure (they are not undone): ${calls.map((call) => `${call.name} (${call.status})`).join(', ')}`\n : 'No tool calls were made.';\n return `${head}\\n\\n${summary}`;\n}\n\nexport function truncate(\n items: readonly CodemodeOutputItem[],\n maxTokens: number\n): { items: CodemodeOutputItem[]; full: string | null } {\n const text = items\n .filter((item) => item.type === 'text')\n .map((item) => item.text)\n .join('\\n');\n const limit = maxTokens * CHARS_PER_TOKEN;\n if (text.length <= limit) return { items: [...items], full: null };\n const half = Math.floor(limit / 2);\n const kept = `${text.slice(0, half)}\\n\\n[\u2026 ${text.length - 2 * half} characters omitted \u2026]\\n\\n${text.slice(-half)}`;\n return {\n items: [{ type: 'text', text: kept }, ...items.filter((item) => item.type === 'image')],\n full: text,\n };\n}\n\nasync function spill(api: ToolExecutionApi, text: string, context: Context): Promise<string> {\n const path = `/tmp/slicc-codemode-${api.callId.replace(/[^\\w-]/g, '_')}.txt`;\n const written = await api.env?.writeFile(path, text, context);\n return written?.ok ? path : '';\n}\n\nasync function saveStore(\n api: ToolExecutionApi,\n result: CodemodeResult,\n context: Context\n): Promise<void> {\n if (!result.ok) return;\n const { set, delete: removed } = result.storeWrites;\n if (!Object.keys(set).length && !removed.length) return;\n await api.commit(async (tx) => {\n const doc = await tx.doc(CodemodeDoc, api.conversationId);\n for (const key of removed) delete doc.store[key];\n Object.assign(doc.store, set);\n }, context);\n}\n\nexport function codemodeTool(options: CodemodeOptions): ToolRegistration {\n return defineTool({\n name: CODEMODE,\n description: codemodeDescription(options.declared, options.extra !== undefined),\n parameters: Type.Object({ code: Type.String({ description: 'Raw JavaScript source.' }) }),\n replay: 'unsafe',\n async execute({ code }, api, context) {\n const started = performance.now();\n const parsed = parseCodemodeSource(code);\n const agent = await api.agent(context);\n const callable = agent.tools.filter((tool) => tool.name !== CODEMODE);\n let count = 0;\n const tools: CodemodeTool[] = callable.map((tool) => ({\n ...scriptTool(tool),\n execute: (args, { signal }) =>\n callNested(tool, args, api, withAbortSignal(signal, context), `${api.callId}:${++count}`),\n }));\n const extra = options.extra\n ? await options.extra(api, agent, context)\n : { tools: [], globals: [] };\n const replaced = new Set(extra.tools.map((tool) => tool.name));\n const scripted = [...tools.filter((tool) => !replaced.has(tool.name)), ...extra.tools];\n const saved = await api.snapshot(CodemodeDoc, api.conversationId, context);\n const sandbox = options.sandbox({\n tools: scripted,\n ...(extra.globals.length ? { globals: extra.globals } : {}),\n timeoutMs: parsed.options.timeoutMs ?? Number.POSITIVE_INFINITY,\n memoryLimitBytes: MEMORY_LIMIT_BYTES,\n });\n let result: CodemodeResult;\n try {\n result = await sandbox.execute(parsed.code, {\n signal: context.abortSignal as AbortSignal,\n store: { ...saved?.store },\n });\n } finally {\n await sandbox.close();\n }\n await saveStore(api, result, context);\n const output = [...result.output];\n if (result.ok && result.value !== undefined)\n output.push({ type: 'text', text: valueText(result.value) });\n const items = formatOutput(output);\n if (!result.ok)\n items.push({\n type: 'text',\n text: `Script error:\\n${formatError(result.error, result.calls)}`,\n });\n const cut = truncate(\n await scrubItems(joinText(items)),\n parsed.options.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS\n );\n const path = cut.full === null ? '' : await spill(api, cut.full, context);\n const note = path ? [{ type: 'text' as const, text: `Full output: ${path}` }] : [];\n const seconds = ((performance.now() - started) / 1000).toFixed(1);\n const header = `${result.ok ? 'Script completed' : 'Script failed'}\\nWall time ${seconds} seconds\\nOutput:\\n`;\n return {\n content: [{ type: 'text', text: header }, ...joinText([...cut.items, ...note])],\n ...(result.ok ? {} : { isError: true }),\n };\n },\n }) as ToolRegistration;\n}\n\nexport function codemodeExtension(tools: readonly ToolRegistration[]): Extension {\n return defineExtension({ name: 'slicc-codemode', tools });\n}\n\nexport function disabledBySettings(text: string | undefined): boolean {\n if (!text) return false;\n try {\n const settings = JSON.parse(text) as { defaultTools?: unknown };\n return Array.isArray(settings.defaultTools) && settings.defaultTools.includes(`-${CODEMODE}`);\n } catch {\n return false;\n }\n}\n\nexport type Fetcher = (url: string) => Promise<Response>;\n\nexport function wasmCandidates(from: URL): string[] {\n const out: string[] = [];\n let dir = new URL('.', from);\n for (;;) {\n out.push(new URL('node_modules/quickjs-wasi/quickjs.wasm', dir).href);\n if (dir.pathname === '/') return out;\n dir = new URL('..', dir);\n }\n}\n\nexport async function locateWasm(\n fetcher: Fetcher,\n from: URL\n): Promise<WebAssembly.Module | undefined> {\n for (const url of wasmCandidates(from)) {\n const response = await fetcher(url).catch(() => undefined);\n if (response?.ok) return WebAssembly.compile(await response.arrayBuffer());\n }\n return undefined;\n}\n\nexport function sandboxFactory(\n wasm: () => Promise<WebAssembly.Module | undefined>,\n workerUrl?: string | URL\n): SandboxFactory {\n let found: Promise<WebAssembly.Module | undefined> | undefined;\n return (options) => {\n found ??= wasm().catch(() => undefined);\n return new CodemodeSandbox({\n ...options,\n ...(workerUrl ? { workerUrl } : {}),\n wasm: found.then((module) => module ?? loadQuickJSWasm()),\n });\n };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { Context, JsonValue } from '@earendil-works/chord';\nimport { withAbortSignal } from '@earendil-works/chord/context';\nimport { Type } from '@earendil-works/pi-ai';\nimport { validateToolArguments } from '@earendil-works/pi-ai/utils/validation';\nimport {\n type CodemodeError,\n type CodemodeJsonSchema,\n type CodemodeOutputItem,\n type CodemodeResult,\n CodemodeSandbox,\n type CodemodeSandboxOptions,\n type CodemodeTool,\n loadQuickJSWasm,\n parseCodemodeSource,\n renderDeclarations,\n} from '@earendil-works/pi-codemode';\nimport {\n type Agent,\n defineDoc,\n defineExtension,\n defineTool,\n type Extension,\n type ToolExecutionApi,\n type ToolRegistration,\n} from '@earendil-works/pi-durable';\nimport { scrubItems } from '../secrets/scrub.ts';\n\nexport const CODEMODE = 'codemode';\nexport const DEFAULT_MAX_OUTPUT_TOKENS = 10_000;\nexport const MEMORY_LIMIT_BYTES = 256 * 1024 * 1024;\nconst CHARS_PER_TOKEN = 4;\n\nexport const CodemodeDoc = defineDoc<{ store: Record<string, JsonValue> }>({\n kind: 'slicc.codemode',\n version: 1,\n scope: 'conversation',\n history: 'rewindable',\n fork: 'asOf',\n initial: () => ({ store: {} }),\n});\n\nexport interface Sandbox {\n execute(\n code: string,\n options: { signal?: AbortSignal; store?: Record<string, unknown> }\n ): Promise<CodemodeResult>;\n close(): Promise<void>;\n}\n\nexport type SandboxFactory = (options: CodemodeSandboxOptions) => Sandbox;\n\nexport interface CodemodeExtra {\n tools: CodemodeTool[];\n globals: CodemodeTool[];\n}\n\nexport interface CodemodeOptions {\n sandbox: SandboxFactory;\n declared: readonly ToolRegistration[];\n extra?: (api: ToolExecutionApi, agent: Agent, context: Context) => Promise<CodemodeExtra>;\n}\n\nconst INTRO = `Run JavaScript that calls other tools. The input is raw JavaScript (not JSON, no code fence), run as an async function body in a QuickJS sandbox: top-level \\`await\\` and \\`return\\` work. No Node, file system, network, or timers.\n- \\`await tools.<name>({ ...args })\\` resolves to a string, or an object if the tool's declaration says so, and rejects with an Error on failure. Calls still running when the script ends are cancelled.\n- Optional first line: \\`// @options: {\"max_output_tokens\": 10000, \"timeout_ms\": 60000}\\``;\n\nconst GLOBALS = [\n 'Globals:',\n '- `text(value)`, `image(dataUrlOrImageBlock)`, `console.log(...)`, and top-level `return` add output; `exit()` ends the script. With several text items, each starts with a `==> text N/M <==` line, and `console` lines follow the other output in one `<console_output>` block.',\n '- `store(key, value)` and `load(key)` keep JSON values across codemode calls in this conversation.',\n '- `ALL_TOOLS` lists every tool scripts can call.',\n].join('\\n');\n\nconst DISCOVERY =\n '- `await searchTools(query, { limit?, namespace? })`, `await describeTool(name)`, `await describeNamespace(name)`: find unlisted tools, such as MCP tools.';\n\nconst BASH_OUTPUT: CodemodeJsonSchema = {\n type: 'object',\n properties: { output: { type: 'string' }, exit_code: { type: 'number' } },\n required: ['output', 'exit_code'],\n};\n\nfunction scriptTool(tool: ToolRegistration): Omit<CodemodeTool, 'execute'> {\n return {\n name: tool.name,\n description: tool.description,\n inputSchema: tool.parameters as CodemodeJsonSchema,\n ...(tool.name === 'bash' ? { outputSchema: BASH_OUTPUT } : {}),\n };\n}\n\nexport function codemodeDescription(\n declared: readonly ToolRegistration[],\n discovery = false\n): string {\n const tools = declared\n .filter((tool) => tool.name !== CODEMODE)\n .map((tool) => ({ ...scriptTool(tool), execute: () => undefined }));\n const sections = [INTRO, discovery ? `${GLOBALS}\\n${DISCOVERY}` : GLOBALS];\n if (tools.length) sections.push(`Nested tools:\\n${renderDeclarations({ tools })}`);\n return sections.join('\\n\\n');\n}\n\nfunction textOf(content: readonly { type: string; text?: string }[] | undefined): string {\n return (content ?? [])\n .filter((part) => part.type === 'text')\n .map((part) => part.text)\n .join('\\n');\n}\n\nexport async function callNested(\n tool: ToolRegistration,\n args: unknown,\n api: ToolExecutionApi,\n context: Context,\n id: string\n): Promise<unknown> {\n const prepared = tool.prepareArguments ? tool.prepareArguments(args) : args;\n const valid = validateToolArguments(tool, {\n type: 'toolCall',\n id,\n name: tool.name,\n arguments: prepared as never,\n });\n const decoder = new TextDecoder();\n let output = '';\n const nested: ToolExecutionApi = {\n ...api,\n callId: id,\n outputWindow: undefined,\n output: (chunk) => {\n output += typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });\n },\n retainedOutput: () => ({ text: output, truncated: false }),\n diagnostic: () => undefined,\n details: async () => undefined,\n };\n const bash = tool.name === 'bash';\n const result = await tool.execute(valid, nested, context);\n const text = result.output ? textOf(result.output) : output;\n const exit = (result.structuredOutput as { exitCode?: unknown } | undefined)?.exitCode;\n if (bash && typeof exit === 'number') return { output: text, exit_code: exit };\n if (result.isError) throw new Error(text || `Tool \"${tool.name}\" failed`);\n return bash ? { output: text, exit_code: 0 } : text;\n}\n\nfunction valueText(value: unknown): string {\n return typeof value === 'string' ? value : (JSON.stringify(value) ?? String(value));\n}\n\nexport function formatOutput(output: readonly CodemodeOutputItem[]): CodemodeOutputItem[] {\n const total = output.filter((item) => item.type === 'text' && !item.console).length;\n const items: CodemodeOutputItem[] = [];\n const lines: string[] = [];\n let index = 0;\n for (const item of output) {\n if (item.type === 'image') items.push(item);\n else if (item.console) lines.push(item.text);\n else {\n index++;\n items.push({\n type: 'text',\n text: total > 1 ? `==> text ${index}/${total} <==\\n${item.text}` : item.text,\n });\n }\n }\n if (lines.length)\n items.push({ type: 'text', text: `<console_output>\\n${lines.join('\\n')}\\n</console_output>` });\n return items;\n}\n\nexport function joinText(items: readonly CodemodeOutputItem[]): CodemodeOutputItem[] {\n const joined: CodemodeOutputItem[] = [];\n for (const item of items) {\n const last = joined.at(-1);\n if (item.type === 'text' && last?.type === 'text') {\n const separator = last.text === '' || last.text.endsWith('\\n') ? '' : '\\n';\n joined[joined.length - 1] = { type: 'text', text: `${last.text}${separator}${item.text}` };\n } else joined.push(item);\n }\n return joined;\n}\n\nexport function formatError(error: CodemodeError, calls: CodemodeResult['calls']): string {\n const heads: Record<string, string> = {\n timeout: `Script timed out: ${error.message}`,\n aborted: `Script aborted: ${error.message}`,\n sandbox: `Script sandbox failed: ${error.message}`,\n };\n const head =\n error.kind === 'script'\n ? (error.stack ?? `${error.name ?? 'Error'}: ${error.message}`)\n : heads[error.kind];\n const summary = calls.length\n ? `Tool calls made before the failure (they are not undone): ${calls.map((call) => `${call.name} (${call.status})`).join(', ')}`\n : 'No tool calls were made.';\n return `${head}\\n\\n${summary}`;\n}\n\nexport function truncate(\n items: readonly CodemodeOutputItem[],\n maxTokens: number\n): { items: CodemodeOutputItem[]; full: string | null } {\n const text = items\n .filter((item) => item.type === 'text')\n .map((item) => item.text)\n .join('\\n');\n const limit = maxTokens * CHARS_PER_TOKEN;\n if (text.length <= limit) return { items: [...items], full: null };\n const half = Math.floor(limit / 2);\n const kept = `${text.slice(0, half)}\\n\\n[\u2026 ${text.length - 2 * half} characters omitted \u2026]\\n\\n${text.slice(-half)}`;\n return {\n items: [{ type: 'text', text: kept }, ...items.filter((item) => item.type === 'image')],\n full: text,\n };\n}\n\nasync function spill(api: ToolExecutionApi, text: string, context: Context): Promise<string> {\n const path = `/tmp/slicc-codemode-${api.callId.replace(/[^\\w-]/g, '_')}.txt`;\n const written = await api.env?.writeFile(path, text, context);\n return written?.ok ? path : '';\n}\n\nasync function saveStore(\n api: ToolExecutionApi,\n result: CodemodeResult,\n context: Context\n): Promise<void> {\n if (!result.ok) return;\n const { set, delete: removed } = result.storeWrites;\n if (!Object.keys(set).length && !removed.length) return;\n await api.commit(async (tx) => {\n const doc = await tx.doc(CodemodeDoc, api.conversationId);\n for (const key of removed) delete doc.store[key];\n Object.assign(doc.store, set);\n }, context);\n}\n\nexport function codemodeTool(options: CodemodeOptions): ToolRegistration {\n return defineTool({\n name: CODEMODE,\n description: codemodeDescription(options.declared, options.extra !== undefined),\n parameters: Type.Object({ code: Type.String({ description: 'Raw JavaScript source.' }) }),\n replay: 'unsafe',\n async execute({ code }, api, context) {\n const started = performance.now();\n const parsed = parseCodemodeSource(code);\n const agent = await api.agent(context);\n const callable = agent.tools.filter((tool) => tool.name !== CODEMODE);\n let count = 0;\n const tools: CodemodeTool[] = callable.map((tool) => ({\n ...scriptTool(tool),\n execute: (args, { signal }) =>\n callNested(tool, args, api, withAbortSignal(signal, context), `${api.callId}:${++count}`),\n }));\n const extra = options.extra\n ? await options.extra(api, agent, context)\n : { tools: [], globals: [] };\n const replaced = new Set(extra.tools.map((tool) => tool.name));\n const scripted = [...tools.filter((tool) => !replaced.has(tool.name)), ...extra.tools];\n const saved = await api.snapshot(CodemodeDoc, api.conversationId, context);\n const sandbox = options.sandbox({\n tools: scripted,\n ...(extra.globals.length ? { globals: extra.globals } : {}),\n timeoutMs: parsed.options.timeoutMs ?? Number.POSITIVE_INFINITY,\n memoryLimitBytes: MEMORY_LIMIT_BYTES,\n });\n let result: CodemodeResult;\n try {\n result = await sandbox.execute(parsed.code, {\n signal: context.abortSignal as AbortSignal,\n store: { ...saved?.store },\n });\n } finally {\n await sandbox.close();\n }\n await saveStore(api, result, context);\n const output = [...result.output];\n if (result.ok && result.value !== undefined)\n output.push({ type: 'text', text: valueText(result.value) });\n const items = formatOutput(output);\n if (!result.ok)\n items.push({\n type: 'text',\n text: `Script error:\\n${formatError(result.error, result.calls)}`,\n });\n const cut = truncate(\n await scrubItems(joinText(items)),\n parsed.options.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS\n );\n const path = cut.full === null ? '' : await spill(api, cut.full, context);\n const note = path ? [{ type: 'text' as const, text: `Full output: ${path}` }] : [];\n const seconds = ((performance.now() - started) / 1000).toFixed(1);\n const header = `${result.ok ? 'Script completed' : 'Script failed'}\\nWall time ${seconds} seconds\\nOutput:\\n`;\n return {\n output: [{ type: 'text', text: header }, ...joinText([...cut.items, ...note])],\n ...(result.ok ? {} : { isError: true }),\n };\n },\n }) as ToolRegistration;\n}\n\nexport function codemodeExtension(tools: readonly ToolRegistration[]): Extension {\n return defineExtension({ name: 'slicc-codemode', tools });\n}\n\nexport function disabledBySettings(text: string | undefined): boolean {\n if (!text) return false;\n try {\n const settings = JSON.parse(text) as { defaultTools?: unknown };\n return Array.isArray(settings.defaultTools) && settings.defaultTools.includes(`-${CODEMODE}`);\n } catch {\n return false;\n }\n}\n\nexport type Fetcher = (url: string) => Promise<Response>;\n\nexport function wasmCandidates(from: URL): string[] {\n const out: string[] = [];\n let dir = new URL('.', from);\n for (;;) {\n out.push(new URL('node_modules/quickjs-wasi/quickjs.wasm', dir).href);\n if (dir.pathname === '/') return out;\n dir = new URL('..', dir);\n }\n}\n\nexport async function locateWasm(\n fetcher: Fetcher,\n from: URL\n): Promise<WebAssembly.Module | undefined> {\n for (const url of wasmCandidates(from)) {\n const response = await fetcher(url).catch(() => undefined);\n if (response?.ok) return WebAssembly.compile(await response.arrayBuffer());\n }\n return undefined;\n}\n\nexport function sandboxFactory(\n wasm: () => Promise<WebAssembly.Module | undefined>,\n workerUrl?: string | URL\n): SandboxFactory {\n let found: Promise<WebAssembly.Module | undefined> | undefined;\n return (options) => {\n found ??= wasm().catch(() => undefined);\n return new CodemodeSandbox({\n ...options,\n ...(workerUrl ? { workerUrl } : {}),\n wasm: found.then((module) => module ?? loadQuickJSWasm()),\n });\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }