@yolk-sdk/codemode 0.1.0-canary.102 → 0.1.0-canary.103

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.
package/README.md CHANGED
@@ -15,10 +15,10 @@ Requires Node.js 22.19+ (the pi engine's minimum). `@yolk-sdk/codemode/node` is
15
15
 
16
16
  ## Subpaths
17
17
 
18
- | Subpath | Purpose |
19
- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
20
- | `@yolk-sdk/codemode` | Runtime-neutral: `makeCodeModeTool`, the `CodeModeExecutor` interface, catalog/discovery, store rebuild, classifier tool |
21
- | `@yolk-sdk/codemode/node` | `makePiCodeModeExecutor`: QuickJS (WebAssembly) in a worker thread per script, on `@earendil-works/pi-codemode` 1.0.0 |
18
+ | Subpath | Purpose |
19
+ | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
20
+ | `@yolk-sdk/codemode` | Runtime-neutral: `makeCodeModeTool` (incl. `stage` for staged plans), the `CodeModeExecutor` interface, catalog/discovery, store rebuild, classifier tool |
21
+ | `@yolk-sdk/codemode/node` | `makePiCodeModeExecutor`: QuickJS (WebAssembly) in a worker thread per script, on `@earendil-works/pi-codemode` 1.0.0 |
22
22
 
23
23
  ## Example
24
24
 
@@ -127,6 +127,23 @@ Hosts that re-execute steps (Vercel Workflow's queue is at-least-once) pass a du
127
127
  Nested write calls are ledgered under `<toolCallId>/<seq>` and receive a stable `idempotencyKey`.
128
128
  Calls the ledger policy skips (by default read-only calls) are not listed.
129
129
 
130
+ ## Staged writes
131
+
132
+ When the resolution has plans (`resolveTools(..., { interactionHost, plans: { store } })`) and a
133
+ plan review tool (`makePlanReviewTool` from `@yolk-sdk/agent/tools`), scripts get
134
+ `await stage(name, args)` for approval-gated tools marked `staging`. `stage` validates the arguments
135
+ and records the call without running or ledgering it. Once a script stages, it may only run read
136
+ tools, and `stage` is rejected after a direct write; duplicates and calls past
137
+ `maxCalls`/`maxArgsBytes` are rejected. A successful script saves
138
+ the plan once and its result tells the model to call `review_plan({ planId, planDigest })`
139
+ (`structuredContent.codemode.plan`); a failed script discards its staged calls. Approval-gated tools
140
+ stay fail-closed for `tools.<name>()`. Pass `staging: false` to turn it off or `staging: { maxCalls }`
141
+ to lower the cap.
142
+
143
+ Applied plan calls run inside the review action: they bypass `beforeNestedCall` and `ToolExecutor`
144
+ decorators. Wire the same per-call check as `makePlanReviewTool({ beforeCall })`. See
145
+ [ADR 0005](https://github.com/magoz/yolk-sdk/blob/main/docs/adr/0005-staged-tool-plans.md).
146
+
130
147
  ## Limits
131
148
 
132
149
  | Limit | Default | Notes |
@@ -156,7 +173,8 @@ permits. Nested-call records carry token usage; cost stays in the result's
156
173
  `ResolvedToolSet.execute`) do not see nested calls. Use `beforeNestedCall` or registration-level
157
174
  wrappers for per-call checks.
158
175
  Approval, input, interaction, background, `question`, and `subagent` tools never run from code
159
- mode.
176
+ mode (stageable approval tools are only staged, and run after review through the plan review
177
+ tool, whose `beforeCall` is their per-call check).
160
178
  - Where the tool is advertised (voice sessions do not get it in v1), deadlines, and limits.
161
179
  - Next.js: `serverExternalPackages: ['@yolk-sdk/codemode', '@earendil-works/pi-codemode', 'quickjs-wasi']`.
162
180
  - Vercel Workflow: run the code mode call inside the tool-batch step (`'use step'`), never inside a
@@ -52,17 +52,24 @@ type CodeModeListing = {
52
52
  * a namespace whose next tool does not fit drops out. `search` tools are never candidates.
53
53
  */
54
54
  declare const selectCodeModeListing: (catalog: ReadonlyArray<CodeModeCatalogTool>, budget: number) => CodeModeListing;
55
+ /** Staged tool plans as the description shows them (`NestedToolStagingDescription`). */
56
+ type CodeModeStagingDescription = {
57
+ readonly tools: ReadonlyArray<NestedTool>;
58
+ readonly reviewToolName: string;
59
+ };
55
60
  type CodeModeDescriptionInput = {
56
61
  readonly tools: ReadonlyArray<NestedTool>;
57
62
  readonly inlineBudget?: number; /** Mention `store()`/`load()` persistence. */
58
- readonly store?: boolean;
63
+ readonly store?: boolean; /** Document `stage()` and list the stageable tools (only when some exist). */
64
+ readonly staging?: CodeModeStagingDescription;
59
65
  };
60
66
  /**
61
67
  * The code mode tool description: intro, globals one per line, then nested tools grouped by
62
68
  * namespace, then one fixed line pointing to `searchTools`/`describeTool`/`describeNamespace`.
63
69
  * `all` tools get one line each outside the budget; `listed` tools are declared within the inline
64
70
  * budget; `search` tools never contribute, so adding or removing them (even whole namespaces of
65
- * them) leaves the text byte-identical.
71
+ * them) leaves the text byte-identical. With `staging` (and stageable tools), `stage()` is listed
72
+ * with the globals and the stageable tool names follow the nested tools.
66
73
  */
67
74
  declare const renderCodeModeDescription: (input: CodeModeDescriptionInput) => string;
68
75
  /** `describeTool` text: the description and TypeScript declaration of one tool. */
@@ -70,5 +77,5 @@ declare const describeCodeModeTool: (tool: CodeModeCatalogTool) => string;
70
77
  /** Finds a tool by identifier or raw name. */
71
78
  declare const findCodeModeTool: (catalog: ReadonlyArray<CodeModeCatalogTool>, name: string) => CodeModeCatalogTool | undefined;
72
79
  //#endregion
73
- export { CodeModeCatalogTool, CodeModeDescriptionInput, CodeModeListing, CodeModeToolExposure, codeModeCallLabels, codeModeCatalog, defaultCodeModeInlineBudget, describeCodeModeTool, estimateCodeModeTokens, findCodeModeTool, renderCodeModeDescription, selectCodeModeListing };
80
+ export { CodeModeCatalogTool, CodeModeDescriptionInput, CodeModeListing, CodeModeStagingDescription, CodeModeToolExposure, codeModeCallLabels, codeModeCatalog, defaultCodeModeInlineBudget, describeCodeModeTool, estimateCodeModeTokens, findCodeModeTool, renderCodeModeDescription, selectCodeModeListing };
74
81
  //# sourceMappingURL=catalog.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"catalog.d.mts","names":[],"sources":["../src/catalog.ts"],"mappings":";;;;;;AAcA;KAAY,oBAAA;;KAGA,mBAAA;EAHoB,wEAKrB,IAAA,UAFoB;EAAA,SAIpB,UAAA;EAYa;;;;;EAAA,SANb,SAAA,UANA;EAAA,SAQA,SAAA;EAAA,SAEA,oBAAA;EAAA,SACA,WAAA;EAAA,SACA,WAAA,EAAa,kBAAA,EAAA;EAAA,SAEb,YAAA,EAAc,kBAAA,EAAA;EAAA,SAEd,UAAA;EAAA,SACA,QAAA,EAAU,oBAAA;AAAA;;AAAoB;AAYzC;;;;;cAAa,kBAAA,GAAsB,KAAA,EAAO,aAAA,aAAwB,aAAa;;cAelE,eAAA,GACX,KAAA,EAAO,aAAA,CAAc,UAAA,MACpB,aAAA,CAAc,mBAAA;AAjB8D;AAAA,cAwClE,2BAAA;;cAGA,sBAAA,GAA0B,IAAY;AAAA,KA4DvC,eAAA;EAvFH;;;EAAA,SA2FE,MAAA,EAAQ,aAAA,CAAc,mBAAA,GA1FjB;EAAA,SA4FL,kBAAA,EAAoB,aAAA;AAAA;;;;;AA5FK;AAuBpC;cA8Ea,qBAAA,GACX,OAAA,EAAS,aAAA,CAAc,mBAAA,GACvB,MAAA,aACC,eAAA;AAAA,KAuFS,wBAAA;EAAA,SACD,KAAA,EAAO,aAAa,CAAC,UAAA;EAAA,SACrB,YAAA,WAvKE;EAAA,SAyKF,KAAA;AAAA;;AAzKwC;AA4DnD;;;;;cAuHa,yBAAA,GAA6B,KAA+B,EAAxB,wBAAwB;;cAwB5D,oBAAA,GAAwB,IAAyB,EAAnB,mBAAmB;;cAIjD,gBAAA,GACX,OAAA,EAAS,aAAA,CAAc,mBAAA,GACvB,IAAA,aACC,mBAAA"}
1
+ {"version":3,"file":"catalog.d.mts","names":[],"sources":["../src/catalog.ts"],"mappings":";;;;;;AAcA;KAAY,oBAAA;;KAGA,mBAAA;EAHoB,wEAKrB,IAAA,UAFoB;EAAA,SAIpB,UAAA;EAYa;;;;;EAAA,SANb,SAAA,UANA;EAAA,SAQA,SAAA;EAAA,SAEA,oBAAA;EAAA,SACA,WAAA;EAAA,SACA,WAAA,EAAa,kBAAA,EAAA;EAAA,SAEb,YAAA,EAAc,kBAAA,EAAA;EAAA,SAEd,UAAA;EAAA,SACA,QAAA,EAAU,oBAAA;AAAA;;AAAoB;AAYzC;;;;;cAAa,kBAAA,GAAsB,KAAA,EAAO,aAAA,aAAwB,aAAa;;cAelE,eAAA,GACX,KAAA,EAAO,aAAA,CAAc,UAAA,MACpB,aAAA,CAAc,mBAAA;AAjB8D;AAAA,cAwClE,2BAAA;;cAGA,sBAAA,GAA0B,IAAY;AAAA,KA4DvC,eAAA;EAvFH;;;EAAA,SA2FE,MAAA,EAAQ,aAAA,CAAc,mBAAA,GA1FjB;EAAA,SA4FL,kBAAA,EAAoB,aAAA;AAAA;;;;;AA5FK;AAuBpC;cA8Ea,qBAAA,GACX,OAAA,EAAS,aAAA,CAAc,mBAAA,GACvB,MAAA,aACC,eAAA;;KA4GS,0BAAA;EAAA,SACD,KAAA,EAAO,aAAa,CAAC,UAAA;EAAA,SACrB,cAAA;AAAA;AAAA,KAGC,wBAAA;EAAA,SACD,KAAA,EAAO,aAAA,CAAc,UAAA;EAAA,SACrB,YAAA,WArIC;EAAA,SAuID,KAAA;WAEA,OAAA,GAAU,0BAAA;AAAA;;;;;;;;;cAWR,yBAAA,GAA6B,KAA+B,EAAxB,wBAAwB;AA9I7B;AAAA,cA8K/B,oBAAA,GAAwB,IAAyB,EAAnB,mBAAmB;;cAIjD,gBAAA,GACX,OAAA,EAAS,aAAA,CAAc,mBAAA,GACvB,IAAA,aACC,mBAAA"}
package/dist/catalog.mjs CHANGED
@@ -112,6 +112,19 @@ const intro = [
112
112
  "Call tools as `await tools.<id>(args)`. Arguments must be plain JSON (no `NaN`, `undefined` array items, `Map`, or functions). A call resolves to the declared result type and rejects with an Error whose message starts with `tools.<id>:` when the tool fails. Calls still running when the script ends are cancelled, so await every call (use `Promise.all` for parallel calls).",
113
113
  "Only the script output and its return value come back to you: filter and aggregate inside the script and return a small JSON value."
114
114
  ].join("\n");
115
+ const stageLine = (reviewToolName) => `- \`await stage(name, args)\`: record a call of a stageable tool for review instead of running it; resolves to \`{ staged: true, key, index }\`. Nothing runs until a person approves the plan through the \`${reviewToolName}\` tool, which the result tells you how to call. Once a script stages, it may only run read tools; a script that already ran a write tool cannot stage.`;
116
+ /** Stageable tool names listed in the description; the rest are summarized with a count. */
117
+ const maxListedStageableTools = 20;
118
+ const stageableSection = (staging) => {
119
+ const names = staging.tools.map((tool) => tool.def.name);
120
+ const hidden = names.length - maxListedStageableTools;
121
+ return [
122
+ "## Stageable tools",
123
+ "Approval-gated tools a script cannot call through `tools`, only stage with `await stage(\"<name>\", args)` (arguments as for the tool of the same name):",
124
+ ...names.slice(0, maxListedStageableTools).map((name) => `- \`${name}\``),
125
+ ...hidden > 0 ? [`- … and ${hidden} more (an unknown name rejects with the stageable tools).`] : []
126
+ ].join("\n");
127
+ };
115
128
  const globalLines = (store) => [
116
129
  "- `text(value)` and `console.log(...values)`: append text to the output.",
117
130
  "- `image(dataUrl)`: append a base64 image (a `data:` URL or `{ type: \"image\", data, mimeType }`).",
@@ -143,14 +156,21 @@ const unlistedToolsLine = "More tools may be callable than are listed here: find
143
156
  * namespace, then one fixed line pointing to `searchTools`/`describeTool`/`describeNamespace`.
144
157
  * `all` tools get one line each outside the budget; `listed` tools are declared within the inline
145
158
  * budget; `search` tools never contribute, so adding or removing them (even whole namespaces of
146
- * them) leaves the text byte-identical.
159
+ * them) leaves the text byte-identical. With `staging` (and stageable tools), `stage()` is listed
160
+ * with the globals and the stageable tool names follow the nested tools.
147
161
  */
148
162
  const renderCodeModeDescription = (input) => {
149
163
  const sections = [...groupByNamespace(selectCodeModeListing(codeModeCatalog(input.tools), input.inlineBudget ?? 3e3).listed.map((tool) => ({ tool })))].map(([namespace, items]) => namespaceSection(namespace, items.map((item) => item.tool)));
164
+ const staging = input.staging !== void 0 && input.staging.tools.length > 0 ? input.staging : void 0;
150
165
  return [
151
166
  intro,
152
- ["Globals:", ...globalLines(input.store === true)].join("\n"),
167
+ [
168
+ "Globals:",
169
+ ...globalLines(input.store === true),
170
+ ...staging === void 0 ? [] : [stageLine(staging.reviewToolName)]
171
+ ].join("\n"),
153
172
  sections.length === 0 ? "Nested tools: none are listed here." : ["## Nested tools by namespace", ...sections].join("\n\n"),
173
+ ...staging === void 0 ? [] : [stageableSection(staging)],
154
174
  unlistedToolsLine
155
175
  ].join("\n\n");
156
176
  };
@@ -1 +1 @@
1
- {"version":3,"file":"catalog.mjs","names":[],"sources":["../src/catalog.ts"],"sourcesContent":["import { Predicate } from 'effect'\nimport {\n renderDeclarations,\n renderToolOutputType,\n renderToolSample,\n toCodemodeIdentifier\n} from '@earendil-works/pi-codemode/declarations'\nimport { toolDiscovery } from '@yolk-sdk/agent/protocol'\nimport type { NestedTool } from '@yolk-sdk/agent/tools'\nimport type { CodeModeJsonSchema } from './executor.ts'\n\n/** How a nested tool reaches scripts: `all` tools are also direct model tools; `listed` and\n * `search` are the two discovery modes of `codemode`-only tools.\n */\nexport type CodeModeToolExposure = 'all' | 'listed' | 'search'\n\n/** One nested tool as scripts see it. */\nexport type CodeModeCatalogTool = {\n /** Raw tool name; scripts may call `tools[\"<name>\"](args)`. */\n readonly name: string\n /** Identifier for `tools.<identifier>(args)` (invalid identifier characters become `_`). */\n readonly identifier: string\n /**\n * How scripts call this tool, used to prefix its rejections: `tools.<identifier>`, or\n * `tools[\"<name>\"]` when an earlier tool already has the identifier (see `codeModeCallLabels`;\n * when the name is taken too, the tool is unreachable and the label only names it).\n */\n readonly callLabel: string\n /** The tool's `ToolModule.id`. */\n readonly namespace: string\n /** The module's `ToolModule.description`, when set. */\n readonly namespaceDescription?: string\n readonly description: string\n readonly inputSchema: CodeModeJsonSchema\n /** The declared output schema, or `{ type: 'string' }` for text results. */\n readonly outputSchema: CodeModeJsonSchema\n /** True when the tool declares an output schema (calls resolve to `structuredContent`). */\n readonly structured: boolean\n readonly exposure: CodeModeToolExposure\n}\n\nconst textOutputSchema: CodeModeJsonSchema = { type: 'string' }\n\n/**\n * The call label of each tool name, in order, as pi binds `tools`: each tool takes\n * `tools[<identifier>]` unless an earlier tool holds that key (first tool wins), then\n * `tools[<name>]` if still free. The label is `tools.<identifier>` for the tool that holds its\n * identifier, otherwise `tools[\"<name>\"]`. The executor guard and the host rejection prefix share\n * it, so both name a call the same way.\n */\nexport const codeModeCallLabels = (names: ReadonlyArray<string>): ReadonlyArray<string> => {\n const bound = new Set<string>()\n\n return names.map(name => {\n const identifier = toCodemodeIdentifier(name)\n const ownsIdentifier = !bound.has(identifier)\n\n bound.add(identifier)\n bound.add(name)\n\n return ownsIdentifier ? `tools.${identifier}` : `tools[${JSON.stringify(name)}]`\n })\n}\n\n/** Catalog of nested tools in resolution order. */\nexport const codeModeCatalog = (\n tools: ReadonlyArray<NestedTool>\n): ReadonlyArray<CodeModeCatalogTool> => {\n const labels = codeModeCallLabels(tools.map(({ def }) => def.name))\n\n return tools.map(({ def, moduleId, moduleDescription }, index) => {\n const tool: CodeModeCatalogTool = {\n name: def.name,\n identifier: toCodemodeIdentifier(def.name),\n callLabel: labels[index] ?? `tools[${JSON.stringify(def.name)}]`,\n namespace: moduleId,\n description: def.description,\n inputSchema: def.parameters,\n outputSchema: def.outputSchema ?? textOutputSchema,\n structured: def.outputSchema !== undefined,\n exposure: toolDiscovery(def) ?? 'all'\n }\n\n return moduleDescription === undefined\n ? tool\n : { ...tool, namespaceDescription: moduleDescription }\n })\n}\n\n/** Default inline budget of the nested tool listing, in estimated tokens. */\nexport const defaultCodeModeInlineBudget = 3000\n\n/** Estimated tokens of a text: four characters per token, rounded up. */\nexport const estimateCodeModeTokens = (text: string): number => Math.ceil(text.length / 4)\n\nconst maxShortTypeChars = 60\n\nconst shortType = (schema: CodeModeJsonSchema): string => {\n const rendered = renderToolOutputType(schema)\n\n if (rendered.length <= maxShortTypeChars) return rendered\n\n if (!Predicate.isBoolean(schema)) {\n if (schema.type === 'array') return 'Array<unknown>'\n\n if (schema.type === 'object') return 'object'\n }\n\n return 'unknown'\n}\n\n// Declarations only read names, descriptions, and schemas; `execute` is never called.\nconst declarationTool = (tool: CodeModeCatalogTool) => ({\n name: tool.name,\n description: tool.description,\n inputSchema: tool.inputSchema,\n outputSchema: tool.outputSchema,\n execute: () => undefined\n})\n\nconst directToolLine = (tool: CodeModeCatalogTool) =>\n `- \\`tools.${tool.identifier}(args)\\` takes the arguments of the \\`${tool.name}\\` tool and resolves to \\`${shortType(tool.outputSchema)}\\`.`\n\nconst declarationMembers = (tools: ReadonlyArray<CodeModeCatalogTool>) =>\n renderDeclarations({ tools: tools.map(declarationTool) })\n\ntype Candidate = {\n readonly tool: CodeModeCatalogTool\n readonly index: number\n readonly cost: number\n}\n\nconst candidateCost = (tool: CodeModeCatalogTool) =>\n estimateCodeModeTokens(declarationMembers([tool]))\n\nconst groupByNamespace = <A extends { readonly tool: CodeModeCatalogTool }>(\n items: ReadonlyArray<A>\n): ReadonlyMap<string, ReadonlyArray<A>> => {\n const groups = new Map<string, Array<A>>()\n\n for (const item of items) {\n const group = groups.get(item.tool.namespace)\n\n if (group === undefined) {\n groups.set(item.tool.namespace, [item])\n } else {\n group.push(item)\n }\n }\n\n return groups\n}\n\nexport type CodeModeListing = {\n /** Tools placed in the description, in catalog order: every `all` tool, and the `listed` tools\n * that fit the budget.\n */\n readonly listed: ReadonlyArray<CodeModeCatalogTool>\n /** Namespaces (catalog order) with callable tools the description does not list. */\n readonly unlistedNamespaces: ReadonlyArray<string>\n}\n\n/**\n * Chooses the tools the description lists. `all` tools are always placed, outside the budget\n * (the model already has their declarations). `listed` tools fill `budget` estimated tokens fairly\n * across namespaces: each round, every namespace still in play places its cheapest remaining tool;\n * a namespace whose next tool does not fit drops out. `search` tools are never candidates.\n */\nexport const selectCodeModeListing = (\n catalog: ReadonlyArray<CodeModeCatalogTool>,\n budget: number\n): CodeModeListing => {\n const candidates = catalog.flatMap((tool, index): ReadonlyArray<Candidate> =>\n tool.exposure === 'listed' ? [{ tool, index, cost: candidateCost(tool) }] : []\n )\n\n const queues = new Map(\n [...groupByNamespace(candidates)].map(([namespace, group]) => [\n namespace,\n [...group].sort((left, right) => left.cost - right.cost || left.index - right.index)\n ])\n )\n\n const placed = new Set(catalog.flatMap((tool, index) => (tool.exposure === 'all' ? [index] : [])))\n\n const inPlay = new Set(queues.keys())\n let remaining = budget\n\n while (inPlay.size > 0) {\n for (const [namespace, queue] of queues) {\n if (!inPlay.has(namespace)) continue\n\n const next = queue.shift()\n\n if (next === undefined || next.cost > remaining) {\n inPlay.delete(namespace)\n continue\n }\n\n placed.add(next.index)\n remaining -= next.cost\n }\n }\n\n const unlisted = new Set(\n catalog.flatMap((tool, index) => (placed.has(index) ? [] : [tool.namespace]))\n )\n\n return {\n listed: catalog.filter((_, index) => placed.has(index)),\n unlistedNamespaces: [...new Set(catalog.map(tool => tool.namespace))].filter(namespace =>\n unlisted.has(namespace)\n )\n }\n}\n\nconst intro = [\n 'Run a JavaScript script that calls tools and returns only what matters.',\n '`code` is the body of an async function: top-level `await` and `return` work. Write plain JavaScript; simple TypeScript annotations are stripped. There is no Node.js, network, filesystem, `require`, `fetch`, or timers.',\n 'Call tools as `await tools.<id>(args)`. Arguments must be plain JSON (no `NaN`, `undefined` array items, `Map`, or functions). A call resolves to the declared result type and rejects with an Error whose message starts with `tools.<id>:` when the tool fails. Calls still running when the script ends are cancelled, so await every call (use `Promise.all` for parallel calls).',\n 'Only the script output and its return value come back to you: filter and aggregate inside the script and return a small JSON value.'\n].join('\\n')\n\nconst globalLines = (store: boolean) => [\n '- `text(value)` and `console.log(...values)`: append text to the output.',\n '- `image(dataUrl)`: append a base64 image (a `data:` URL or `{ type: \"image\", data, mimeType }`).',\n '- `exit()`: end the script successfully.',\n '- `ALL_TOOLS`: `{ name, description }` of every callable tool.',\n '- `await searchTools(query, { limit?, namespace? })`: find tools by topic; resolves to `Array<{ name: string; description: string }>` (default limit 8).',\n '- `await describeTool(name)`: the description and TypeScript declaration of a tool, or `undefined`.',\n '- `await describeNamespace(name)`: `{ name, description?, tools: Array<{ name, description }> }` for a namespace, or `undefined`.',\n ...(store\n ? [\n '- `store(key, value)` and `load(key)`: keep small JSON values for later scripts; writes are saved only when the script succeeds.'\n ]\n : [])\n]\n\nconst namespaceSection = (namespace: string, tools: ReadonlyArray<CodeModeCatalogTool>) => {\n const declared = tools.filter(tool => tool.exposure !== 'all')\n const direct = tools.filter(tool => tool.exposure === 'all')\n\n const description = tools.find(\n tool => tool.namespaceDescription !== undefined\n )?.namespaceDescription\n\n return [\n `### ${namespace}`,\n ...(description === undefined ? [] : [description]),\n ...(declared.length > 0 ? ['```ts', declarationMembers(declared), '```'] : []),\n ...direct.map(directToolLine)\n ].join('\\n')\n}\n\n// Fixed text: never derived from `search` tools or from which tools did not fit the budget.\nconst unlistedToolsLine =\n 'More tools may be callable than are listed here: find them with `await searchTools(query, { namespace? })` and read one with `await describeTool(name)` or `await describeNamespace(name)` before calling it.'\n\nexport type CodeModeDescriptionInput = {\n readonly tools: ReadonlyArray<NestedTool>\n readonly inlineBudget?: number\n /** Mention `store()`/`load()` persistence. */\n readonly store?: boolean\n}\n\n/**\n * The code mode tool description: intro, globals one per line, then nested tools grouped by\n * namespace, then one fixed line pointing to `searchTools`/`describeTool`/`describeNamespace`.\n * `all` tools get one line each outside the budget; `listed` tools are declared within the inline\n * budget; `search` tools never contribute, so adding or removing them (even whole namespaces of\n * them) leaves the text byte-identical.\n */\nexport const renderCodeModeDescription = (input: CodeModeDescriptionInput): string => {\n const catalog = codeModeCatalog(input.tools)\n\n const listing = selectCodeModeListing(catalog, input.inlineBudget ?? defaultCodeModeInlineBudget)\n\n const sections = [...groupByNamespace(listing.listed.map(tool => ({ tool })))].map(\n ([namespace, items]) =>\n namespaceSection(\n namespace,\n items.map(item => item.tool)\n )\n )\n\n return [\n intro,\n ['Globals:', ...globalLines(input.store === true)].join('\\n'),\n sections.length === 0\n ? 'Nested tools: none are listed here.'\n : ['## Nested tools by namespace', ...sections].join('\\n\\n'),\n unlistedToolsLine\n ].join('\\n\\n')\n}\n\n/** `describeTool` text: the description and TypeScript declaration of one tool. */\nexport const describeCodeModeTool = (tool: CodeModeCatalogTool): string =>\n renderToolSample(declarationTool(tool))\n\n/** Finds a tool by identifier or raw name. */\nexport const findCodeModeTool = (\n catalog: ReadonlyArray<CodeModeCatalogTool>,\n name: string\n): CodeModeCatalogTool | undefined =>\n catalog.find(tool => tool.identifier === name) ?? catalog.find(tool => tool.name === name)\n"],"mappings":"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"}
1
+ {"version":3,"file":"catalog.mjs","names":[],"sources":["../src/catalog.ts"],"sourcesContent":["import { Predicate } from 'effect'\nimport {\n renderDeclarations,\n renderToolOutputType,\n renderToolSample,\n toCodemodeIdentifier\n} from '@earendil-works/pi-codemode/declarations'\nimport { toolDiscovery } from '@yolk-sdk/agent/protocol'\nimport type { NestedTool } from '@yolk-sdk/agent/tools'\nimport type { CodeModeJsonSchema } from './executor.ts'\n\n/** How a nested tool reaches scripts: `all` tools are also direct model tools; `listed` and\n * `search` are the two discovery modes of `codemode`-only tools.\n */\nexport type CodeModeToolExposure = 'all' | 'listed' | 'search'\n\n/** One nested tool as scripts see it. */\nexport type CodeModeCatalogTool = {\n /** Raw tool name; scripts may call `tools[\"<name>\"](args)`. */\n readonly name: string\n /** Identifier for `tools.<identifier>(args)` (invalid identifier characters become `_`). */\n readonly identifier: string\n /**\n * How scripts call this tool, used to prefix its rejections: `tools.<identifier>`, or\n * `tools[\"<name>\"]` when an earlier tool already has the identifier (see `codeModeCallLabels`;\n * when the name is taken too, the tool is unreachable and the label only names it).\n */\n readonly callLabel: string\n /** The tool's `ToolModule.id`. */\n readonly namespace: string\n /** The module's `ToolModule.description`, when set. */\n readonly namespaceDescription?: string\n readonly description: string\n readonly inputSchema: CodeModeJsonSchema\n /** The declared output schema, or `{ type: 'string' }` for text results. */\n readonly outputSchema: CodeModeJsonSchema\n /** True when the tool declares an output schema (calls resolve to `structuredContent`). */\n readonly structured: boolean\n readonly exposure: CodeModeToolExposure\n}\n\nconst textOutputSchema: CodeModeJsonSchema = { type: 'string' }\n\n/**\n * The call label of each tool name, in order, as pi binds `tools`: each tool takes\n * `tools[<identifier>]` unless an earlier tool holds that key (first tool wins), then\n * `tools[<name>]` if still free. The label is `tools.<identifier>` for the tool that holds its\n * identifier, otherwise `tools[\"<name>\"]`. The executor guard and the host rejection prefix share\n * it, so both name a call the same way.\n */\nexport const codeModeCallLabels = (names: ReadonlyArray<string>): ReadonlyArray<string> => {\n const bound = new Set<string>()\n\n return names.map(name => {\n const identifier = toCodemodeIdentifier(name)\n const ownsIdentifier = !bound.has(identifier)\n\n bound.add(identifier)\n bound.add(name)\n\n return ownsIdentifier ? `tools.${identifier}` : `tools[${JSON.stringify(name)}]`\n })\n}\n\n/** Catalog of nested tools in resolution order. */\nexport const codeModeCatalog = (\n tools: ReadonlyArray<NestedTool>\n): ReadonlyArray<CodeModeCatalogTool> => {\n const labels = codeModeCallLabels(tools.map(({ def }) => def.name))\n\n return tools.map(({ def, moduleId, moduleDescription }, index) => {\n const tool: CodeModeCatalogTool = {\n name: def.name,\n identifier: toCodemodeIdentifier(def.name),\n callLabel: labels[index] ?? `tools[${JSON.stringify(def.name)}]`,\n namespace: moduleId,\n description: def.description,\n inputSchema: def.parameters,\n outputSchema: def.outputSchema ?? textOutputSchema,\n structured: def.outputSchema !== undefined,\n exposure: toolDiscovery(def) ?? 'all'\n }\n\n return moduleDescription === undefined\n ? tool\n : { ...tool, namespaceDescription: moduleDescription }\n })\n}\n\n/** Default inline budget of the nested tool listing, in estimated tokens. */\nexport const defaultCodeModeInlineBudget = 3000\n\n/** Estimated tokens of a text: four characters per token, rounded up. */\nexport const estimateCodeModeTokens = (text: string): number => Math.ceil(text.length / 4)\n\nconst maxShortTypeChars = 60\n\nconst shortType = (schema: CodeModeJsonSchema): string => {\n const rendered = renderToolOutputType(schema)\n\n if (rendered.length <= maxShortTypeChars) return rendered\n\n if (!Predicate.isBoolean(schema)) {\n if (schema.type === 'array') return 'Array<unknown>'\n\n if (schema.type === 'object') return 'object'\n }\n\n return 'unknown'\n}\n\n// Declarations only read names, descriptions, and schemas; `execute` is never called.\nconst declarationTool = (tool: CodeModeCatalogTool) => ({\n name: tool.name,\n description: tool.description,\n inputSchema: tool.inputSchema,\n outputSchema: tool.outputSchema,\n execute: () => undefined\n})\n\nconst directToolLine = (tool: CodeModeCatalogTool) =>\n `- \\`tools.${tool.identifier}(args)\\` takes the arguments of the \\`${tool.name}\\` tool and resolves to \\`${shortType(tool.outputSchema)}\\`.`\n\nconst declarationMembers = (tools: ReadonlyArray<CodeModeCatalogTool>) =>\n renderDeclarations({ tools: tools.map(declarationTool) })\n\ntype Candidate = {\n readonly tool: CodeModeCatalogTool\n readonly index: number\n readonly cost: number\n}\n\nconst candidateCost = (tool: CodeModeCatalogTool) =>\n estimateCodeModeTokens(declarationMembers([tool]))\n\nconst groupByNamespace = <A extends { readonly tool: CodeModeCatalogTool }>(\n items: ReadonlyArray<A>\n): ReadonlyMap<string, ReadonlyArray<A>> => {\n const groups = new Map<string, Array<A>>()\n\n for (const item of items) {\n const group = groups.get(item.tool.namespace)\n\n if (group === undefined) {\n groups.set(item.tool.namespace, [item])\n } else {\n group.push(item)\n }\n }\n\n return groups\n}\n\nexport type CodeModeListing = {\n /** Tools placed in the description, in catalog order: every `all` tool, and the `listed` tools\n * that fit the budget.\n */\n readonly listed: ReadonlyArray<CodeModeCatalogTool>\n /** Namespaces (catalog order) with callable tools the description does not list. */\n readonly unlistedNamespaces: ReadonlyArray<string>\n}\n\n/**\n * Chooses the tools the description lists. `all` tools are always placed, outside the budget\n * (the model already has their declarations). `listed` tools fill `budget` estimated tokens fairly\n * across namespaces: each round, every namespace still in play places its cheapest remaining tool;\n * a namespace whose next tool does not fit drops out. `search` tools are never candidates.\n */\nexport const selectCodeModeListing = (\n catalog: ReadonlyArray<CodeModeCatalogTool>,\n budget: number\n): CodeModeListing => {\n const candidates = catalog.flatMap((tool, index): ReadonlyArray<Candidate> =>\n tool.exposure === 'listed' ? [{ tool, index, cost: candidateCost(tool) }] : []\n )\n\n const queues = new Map(\n [...groupByNamespace(candidates)].map(([namespace, group]) => [\n namespace,\n [...group].sort((left, right) => left.cost - right.cost || left.index - right.index)\n ])\n )\n\n const placed = new Set(catalog.flatMap((tool, index) => (tool.exposure === 'all' ? [index] : [])))\n\n const inPlay = new Set(queues.keys())\n let remaining = budget\n\n while (inPlay.size > 0) {\n for (const [namespace, queue] of queues) {\n if (!inPlay.has(namespace)) continue\n\n const next = queue.shift()\n\n if (next === undefined || next.cost > remaining) {\n inPlay.delete(namespace)\n continue\n }\n\n placed.add(next.index)\n remaining -= next.cost\n }\n }\n\n const unlisted = new Set(\n catalog.flatMap((tool, index) => (placed.has(index) ? [] : [tool.namespace]))\n )\n\n return {\n listed: catalog.filter((_, index) => placed.has(index)),\n unlistedNamespaces: [...new Set(catalog.map(tool => tool.namespace))].filter(namespace =>\n unlisted.has(namespace)\n )\n }\n}\n\nconst intro = [\n 'Run a JavaScript script that calls tools and returns only what matters.',\n '`code` is the body of an async function: top-level `await` and `return` work. Write plain JavaScript; simple TypeScript annotations are stripped. There is no Node.js, network, filesystem, `require`, `fetch`, or timers.',\n 'Call tools as `await tools.<id>(args)`. Arguments must be plain JSON (no `NaN`, `undefined` array items, `Map`, or functions). A call resolves to the declared result type and rejects with an Error whose message starts with `tools.<id>:` when the tool fails. Calls still running when the script ends are cancelled, so await every call (use `Promise.all` for parallel calls).',\n 'Only the script output and its return value come back to you: filter and aggregate inside the script and return a small JSON value.'\n].join('\\n')\n\nconst stageLine = (reviewToolName: string) =>\n `- \\`await stage(name, args)\\`: record a call of a stageable tool for review instead of running it; resolves to \\`{ staged: true, key, index }\\`. Nothing runs until a person approves the plan through the \\`${reviewToolName}\\` tool, which the result tells you how to call. Once a script stages, it may only run read tools; a script that already ran a write tool cannot stage.`\n\n/** Stageable tool names listed in the description; the rest are summarized with a count. */\nconst maxListedStageableTools = 20\n\nconst stageableSection = (staging: CodeModeStagingDescription) => {\n const names = staging.tools.map(tool => tool.def.name)\n const hidden = names.length - maxListedStageableTools\n\n return [\n '## Stageable tools',\n 'Approval-gated tools a script cannot call through `tools`, only stage with `await stage(\"<name>\", args)` (arguments as for the tool of the same name):',\n ...names.slice(0, maxListedStageableTools).map(name => `- \\`${name}\\``),\n ...(hidden > 0\n ? [`- … and ${hidden} more (an unknown name rejects with the stageable tools).`]\n : [])\n ].join('\\n')\n}\n\nconst globalLines = (store: boolean) => [\n '- `text(value)` and `console.log(...values)`: append text to the output.',\n '- `image(dataUrl)`: append a base64 image (a `data:` URL or `{ type: \"image\", data, mimeType }`).',\n '- `exit()`: end the script successfully.',\n '- `ALL_TOOLS`: `{ name, description }` of every callable tool.',\n '- `await searchTools(query, { limit?, namespace? })`: find tools by topic; resolves to `Array<{ name: string; description: string }>` (default limit 8).',\n '- `await describeTool(name)`: the description and TypeScript declaration of a tool, or `undefined`.',\n '- `await describeNamespace(name)`: `{ name, description?, tools: Array<{ name, description }> }` for a namespace, or `undefined`.',\n ...(store\n ? [\n '- `store(key, value)` and `load(key)`: keep small JSON values for later scripts; writes are saved only when the script succeeds.'\n ]\n : [])\n]\n\nconst namespaceSection = (namespace: string, tools: ReadonlyArray<CodeModeCatalogTool>) => {\n const declared = tools.filter(tool => tool.exposure !== 'all')\n const direct = tools.filter(tool => tool.exposure === 'all')\n\n const description = tools.find(\n tool => tool.namespaceDescription !== undefined\n )?.namespaceDescription\n\n return [\n `### ${namespace}`,\n ...(description === undefined ? [] : [description]),\n ...(declared.length > 0 ? ['```ts', declarationMembers(declared), '```'] : []),\n ...direct.map(directToolLine)\n ].join('\\n')\n}\n\n// Fixed text: never derived from `search` tools or from which tools did not fit the budget.\nconst unlistedToolsLine =\n 'More tools may be callable than are listed here: find them with `await searchTools(query, { namespace? })` and read one with `await describeTool(name)` or `await describeNamespace(name)` before calling it.'\n\n/** Staged tool plans as the description shows them (`NestedToolStagingDescription`). */\nexport type CodeModeStagingDescription = {\n readonly tools: ReadonlyArray<NestedTool>\n readonly reviewToolName: string\n}\n\nexport type CodeModeDescriptionInput = {\n readonly tools: ReadonlyArray<NestedTool>\n readonly inlineBudget?: number\n /** Mention `store()`/`load()` persistence. */\n readonly store?: boolean\n /** Document `stage()` and list the stageable tools (only when some exist). */\n readonly staging?: CodeModeStagingDescription\n}\n\n/**\n * The code mode tool description: intro, globals one per line, then nested tools grouped by\n * namespace, then one fixed line pointing to `searchTools`/`describeTool`/`describeNamespace`.\n * `all` tools get one line each outside the budget; `listed` tools are declared within the inline\n * budget; `search` tools never contribute, so adding or removing them (even whole namespaces of\n * them) leaves the text byte-identical. With `staging` (and stageable tools), `stage()` is listed\n * with the globals and the stageable tool names follow the nested tools.\n */\nexport const renderCodeModeDescription = (input: CodeModeDescriptionInput): string => {\n const catalog = codeModeCatalog(input.tools)\n\n const listing = selectCodeModeListing(catalog, input.inlineBudget ?? defaultCodeModeInlineBudget)\n\n const sections = [...groupByNamespace(listing.listed.map(tool => ({ tool })))].map(\n ([namespace, items]) =>\n namespaceSection(\n namespace,\n items.map(item => item.tool)\n )\n )\n\n const staging =\n input.staging !== undefined && input.staging.tools.length > 0 ? input.staging : undefined\n\n return [\n intro,\n [\n 'Globals:',\n ...globalLines(input.store === true),\n ...(staging === undefined ? [] : [stageLine(staging.reviewToolName)])\n ].join('\\n'),\n sections.length === 0\n ? 'Nested tools: none are listed here.'\n : ['## Nested tools by namespace', ...sections].join('\\n\\n'),\n ...(staging === undefined ? [] : [stageableSection(staging)]),\n unlistedToolsLine\n ].join('\\n\\n')\n}\n\n/** `describeTool` text: the description and TypeScript declaration of one tool. */\nexport const describeCodeModeTool = (tool: CodeModeCatalogTool): string =>\n renderToolSample(declarationTool(tool))\n\n/** Finds a tool by identifier or raw name. */\nexport const findCodeModeTool = (\n catalog: ReadonlyArray<CodeModeCatalogTool>,\n name: string\n): CodeModeCatalogTool | undefined =>\n catalog.find(tool => tool.identifier === name) ?? catalog.find(tool => tool.name === name)\n"],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { CodeModeError, CodeModeErrorKind, CodeModeExecuteOptions, CodeModeExecutionResult, CodeModeExecutor, CodeModeExecutorGlobal, CodeModeExecutorTool, CodeModeJsonSchema, CodeModeOutputItem, CodeModeStore, CodeModeStoreWrites, CodeModeToolContext } from "./executor.mjs";
2
- import { CodeModeCatalogTool, CodeModeDescriptionInput, CodeModeListing, CodeModeToolExposure, codeModeCatalog, defaultCodeModeInlineBudget, describeCodeModeTool, estimateCodeModeTokens, findCodeModeTool, renderCodeModeDescription, selectCodeModeListing } from "./catalog.mjs";
2
+ import { CodeModeCatalogTool, CodeModeDescriptionInput, CodeModeListing, CodeModeStagingDescription, CodeModeToolExposure, codeModeCatalog, defaultCodeModeInlineBudget, describeCodeModeTool, estimateCodeModeTokens, findCodeModeTool, renderCodeModeDescription, selectCodeModeListing } from "./catalog.mjs";
3
3
  import { ClassifierConcurrencyLimiter, ClassifierToolParams, MakeClassifierToolOptions, classifierToolName, defaultClassifierProcessLimiter, defaultClassifierProcessMaxConcurrency, defaultClassifierToolMaxConcurrency, makeClassifierConcurrencyLimiter, makeClassifierTool } from "./classifier-tool.mjs";
4
4
  import { CodeModeSearchHit, CodeModeSearchOptions, codeModeSearchTokens, defaultCodeModeSearchLimit, searchCodeModeTools } from "./search.mjs";
5
5
  import { CodeModeCallSummary, CodeModeResultSegment, boundCodeModeSegments, codeModeResultSegments, codeModeSegmentsContent, defaultCodeModeMaxImageBytes, defaultCodeModeMaxImages, summarizeCodeModeCalls } from "./output.mjs";
6
- import { CodeModeInterruptedCall, CodeModeInterruptedCallStatus, CodeModeInterruptedCalls, CodeModeStructuredContent, CodeModeToolResultEntry, codeModeStoreFromToolResults, maxCodeModeStoreTotalChars, maxCodeModeStoreValueChars } from "./store.mjs";
7
- import { CodeModeAfterNestedCallInput, CodeModeLimits, CodeModeNestedCallOutcome, MakeCodeModeToolOptions, codeModeDeadlineMarginMs, codeModeMinimumTimeoutMs, codeModeToolName, defaultCodeModeLimits, makeCodeModeTool } from "./tool.mjs";
8
- export { type ClassifierConcurrencyLimiter, ClassifierToolParams, type CodeModeAfterNestedCallInput, type CodeModeCallSummary, type CodeModeCatalogTool, type CodeModeDescriptionInput, type CodeModeError, type CodeModeErrorKind, type CodeModeExecuteOptions, type CodeModeExecutionResult, type CodeModeExecutor, type CodeModeExecutorGlobal, type CodeModeExecutorTool, type CodeModeInterruptedCall, type CodeModeInterruptedCallStatus, type CodeModeInterruptedCalls, type CodeModeJsonSchema, type CodeModeLimits, type CodeModeListing, type CodeModeNestedCallOutcome, type CodeModeOutputItem, type CodeModeResultSegment, type CodeModeSearchHit, type CodeModeSearchOptions, type CodeModeStore, type CodeModeStoreWrites, type CodeModeStructuredContent, type CodeModeToolContext, type CodeModeToolExposure, type CodeModeToolResultEntry, type MakeClassifierToolOptions, type MakeCodeModeToolOptions, boundCodeModeSegments, classifierToolName, codeModeCatalog, codeModeDeadlineMarginMs, codeModeMinimumTimeoutMs, codeModeResultSegments, codeModeSearchTokens, codeModeSegmentsContent, codeModeStoreFromToolResults, codeModeToolName, defaultClassifierProcessLimiter, defaultClassifierProcessMaxConcurrency, defaultClassifierToolMaxConcurrency, defaultCodeModeInlineBudget, defaultCodeModeLimits, defaultCodeModeMaxImageBytes, defaultCodeModeMaxImages, defaultCodeModeSearchLimit, describeCodeModeTool, estimateCodeModeTokens, findCodeModeTool, makeClassifierConcurrencyLimiter, makeClassifierTool, makeCodeModeTool, maxCodeModeStoreTotalChars, maxCodeModeStoreValueChars, renderCodeModeDescription, searchCodeModeTools, selectCodeModeListing, summarizeCodeModeCalls };
6
+ import { CodeModeInterruptedCall, CodeModeInterruptedCallStatus, CodeModeInterruptedCalls, CodeModeStagedPlan, CodeModeStructuredContent, CodeModeToolResultEntry, codeModeStoreFromToolResults, maxCodeModeStoreTotalChars, maxCodeModeStoreValueChars } from "./store.mjs";
7
+ import { CodeModeAfterNestedCallInput, CodeModeLimits, CodeModeNestedCallOutcome, CodeModeStagingOptions, MakeCodeModeToolOptions, codeModeDeadlineMarginMs, codeModeMinimumTimeoutMs, codeModeToolName, defaultCodeModeLimits, makeCodeModeTool } from "./tool.mjs";
8
+ export { type ClassifierConcurrencyLimiter, ClassifierToolParams, type CodeModeAfterNestedCallInput, type CodeModeCallSummary, type CodeModeCatalogTool, type CodeModeDescriptionInput, type CodeModeError, type CodeModeErrorKind, type CodeModeExecuteOptions, type CodeModeExecutionResult, type CodeModeExecutor, type CodeModeExecutorGlobal, type CodeModeExecutorTool, type CodeModeInterruptedCall, type CodeModeInterruptedCallStatus, type CodeModeInterruptedCalls, type CodeModeJsonSchema, type CodeModeLimits, type CodeModeListing, type CodeModeNestedCallOutcome, type CodeModeOutputItem, type CodeModeResultSegment, type CodeModeSearchHit, type CodeModeSearchOptions, type CodeModeStagedPlan, type CodeModeStagingDescription, type CodeModeStagingOptions, type CodeModeStore, type CodeModeStoreWrites, type CodeModeStructuredContent, type CodeModeToolContext, type CodeModeToolExposure, type CodeModeToolResultEntry, type MakeClassifierToolOptions, type MakeCodeModeToolOptions, boundCodeModeSegments, classifierToolName, codeModeCatalog, codeModeDeadlineMarginMs, codeModeMinimumTimeoutMs, codeModeResultSegments, codeModeSearchTokens, codeModeSegmentsContent, codeModeStoreFromToolResults, codeModeToolName, defaultClassifierProcessLimiter, defaultClassifierProcessMaxConcurrency, defaultClassifierToolMaxConcurrency, defaultCodeModeInlineBudget, defaultCodeModeLimits, defaultCodeModeMaxImageBytes, defaultCodeModeMaxImages, defaultCodeModeSearchLimit, describeCodeModeTool, estimateCodeModeTokens, findCodeModeTool, makeClassifierConcurrencyLimiter, makeClassifierTool, makeCodeModeTool, maxCodeModeStoreTotalChars, maxCodeModeStoreValueChars, renderCodeModeDescription, searchCodeModeTools, selectCodeModeListing, summarizeCodeModeCalls };
package/dist/store.d.mts CHANGED
@@ -13,9 +13,19 @@ type CodeModeStructuredContent = {
13
13
  /** With `interrupted`, instead of `interruptedCalls`: the nested calls could not be listed
14
14
  * (the ledger's `list` failed), so any of them may have been applied.
15
15
  */
16
- readonly interruptedCallsUnavailable?: true;
16
+ readonly interruptedCallsUnavailable?: true; /** The staged tool plan a successful script saved (ADR 0005); nothing of it ran yet. */
17
+ readonly plan?: CodeModeStagedPlan;
17
18
  };
18
19
  };
20
+ /** A saved staged tool plan: review it with the `reviewToolName` tool and `{ planId: id,
21
+ * planDigest: digest }`.
22
+ */
23
+ type CodeModeStagedPlan = {
24
+ readonly id: string;
25
+ readonly digest: string;
26
+ readonly count: number;
27
+ readonly reviewToolName: string;
28
+ };
19
29
  /** What an abandoned execution's ledgered nested call is known to have done: `applied` (recorded
20
30
  * a result), `failed` (recorded a `ToolError` or an `isError` result), or `unknown` (started, no
21
31
  * recorded outcome, so it may have been applied).
@@ -73,5 +83,5 @@ declare const codeModeStoreFromToolResults: (entries: ReadonlyArray<CodeModeTool
73
83
  readonly toolName?: string;
74
84
  }) => CodeModeStore;
75
85
  //#endregion
76
- export { CodeModeInterruptedCall, CodeModeInterruptedCallStatus, CodeModeInterruptedCalls, CodeModeStructuredContent, CodeModeToolResultEntry, codeModeStoreFromToolResults, maxCodeModeStoreTotalChars, maxCodeModeStoreValueChars };
86
+ export { CodeModeInterruptedCall, CodeModeInterruptedCallStatus, CodeModeInterruptedCalls, CodeModeStagedPlan, CodeModeStructuredContent, CodeModeToolResultEntry, codeModeStoreFromToolResults, maxCodeModeStoreTotalChars, maxCodeModeStoreValueChars };
77
87
  //# sourceMappingURL=store.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.mts","names":[],"sources":["../src/store.ts"],"mappings":";;;;;AAQA;KAAY,yBAAA;EAAA,SACD,QAAA;IAAA,SACE,EAAA;IAAA,SACA,WAAA,GAAc,mBAAA,EADd;IAAA,SAGA,WAAA,SAFc;IAAA,SAId,gBAAA,GAAmB,wBAAwB;IAA3C;;;IAAA,SAIA,2BAAA;EAAA;AAAA;;;;AAQ4B;KAA7B,6BAAA;;;;KAKA,uBAAA;EAAA,SACD,GAAA;EAAA,SACA,QAAA;EAAA,SACA,IAAA;EAAA,SACA,MAAA,EAAQ,6BAA6B;AAAA;AAAA;AAWhD;;;;;;;AAXgD,KAWpC,wBAAA;EAAA,SACD,KAAA,EAAO,aAAa,CAAC,uBAAA;EAAA,SACrB,QAAA;EAAA,SACA,MAAA;IAAA,SACE,OAAA;IAAA,SACA,MAAA;IAAA,SACA,OAAA;EAAA;AAAA;;cAmBA,0BAAA;AAAuC;AAAA,cAGvC,0BAAA;;KAGD,uBAAA;EAAA,SACD,QAAA;EAAA,SACA,MAAA;IAAA,SAAmB,iBAAA;EAAA;AAAA;;;;;;AAAiB;AAc/C;;;;;cAAa,4BAAA,GACX,OAAA,EAAS,aAAA,CAAc,uBAAA,GACvB,OAAA;EAAA,SAAoB,QAAA;AAAA,MACnB,aAAA"}
1
+ {"version":3,"file":"store.d.mts","names":[],"sources":["../src/store.ts"],"mappings":";;;;;AAQA;KAAY,yBAAA;EAAA,SACD,QAAA;IAAA,SACE,EAAA;IAAA,SACA,WAAA,GAAc,mBAAA,EAUP;IAAA,SARP,WAAA,SAQyB;IAAA,SANzB,gBAAA,GAAmB,wBAAA;IALnB;;;IAAA,SASA,2BAAA,SAJA;IAAA,SAMA,IAAA,GAAO,kBAAA;EAAA;AAAA;;;AAAkB;KAO1B,kBAAA;EAAA,SACD,EAAA;EAAA,SACA,MAAA;EAAA,SACA,KAAA;EAAA,SACA,cAAA;AAAA;;;;AAAc;KAOb,6BAAA;;;;KAKA,uBAAA;EAAA,SACD,GAAA;EAAA,SACA,QAAA;EAAA,SACA,IAAA;EAAA,SACA,MAAA,EAAQ,6BAA6B;AAAA;;;;;;AAAA;AAWhD;;KAAY,wBAAA;EAAA,SACD,KAAA,EAAO,aAAa,CAAC,uBAAA;EAAA,SACrB,QAAA;EAAA,SACA,MAAA;IAAA,SACE,OAAA;IAAA,SACA,MAAA;IAAA,SACA,OAAA;EAAA;AAAA;;cAmBA,0BAAA;AAnBO;AAAA,cAsBP,0BAAA;;KAGD,uBAAA;EAAA,SACD,QAAA;EAAA,SACA,MAAA;IAAA,SAAmB,iBAAA;EAAA;AAAA;;AALuB;AAGrD;;;;;;;;AAE+C;cAclC,4BAAA,GACX,OAAA,EAAS,aAAA,CAAc,uBAAA,GACvB,OAAA;EAAA,SAAoB,QAAA;AAAA,MACnB,aAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"store.mjs","names":[],"sources":["../src/store.ts"],"sourcesContent":["import { Option } from 'effect'\nimport * as Schema from 'effect/Schema'\nimport type { CodeModeStore, CodeModeStoreWrites } from './executor.ts'\nimport { codeModeToolName } from './tool.ts'\n\n/** `structuredContent` of a code mode `ToolResult`. `storeWrites` is present only for successful\n * scripts that changed the store.\n */\nexport type CodeModeStructuredContent = {\n readonly codemode: {\n readonly ok: boolean\n readonly storeWrites?: CodeModeStoreWrites\n /** Set on the result of a re-executed call whose earlier execution was abandoned. */\n readonly interrupted?: true\n /** With `interrupted`: the abandoned execution's ledgered nested calls. */\n readonly interruptedCalls?: CodeModeInterruptedCalls\n /** With `interrupted`, instead of `interruptedCalls`: the nested calls could not be listed\n * (the ledger's `list` failed), so any of them may have been applied.\n */\n readonly interruptedCallsUnavailable?: true\n }\n}\n\n/** What an abandoned execution's ledgered nested call is known to have done: `applied` (recorded\n * a result), `failed` (recorded a `ToolError` or an `isError` result), or `unknown` (started, no\n * recorded outcome, so it may have been applied).\n */\nexport type CodeModeInterruptedCallStatus = 'applied' | 'failed' | 'unknown'\n\n/** One ledgered nested call of an abandoned execution. `key` is its ledger key\n * (`<toolCallId>/<seq>`); `args` is compact JSON, cut with a trailing `…` (then no longer JSON).\n */\nexport type CodeModeInterruptedCall = {\n readonly key: string\n readonly toolName: string\n readonly args: string\n readonly status: CodeModeInterruptedCallStatus\n}\n\n/**\n * `structuredContent.codemode.interruptedCalls`: plain JSON, in sequence order, bounded like\n * `nestedCalls` (at most `limits.maxNestedCalls` calls; arguments within 8 KiB per call and\n * 32 KiB in total). `complete: false` means calls were dropped or arguments cut, here or already\n * in the ledger entry (`ToolLedgerEntry.argsTruncated`); `counts` covers every ledgered nested\n * call, dropped ones included. Calls the ledger policy skips (by default read-only calls) never\n * appear.\n */\nexport type CodeModeInterruptedCalls = {\n readonly calls: ReadonlyArray<CodeModeInterruptedCall>\n readonly complete: boolean\n readonly counts: {\n readonly applied: number\n readonly failed: number\n readonly unknown: number\n }\n}\n\nconst StoreWritesSchema = Schema.Struct({\n set: Schema.Record(Schema.String, Schema.Json),\n delete: Schema.Array(Schema.String)\n})\n\nconst CodeModeStructuredContentSchema = Schema.Struct({\n codemode: Schema.Struct({\n ok: Schema.Boolean,\n storeWrites: Schema.optionalKey(StoreWritesSchema)\n })\n})\n\nconst decodeStructuredContent = Schema.decodeUnknownOption(CodeModeStructuredContentSchema)\n\n/** Largest stored value: characters of JSON (256 KiB), as the pi engine counts them. */\nexport const maxCodeModeStoreValueChars = 256 * 1024\n\n/** Largest store: characters of keys plus JSON values (1 MiB), as the pi engine counts them. */\nexport const maxCodeModeStoreTotalChars = 1024 * 1024\n\n/** One prior tool result with the name of the tool that produced it. */\nexport type CodeModeToolResultEntry = {\n readonly toolName: string\n readonly result: { readonly structuredContent?: unknown }\n}\n\n/**\n * Rebuilds the code mode store from prior tool results, oldest first: applies the `storeWrites` of\n * each successful script of the code mode tool named `toolName` (default `codemode`) in order.\n * Results of other tools (even when their `structuredContent` looks like a code mode result),\n * failed scripts, and malformed contents are skipped. Transcript `ToolResultMessage`s carry no tool\n * name; pair each with the name of its assistant tool call.\n *\n * The rebuilt store keeps the engine bounds: a write whose JSON value exceeds 256 Ki characters, or\n * that would grow the store beyond 1 Mi characters (keys plus JSON values), is dropped and the key\n * keeps its previous value. Deletions always apply. Pass the value from `loadStore`.\n */\nexport const codeModeStoreFromToolResults = (\n entries: ReadonlyArray<CodeModeToolResultEntry>,\n options: { readonly toolName?: string } = {}\n): CodeModeStore => {\n const toolName = options.toolName ?? codeModeToolName\n const store = new Map<string, { readonly value: Schema.Json; readonly chars: number }>()\n let total = 0\n\n for (const entry of entries) {\n if (entry.toolName !== toolName) continue\n\n const content = decodeStructuredContent(entry.result.structuredContent)\n\n if (Option.isNone(content) || !content.value.codemode.ok) continue\n\n const writes = content.value.codemode.storeWrites\n\n if (writes === undefined) continue\n\n for (const key of writes.delete) {\n total -= store.get(key)?.chars ?? 0\n store.delete(key)\n }\n\n for (const [key, value] of Object.entries(writes.set)) {\n const json = JSON.stringify(value)\n\n if (json.length > maxCodeModeStoreValueChars) continue\n\n const chars = key.length + json.length\n const next = total - (store.get(key)?.chars ?? 0) + chars\n\n if (next > maxCodeModeStoreTotalChars) continue\n\n store.set(key, { value, chars })\n total = next\n }\n }\n\n return Object.fromEntries([...store].map(([key, stored]) => [key, stored.value]))\n}\n"],"mappings":";;;;AAyDA,MAAM,oBAAoB,OAAO,OAAO;CACtC,KAAK,OAAO,OAAO,OAAO,QAAQ,OAAO,IAAI;CAC7C,QAAQ,OAAO,MAAM,OAAO,MAAM;AACpC,CAAC;AAED,MAAM,kCAAkC,OAAO,OAAO,EACpD,UAAU,OAAO,OAAO;CACtB,IAAI,OAAO;CACX,aAAa,OAAO,YAAY,iBAAiB;AACnD,CAAC,EACH,CAAC;AAED,MAAM,0BAA0B,OAAO,oBAAoB,+BAA+B;;AAG1F,MAAa,6BAA6B,MAAM;;AAGhD,MAAa,6BAA6B,OAAO;;;;;;;;;;;;AAmBjD,MAAa,gCACX,SACA,UAA0C,CAAC,MACzB;CAClB,MAAM,WAAW,QAAQ,YAAA;CACzB,MAAM,wBAAQ,IAAI,IAAqE;CACvF,IAAI,QAAQ;CAEZ,KAAK,MAAM,SAAS,SAAS;EAC3B,IAAI,MAAM,aAAa,UAAU;EAEjC,MAAM,UAAU,wBAAwB,MAAM,OAAO,iBAAiB;EAEtE,IAAI,OAAO,OAAO,OAAO,KAAK,CAAC,QAAQ,MAAM,SAAS,IAAI;EAE1D,MAAM,SAAS,QAAQ,MAAM,SAAS;EAEtC,IAAI,WAAW,KAAA,GAAW;EAE1B,KAAK,MAAM,OAAO,OAAO,QAAQ;GAC/B,SAAS,MAAM,IAAI,GAAG,GAAG,SAAS;GAClC,MAAM,OAAO,GAAG;EAClB;EAEA,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,GAAG,GAAG;GACrD,MAAM,OAAO,KAAK,UAAU,KAAK;GAEjC,IAAI,KAAK,SAAA,QAAqC;GAE9C,MAAM,QAAQ,IAAI,SAAS,KAAK;GAChC,MAAM,OAAO,SAAS,MAAM,IAAI,GAAG,GAAG,SAAS,KAAK;GAEpD,IAAI,OAAA,SAAmC;GAEvC,MAAM,IAAI,KAAK;IAAE;IAAO;GAAM,CAAC;GAC/B,QAAQ;EACV;CACF;CAEA,OAAO,OAAO,YAAY,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,KAAK,YAAY,CAAC,KAAK,OAAO,KAAK,CAAC,CAAC;AAClF"}
1
+ {"version":3,"file":"store.mjs","names":[],"sources":["../src/store.ts"],"sourcesContent":["import { Option } from 'effect'\nimport * as Schema from 'effect/Schema'\nimport type { CodeModeStore, CodeModeStoreWrites } from './executor.ts'\nimport { codeModeToolName } from './tool.ts'\n\n/** `structuredContent` of a code mode `ToolResult`. `storeWrites` is present only for successful\n * scripts that changed the store.\n */\nexport type CodeModeStructuredContent = {\n readonly codemode: {\n readonly ok: boolean\n readonly storeWrites?: CodeModeStoreWrites\n /** Set on the result of a re-executed call whose earlier execution was abandoned. */\n readonly interrupted?: true\n /** With `interrupted`: the abandoned execution's ledgered nested calls. */\n readonly interruptedCalls?: CodeModeInterruptedCalls\n /** With `interrupted`, instead of `interruptedCalls`: the nested calls could not be listed\n * (the ledger's `list` failed), so any of them may have been applied.\n */\n readonly interruptedCallsUnavailable?: true\n /** The staged tool plan a successful script saved (ADR 0005); nothing of it ran yet. */\n readonly plan?: CodeModeStagedPlan\n }\n}\n\n/** A saved staged tool plan: review it with the `reviewToolName` tool and `{ planId: id,\n * planDigest: digest }`.\n */\nexport type CodeModeStagedPlan = {\n readonly id: string\n readonly digest: string\n readonly count: number\n readonly reviewToolName: string\n}\n\n/** What an abandoned execution's ledgered nested call is known to have done: `applied` (recorded\n * a result), `failed` (recorded a `ToolError` or an `isError` result), or `unknown` (started, no\n * recorded outcome, so it may have been applied).\n */\nexport type CodeModeInterruptedCallStatus = 'applied' | 'failed' | 'unknown'\n\n/** One ledgered nested call of an abandoned execution. `key` is its ledger key\n * (`<toolCallId>/<seq>`); `args` is compact JSON, cut with a trailing `…` (then no longer JSON).\n */\nexport type CodeModeInterruptedCall = {\n readonly key: string\n readonly toolName: string\n readonly args: string\n readonly status: CodeModeInterruptedCallStatus\n}\n\n/**\n * `structuredContent.codemode.interruptedCalls`: plain JSON, in sequence order, bounded like\n * `nestedCalls` (at most `limits.maxNestedCalls` calls; arguments within 8 KiB per call and\n * 32 KiB in total). `complete: false` means calls were dropped or arguments cut, here or already\n * in the ledger entry (`ToolLedgerEntry.argsTruncated`); `counts` covers every ledgered nested\n * call, dropped ones included. Calls the ledger policy skips (by default read-only calls) never\n * appear.\n */\nexport type CodeModeInterruptedCalls = {\n readonly calls: ReadonlyArray<CodeModeInterruptedCall>\n readonly complete: boolean\n readonly counts: {\n readonly applied: number\n readonly failed: number\n readonly unknown: number\n }\n}\n\nconst StoreWritesSchema = Schema.Struct({\n set: Schema.Record(Schema.String, Schema.Json),\n delete: Schema.Array(Schema.String)\n})\n\nconst CodeModeStructuredContentSchema = Schema.Struct({\n codemode: Schema.Struct({\n ok: Schema.Boolean,\n storeWrites: Schema.optionalKey(StoreWritesSchema)\n })\n})\n\nconst decodeStructuredContent = Schema.decodeUnknownOption(CodeModeStructuredContentSchema)\n\n/** Largest stored value: characters of JSON (256 KiB), as the pi engine counts them. */\nexport const maxCodeModeStoreValueChars = 256 * 1024\n\n/** Largest store: characters of keys plus JSON values (1 MiB), as the pi engine counts them. */\nexport const maxCodeModeStoreTotalChars = 1024 * 1024\n\n/** One prior tool result with the name of the tool that produced it. */\nexport type CodeModeToolResultEntry = {\n readonly toolName: string\n readonly result: { readonly structuredContent?: unknown }\n}\n\n/**\n * Rebuilds the code mode store from prior tool results, oldest first: applies the `storeWrites` of\n * each successful script of the code mode tool named `toolName` (default `codemode`) in order.\n * Results of other tools (even when their `structuredContent` looks like a code mode result),\n * failed scripts, and malformed contents are skipped. Transcript `ToolResultMessage`s carry no tool\n * name; pair each with the name of its assistant tool call.\n *\n * The rebuilt store keeps the engine bounds: a write whose JSON value exceeds 256 Ki characters, or\n * that would grow the store beyond 1 Mi characters (keys plus JSON values), is dropped and the key\n * keeps its previous value. Deletions always apply. Pass the value from `loadStore`.\n */\nexport const codeModeStoreFromToolResults = (\n entries: ReadonlyArray<CodeModeToolResultEntry>,\n options: { readonly toolName?: string } = {}\n): CodeModeStore => {\n const toolName = options.toolName ?? codeModeToolName\n const store = new Map<string, { readonly value: Schema.Json; readonly chars: number }>()\n let total = 0\n\n for (const entry of entries) {\n if (entry.toolName !== toolName) continue\n\n const content = decodeStructuredContent(entry.result.structuredContent)\n\n if (Option.isNone(content) || !content.value.codemode.ok) continue\n\n const writes = content.value.codemode.storeWrites\n\n if (writes === undefined) continue\n\n for (const key of writes.delete) {\n total -= store.get(key)?.chars ?? 0\n store.delete(key)\n }\n\n for (const [key, value] of Object.entries(writes.set)) {\n const json = JSON.stringify(value)\n\n if (json.length > maxCodeModeStoreValueChars) continue\n\n const chars = key.length + json.length\n const next = total - (store.get(key)?.chars ?? 0) + chars\n\n if (next > maxCodeModeStoreTotalChars) continue\n\n store.set(key, { value, chars })\n total = next\n }\n }\n\n return Object.fromEntries([...store].map(([key, stored]) => [key, stored.value]))\n}\n"],"mappings":";;;;AAqEA,MAAM,oBAAoB,OAAO,OAAO;CACtC,KAAK,OAAO,OAAO,OAAO,QAAQ,OAAO,IAAI;CAC7C,QAAQ,OAAO,MAAM,OAAO,MAAM;AACpC,CAAC;AAED,MAAM,kCAAkC,OAAO,OAAO,EACpD,UAAU,OAAO,OAAO;CACtB,IAAI,OAAO;CACX,aAAa,OAAO,YAAY,iBAAiB;AACnD,CAAC,EACH,CAAC;AAED,MAAM,0BAA0B,OAAO,oBAAoB,+BAA+B;;AAG1F,MAAa,6BAA6B,MAAM;;AAGhD,MAAa,6BAA6B,OAAO;;;;;;;;;;;;AAmBjD,MAAa,gCACX,SACA,UAA0C,CAAC,MACzB;CAClB,MAAM,WAAW,QAAQ,YAAA;CACzB,MAAM,wBAAQ,IAAI,IAAqE;CACvF,IAAI,QAAQ;CAEZ,KAAK,MAAM,SAAS,SAAS;EAC3B,IAAI,MAAM,aAAa,UAAU;EAEjC,MAAM,UAAU,wBAAwB,MAAM,OAAO,iBAAiB;EAEtE,IAAI,OAAO,OAAO,OAAO,KAAK,CAAC,QAAQ,MAAM,SAAS,IAAI;EAE1D,MAAM,SAAS,QAAQ,MAAM,SAAS;EAEtC,IAAI,WAAW,KAAA,GAAW;EAE1B,KAAK,MAAM,OAAO,OAAO,QAAQ;GAC/B,SAAS,MAAM,IAAI,GAAG,GAAG,SAAS;GAClC,MAAM,OAAO,GAAG;EAClB;EAEA,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,GAAG,GAAG;GACrD,MAAM,OAAO,KAAK,UAAU,KAAK;GAEjC,IAAI,KAAK,SAAA,QAAqC;GAE9C,MAAM,QAAQ,IAAI,SAAS,KAAK;GAChC,MAAM,OAAO,SAAS,MAAM,IAAI,GAAG,GAAG,SAAS,KAAK;GAEpD,IAAI,OAAA,SAAmC;GAEvC,MAAM,IAAI,KAAK;IAAE;IAAO;GAAM,CAAC;GAC/B,QAAQ;EACV;CACF;CAEA,OAAO,OAAO,YAAY,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,KAAK,YAAY,CAAC,KAAK,OAAO,KAAK,CAAC,CAAC;AAClF"}
package/dist/tool.d.mts CHANGED
@@ -71,6 +71,16 @@ type MakeCodeModeToolOptions<Context> = {
71
71
  * throw) is logged and never changes the call's result.
72
72
  */
73
73
  readonly afterNestedCall?: (input: CodeModeAfterNestedCallInput<Context>) => Effect.Effect<void>;
74
+ /**
75
+ * Staged tool plans (ADR 0005). When the resolution offers staging (`resolveTools(..., { plans,
76
+ * interactionHost })` with a plan review tool), scripts get `stage(name, args)` for stageable
77
+ * approval-gated tools. `false` turns it off for this tool; `maxCalls` may only lower the
78
+ * resolution's cap.
79
+ */
80
+ readonly staging?: CodeModeStagingOptions | false;
81
+ };
82
+ type CodeModeStagingOptions = {
83
+ /** Staged calls per script; `stage` rejects past it. At most the resolution's `maxCalls`. */readonly maxCalls?: number;
74
84
  };
75
85
  /** Outcome of a nested call for `afterNestedCall`. */
76
86
  type CodeModeNestedCallOutcome = 'success' | 'failure' | 'interrupted';
@@ -99,5 +109,5 @@ type CodeModeAfterNestedCallInput<Context> = {
99
109
  */
100
110
  declare const makeCodeModeTool: <Context>(options: MakeCodeModeToolOptions<Context>) => ToolRegistration<Context>;
101
111
  //#endregion
102
- export { CodeModeAfterNestedCallInput, CodeModeLimits, CodeModeNestedCallOutcome, MakeCodeModeToolOptions, codeModeDeadlineMarginMs, codeModeMinimumTimeoutMs, codeModeToolName, defaultCodeModeLimits, makeCodeModeTool };
112
+ export { CodeModeAfterNestedCallInput, CodeModeLimits, CodeModeNestedCallOutcome, CodeModeStagingOptions, MakeCodeModeToolOptions, codeModeDeadlineMarginMs, codeModeMinimumTimeoutMs, codeModeToolName, defaultCodeModeLimits, makeCodeModeTool };
103
113
  //# sourceMappingURL=tool.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tool.d.mts","names":[],"sources":["../src/tool.ts"],"mappings":";;;;;;;;cAsEa,gBAAA;AAAb;AAAA,KAGY,cAAA;kGAED,SAAA,WALkB;EAAA,SAOlB,gBAAA;EAJe;;;;EAAA,SASf,cAAA;EAAA,SAEA,cAAA,WAEA;EAAA,SAAA,SAAA;EAIa;AAAA;AAGxB;EAHwB,SAAb,aAAA;AAAA;AAAA,cAGE,qBAAA;EAAA;;;;;;;;cAUA,wBAAA;;cAGA,wBAAA;AAAA,KAED,uBAAA;EAAA,SACD,QAAA,EAAU,gBAAA,EAHR;EAAA,SAKF,IAAA;WAEA,YAAA;EAAA,SACA,MAAA,GAAS,cAAA;EANR;;;;EAAA,SAWD,QAAA,IAAY,OAAA,EAAS,OAAA;EAAA;;;;EAAA,SAKrB,SAAA,IAAa,OAAA,EAAS,OAAA,KAAY,MAAA,CAAO,MAAA,CAAO,aAAA,EAAe,SAAA;EASvD;;;;;;;EAAA,SADR,gBAAA,IAAoB,KAAA;IAAA,SAClB,IAAA,EAAM,QAAA;IAAA,SACN,OAAA,EAAS,OAAA;EAAA,MACd,MAAA,CAAO,MAAA;EAxBJ;;;;;;;;;EAAA,SAkCA,eAAA,IAAmB,KAAA,EAAO,4BAAA,CAA6B,OAAA,MAAa,MAAA,CAAO,MAAA;AAAA;;KAI1E,yBAAA;AAAA,KAEA,4BAAA;EAAA,SACD,IAAA,EAAM,QAAA;EAAA,SACN,OAAA,EAAS,yBAAA;EAAA,SACT,UAAA;EAAA,SACA,OAAA,EAAS,OAAA;EAAA,SACT,MAAA,GAAS,UAAA;AAAA;;;;;;;;;;AAXwE;AAI5F;;;;AAAqC;AAErC;cAskBa,gBAAA,YACX,OAAA,EAAS,uBAAA,CAAwB,OAAA,MAChC,gBAAA,CAAiB,OAAA"}
1
+ {"version":3,"file":"tool.d.mts","names":[],"sources":["../src/tool.ts"],"mappings":";;;;;;;;cA2Ea,gBAAA;AAAb;AAAA,KAGY,cAAA;kGAED,SAAA,WALkB;EAAA,SAOlB,gBAAA;EAJe;;;;EAAA,SASf,cAAA;EAAA,SAEA,cAAA,WAEA;EAAA,SAAA,SAAA;EAIa;AAAA;AAGxB;EAHwB,SAAb,aAAA;AAAA;AAAA,cAGE,qBAAA;EAAA;;;;;;;;cAUA,wBAAA;;cAGA,wBAAA;AAAA,KAED,uBAAA;EAAA,SACD,QAAA,EAAU,gBAAA,EAHR;EAAA,SAKF,IAAA;WAEA,YAAA;EAAA,SACA,MAAA,GAAS,cAAA;EANR;;;;EAAA,SAWD,QAAA,IAAY,OAAA,EAAS,OAAA;EAAA;;;;EAAA,SAKrB,SAAA,IAAa,OAAA,EAAS,OAAA,KAAY,MAAA,CAAO,MAAA,CAAO,aAAA,EAAe,SAAA;EASvD;;;;;;;EAAA,SADR,gBAAA,IAAoB,KAAA;IAAA,SAClB,IAAA,EAAM,QAAA;IAAA,SACN,OAAA,EAAS,OAAA;EAAA,MACd,MAAA,CAAO,MAAA;EA1BM;;;;;;;;;EAAA,SAoCV,eAAA,IAAmB,KAAA,EAAO,4BAAA,CAA6B,OAAA,MAAa,MAAA,CAAO,MAAA;EArB9D;;;;;;EAAA,SA4Bb,OAAA,GAAU,sBAAA;AAAA;AAAA,KAGT,sBAAA;EArBU,sGAuBX,QAAQ;AAAA;;KAIP,yBAAA;AAAA,KAEA,4BAAA;EAAA,SACD,IAAA,EAAM,QAAA;EAAA,SACN,OAAA,EAAS,yBAAA;EAAA,SACT,UAAA;EAAA,SACA,OAAA,EAAS,OAAA;EAAA,SACT,MAAA,GAAS,UAAA;AAAA;;AAhBuB;AAG3C;;;;AAEmB;AAInB;;;;AAAqC;AAErC;;;;cA+sBa,gBAAA,YACX,OAAA,EAAS,uBAAA,CAAwB,OAAA,MAChC,gBAAA,CAAiB,OAAA"}
package/dist/tool.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { codeModeCatalog, describeCodeModeTool, findCodeModeTool, renderCodeModeDescription } from "./catalog.mjs";
2
2
  import { searchCodeModeTools } from "./search.mjs";
3
3
  import { boundCodeModeSegments, codeModeResultSegments, codeModeSegmentsContent, defaultCodeModeMaxImageBytes } from "./output.mjs";
4
- import { Cause, Clock, Data, Duration, Effect, Exit, Fiber, FiberSet, Option, Predicate, Scope } from "effect";
4
+ import { Cause, Clock, Data, Duration, Effect, Exit, Fiber, FiberSet, Option, Predicate, Result, Scope } from "effect";
5
5
  import { ToolCall, ToolResult, boundNestedToolCallArgs, contentPartText, makeNestedToolCallRecorder, nestedToolCallMaxErrorChars, nestedToolCallRecordLimit, nestedToolCallResultFields, recordNestedToolCall, truncateCodePoints } from "@yolk-sdk/agent/protocol";
6
6
  import * as Schema from "effect/Schema";
7
7
  import { makeTool } from "@yolk-sdk/agent/tools";
@@ -106,6 +106,35 @@ const discoveryGlobals = (catalog) => [
106
106
  }
107
107
  }
108
108
  ];
109
+ /** `stage(name, args)`: records one call into the script's plan (never runs it). */
110
+ const stageGlobal = (plan, fork) => ({
111
+ name: "stage",
112
+ spread: true,
113
+ signature: "(name: string, args: object): Promise<{ staged: true; key: string; index: number }>",
114
+ description: "Record a call of a stageable tool into this script’s plan for a person to review; it does not run.",
115
+ execute: (args, { signal }) => {
116
+ const [name, params] = Array.isArray(args) ? args : [];
117
+ if (signal.aborted) return Promise.reject(/* @__PURE__ */ new Error("stage was cancelled."));
118
+ if (!Predicate.isString(name)) return Promise.reject(/* @__PURE__ */ new Error("stage: the first argument must be a tool name."));
119
+ return new Promise((resolve, reject) => {
120
+ fork(plan.stage({
121
+ name,
122
+ params: params === void 0 ? {} : params
123
+ }), { signal }).addObserver((exit) => {
124
+ if (Exit.isSuccess(exit)) {
125
+ resolve(exit.value);
126
+ return;
127
+ }
128
+ const error = Cause.findErrorOption(exit.cause);
129
+ reject(/* @__PURE__ */ new Error(Option.isSome(error) ? `stage: ${error.value.message}` : Cause.hasInterruptsOnly(exit.cause) ? "stage was cancelled." : "stage failed unexpectedly."));
130
+ });
131
+ });
132
+ }
133
+ });
134
+ const planNotice = (plan) => `\n\nStaged ${plan.count} call${plan.count === 1 ? "" : "s"} as plan ${plan.id}; nothing was applied yet. To apply them, call ${plan.reviewToolName}(${JSON.stringify({
135
+ planId: plan.id,
136
+ planDigest: plan.digest
137
+ })}): a person reviews the plan and selects the calls to apply.`;
109
138
  const hasStoreWrites = (result) => result.storeWrites !== void 0 && (Object.keys(result.storeWrites.set).length > 0 || result.storeWrites.delete.length > 0);
110
139
  const timeoutFor = (limit, deadline, now) => deadline === void 0 ? limit : Math.max(codeModeMinimumTimeoutMs, Math.min(limit, deadline - now - codeModeDeadlineMarginMs));
111
140
  /**
@@ -123,6 +152,12 @@ const runScript = (input) => Effect.gen(function* () {
123
152
  const fibers = yield* FiberSet.make().pipe(Scope.provide(scope));
124
153
  const runFork = yield* FiberSet.runtime(fibers)();
125
154
  const records = [];
155
+ const stagingOptions = options.staging === false ? void 0 : options.staging;
156
+ const staging = options.staging === false ? void 0 : nested.staging;
157
+ const maxStagedCalls = stagingOptions?.maxCalls;
158
+ const plan = staging === void 0 ? void 0 : staging.begin(maxStagedCalls === void 0 ? void 0 : { maxCalls: maxStagedCalls });
159
+ const stageFibers = yield* FiberSet.make().pipe(Scope.provide(scope));
160
+ const stageFork = yield* FiberSet.runtime(stageFibers)();
126
161
  const callTool = (tool) => (args, { signal }) => {
127
162
  if (signal.aborted) return Promise.reject(/* @__PURE__ */ new Error(`${tool.callLabel} was cancelled.`));
128
163
  if (records.length >= limits.maxNestedCalls) return Promise.reject(/* @__PURE__ */ new Error(`${tool.callLabel}: Nested call limit reached: a script may make at most ${limits.maxNestedCalls} tool calls. Batch the work or return partial results.`));
@@ -155,7 +190,15 @@ const runScript = (input) => Effect.gen(function* () {
155
190
  context: input.context
156
191
  }))));
157
192
  });
158
- const admitted = options.beforeNestedCall === void 0 ? run : options.beforeNestedCall({
193
+ const guarded = plan === void 0 ? run : plan.admit(nestedCall).pipe(Effect.matchEffect({
194
+ onFailure: (error) => Effect.succeed(ToolResult.make({
195
+ toolCallId: nestedCall.id,
196
+ content: error.message,
197
+ isError: true
198
+ })),
199
+ onSuccess: () => run
200
+ }));
201
+ const admitted = options.beforeNestedCall === void 0 ? guarded : options.beforeNestedCall({
159
202
  call: nestedCall,
160
203
  context: input.context
161
204
  }).pipe(Effect.matchEffect({
@@ -164,7 +207,7 @@ const runScript = (input) => Effect.gen(function* () {
164
207
  content: message,
165
208
  isError: true
166
209
  })),
167
- onSuccess: () => run
210
+ onSuccess: () => guarded
168
211
  }));
169
212
  const execution = Effect.gen(function* () {
170
213
  const started = yield* Clock.currentTimeMillis;
@@ -195,7 +238,7 @@ const runScript = (input) => Effect.gen(function* () {
195
238
  const result = yield* Effect.tryPromise({
196
239
  try: (signal) => options.executor.execute(input.code, {
197
240
  tools,
198
- globals: discoveryGlobals(catalog),
241
+ globals: plan === void 0 ? discoveryGlobals(catalog) : [...discoveryGlobals(catalog), stageGlobal(plan, stageFork)],
199
242
  timeoutMs,
200
243
  memoryLimitBytes: limits.memoryLimitBytes,
201
244
  store,
@@ -218,7 +261,20 @@ const runScript = (input) => Effect.gen(function* () {
218
261
  output: []
219
262
  }))));
220
263
  yield* boundedWait(FiberSet.clear(fibers));
264
+ yield* boundedWait(FiberSet.clear(stageFibers));
221
265
  const finishedAt = yield* Clock.currentTimeMillis;
266
+ const stagedCount = plan === void 0 ? 0 : (yield* plan.staged).length;
267
+ const saved = plan === void 0 || stagedCount === 0 || !result.ok ? void 0 : yield* plan.finish.pipe(Effect.result);
268
+ const savedPlan = staging === void 0 || saved === void 0 || Result.isFailure(saved) || saved.success === void 0 ? void 0 : {
269
+ id: saved.success.id,
270
+ digest: saved.success.digest,
271
+ count: saved.success.calls.length,
272
+ reviewToolName: staging.reviewToolName
273
+ };
274
+ const planFailure = saved !== void 0 && Result.isFailure(saved) ? saved.failure : void 0;
275
+ const planFailed = planFailure !== void 0;
276
+ const plural = stagedCount === 1 ? "" : "s";
277
+ const stagingNotice = stagedCount === 0 ? void 0 : !result.ok ? `\n\nThe script failed, so its ${stagedCount} staged call${plural} ${stagedCount === 1 ? "was" : "were"} discarded; nothing was applied.` : planFailure !== void 0 ? `\n\nThe script staged ${stagedCount} call${plural}, but the plan could not be saved (${planFailure.message}); nothing was applied. Run the script again.` : savedPlan === void 0 ? void 0 : planNotice(savedPlan);
222
278
  const recorded = records.map((record) => ({
223
279
  id: record.id,
224
280
  name: record.name,
@@ -226,25 +282,34 @@ const runScript = (input) => Effect.gen(function* () {
226
282
  ...record.outcome ?? { status: "cancelled" }
227
283
  }));
228
284
  const { nestedCalls, usage } = nestedToolCallResultFields(recorded.reduce(recordNestedToolCall, makeNestedToolCallRecorder({ maxCalls: limits.maxNestedCalls })));
229
- const content = codeModeSegmentsContent(codeModeResultSegments({
285
+ const segments = codeModeResultSegments({
230
286
  result,
231
287
  wallTimeMs: finishedAt - startedAt,
232
288
  calls: recorded,
233
289
  maxChars: limits.maxOutputChars,
234
290
  maxImages: limits.maxImages,
235
291
  maxImageBytes: limits.maxImageBytes
236
- }));
237
- const structuredContent = { codemode: result.ok && result.storeWrites !== void 0 && hasStoreWrites(result) ? {
292
+ });
293
+ const content = codeModeSegmentsContent(stagingNotice === void 0 ? segments : [...segments, {
294
+ type: "text",
295
+ text: stagingNotice
296
+ }]);
297
+ const ok = result.ok && !planFailed;
298
+ const codemode = ok && result.storeWrites !== void 0 && hasStoreWrites(result) ? {
238
299
  ok: true,
239
300
  storeWrites: result.storeWrites
240
- } : { ok: result.ok } };
301
+ } : { ok };
302
+ const structuredContent = { codemode: savedPlan === void 0 ? codemode : {
303
+ ...codemode,
304
+ plan: savedPlan
305
+ } };
241
306
  const fields = {
242
307
  toolCallId: call.id,
243
308
  content,
244
309
  structuredContent,
245
310
  nestedCalls
246
311
  };
247
- if (!result.ok) fields.isError = true;
312
+ if (!ok) fields.isError = true;
248
313
  if (usage !== void 0) fields.usage = usage;
249
314
  return ToolResult.make(fields);
250
315
  }), (scope, exit) => boundedWait(Scope.close(scope, exit)));
@@ -366,10 +431,15 @@ const makeCodeModeTool = (options) => {
366
431
  parameters: CodeModeParams,
367
432
  access: "write",
368
433
  nestedToolAccess: true,
369
- describe: ({ tools }) => renderCodeModeDescription({
434
+ describe: ({ tools, staging }) => renderCodeModeDescription(staging === void 0 || options.staging === false ? {
370
435
  tools,
371
436
  inlineBudget,
372
437
  store
438
+ } : {
439
+ tools,
440
+ inlineBudget,
441
+ store,
442
+ staging
373
443
  }),
374
444
  abandonedResult: abandonedScriptResult(limits),
375
445
  execute: ({ call, context, params, nested }) => nested === void 0 ? Effect.fail(new ToolError({
package/dist/tool.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"tool.mjs","names":[],"sources":["../src/tool.ts"],"sourcesContent":["import {\n Cause,\n Clock,\n Data,\n Duration,\n Effect,\n Exit,\n Fiber,\n FiberSet,\n Option,\n Predicate,\n Scope\n} from 'effect'\nimport * as Schema from 'effect/Schema'\nimport { ToolError } from '@yolk-sdk/agent/loop'\nimport {\n boundNestedToolCallArgs,\n makeNestedToolCallRecorder,\n nestedToolCallMaxErrorChars,\n nestedToolCallRecordLimit,\n nestedToolCallResultFields,\n recordNestedToolCall,\n truncateCodePoints,\n contentPartText,\n ToolCall,\n ToolResult,\n type AgentUsage,\n type Content,\n type NestedToolCallInput,\n type NestedToolCallStatus\n} from '@yolk-sdk/agent/protocol'\nimport {\n makeTool,\n type NestedToolExecutor,\n type ToolExecutionInput,\n type ToolLedgerAbandonedInput,\n type ToolLedgerEntry,\n type ToolRegistration\n} from '@yolk-sdk/agent/tools'\nimport {\n codeModeCatalog,\n defaultCodeModeInlineBudget,\n describeCodeModeTool,\n findCodeModeTool,\n renderCodeModeDescription,\n type CodeModeCatalogTool\n} from './catalog.ts'\nimport type {\n CodeModeExecutionResult,\n CodeModeExecutor,\n CodeModeExecutorGlobal,\n CodeModeExecutorTool,\n CodeModeStore\n} from './executor.ts'\nimport {\n boundCodeModeSegments,\n codeModeResultSegments,\n codeModeSegmentsContent,\n defaultCodeModeMaxImageBytes,\n defaultCodeModeMaxImages\n} from './output.ts'\nimport { searchCodeModeTools } from './search.ts'\nimport type {\n CodeModeInterruptedCall,\n CodeModeInterruptedCalls,\n CodeModeInterruptedCallStatus,\n CodeModeStructuredContent\n} from './store.ts'\n\n/** Default name of the code mode tool. */\nexport const codeModeToolName = 'codemode'\n\n/** Host-owned limits of one script. */\nexport type CodeModeLimits = {\n /** Overall deadline including nested calls. Default 120000 (clamped by `deadline`). */\n readonly timeoutMs?: number\n /** VM heap cap. Default 64 MiB. */\n readonly memoryLimitBytes?: number\n /** Nested tool calls per script; further calls reject with an Error. Default 256. The\n * `nestedCalls` record keeps up to this many calls, at most `nestedToolCallMaxRecordedCalls`\n * (4096) whatever the limit (argument byte budgets still apply).\n */\n readonly maxNestedCalls?: number\n /** Model-visible result characters, cut head and tail with an omission marker. Default 40000. */\n readonly maxOutputChars?: number\n /** Images kept in the result, in order; later images are dropped with a note. Default 8. */\n readonly maxImages?: number\n /** Base64 characters of images kept in the result in total; an image that would exceed it is\n * dropped with a note. Default 4 MiB.\n */\n readonly maxImageBytes?: number\n}\n\nexport const defaultCodeModeLimits = {\n timeoutMs: 120_000,\n memoryLimitBytes: 64 * 1024 * 1024,\n maxNestedCalls: 256,\n maxOutputChars: 40_000,\n maxImages: defaultCodeModeMaxImages,\n maxImageBytes: defaultCodeModeMaxImageBytes\n} as const satisfies Required<CodeModeLimits>\n\n/** Kept between a host deadline and the script deadline, for result handling. */\nexport const codeModeDeadlineMarginMs = 5_000\n\n/** A deadline never clamps a script below this timeout. */\nexport const codeModeMinimumTimeoutMs = 1_000\n\nexport type MakeCodeModeToolOptions<Context> = {\n readonly executor: CodeModeExecutor\n /** Default `codemode`. */\n readonly name?: string\n /** Estimated tokens (four characters each) for listing nested tools. Default 3000. */\n readonly inlineBudget?: number\n readonly limits?: CodeModeLimits\n /**\n * Epoch milliseconds by which the tool call must end, for example the remaining function budget\n * of a Workflow step. The script timeout is clamped to it minus 5 s, but never below 1 s.\n */\n readonly deadline?: (context: Context) => number | undefined\n /**\n * The store scripts read with `load(key)`, usually rebuilt from the transcript with\n * `codeModeStoreFromToolResults`. Without it every script starts with an empty store.\n */\n readonly loadStore?: (context: Context) => Effect.Effect<CodeModeStore, ToolError>\n /**\n * Runs before each nested call executes, with the nested call (id `<toolCallId>/<seq>`) and the\n * host context. A failure rejects that call in the script with the message and records it as an\n * `error` without executing it. Host decorators around the `ToolExecutor` (outside\n * `ResolvedToolSet.execute`) never see nested calls: put per-call run-authority checks here or in\n * registration-level wrappers.\n */\n readonly beforeNestedCall?: (input: {\n readonly call: ToolCall\n readonly context: Context\n }) => Effect.Effect<void, string>\n /**\n * Runs after each nested call that `beforeNestedCall` admitted, with its outcome: `success` (a\n * result without `isError`), `failure` (an error result or an unexpected failure), or\n * `interrupted` (cancelled, for example still running when the script ended). `durationMs`\n * covers the execution; `result` is present when the call produced one. Calls rejected by\n * `beforeNestedCall` or the call limit never ran and are not reported. The script waits for the\n * hook before it sees the call settle. A failing hook (a failure, a defect, or a synchronous\n * throw) is logged and never changes the call's result.\n */\n readonly afterNestedCall?: (input: CodeModeAfterNestedCallInput<Context>) => Effect.Effect<void>\n}\n\n/** Outcome of a nested call for `afterNestedCall`. */\nexport type CodeModeNestedCallOutcome = 'success' | 'failure' | 'interrupted'\n\nexport type CodeModeAfterNestedCallInput<Context> = {\n readonly call: ToolCall\n readonly outcome: CodeModeNestedCallOutcome\n readonly durationMs: number\n readonly context: Context\n readonly result?: ToolResult\n}\n\nconst CodeModeParams = Schema.Struct({\n code: Schema.String.annotate({\n description:\n 'Raw JavaScript: the body of an async function. Top-level await and return work. Not a function declaration and not a module.'\n })\n})\n\ntype CodeModeParams = typeof CodeModeParams.Type\n\nclass CodeModeExecutorRejected extends Data.TaggedError('CodeModeExecutorRejected')<{\n readonly message: string\n}> {}\n\nconst errorMessage = (error: unknown) =>\n error instanceof Error ? error.message : Predicate.isString(error) ? error : 'unknown error'\n\nconst nestedText = (content: Content) =>\n Predicate.isString(content)\n ? content\n : content\n .map(contentPartText)\n .filter(text => text.length > 0)\n .join('\\n')\n\ntype NestedResolution =\n | { readonly ok: true; readonly value: unknown }\n | { readonly ok: false; readonly message: string }\n\n/** What a nested call resolves to: `structuredContent` for tools with an output schema (when\n * present), text content otherwise; error results reject with their text. A tool that declares an\n * output schema but returns no `structuredContent` resolves to its text: that is a tool bug, kept\n * readable rather than turned into a failure.\n */\nconst resolveNestedResult = (tool: CodeModeCatalogTool, result: ToolResult): NestedResolution => {\n const text = nestedText(result.content)\n\n if (result.isError === true) {\n return { ok: false, message: text.length > 0 ? text : `Tool ${tool.name} failed.` }\n }\n\n return tool.structured && result.structuredContent !== undefined\n ? { ok: true, value: result.structuredContent }\n : { ok: true, value: text }\n}\n\ntype CallOutcome = {\n status: NestedToolCallStatus\n durationMs: number\n error?: string\n usage?: AgentUsage\n}\n\ntype CallRecord = {\n readonly name: string\n readonly args: unknown\n readonly id: string\n outcome?: CallOutcome\n}\n\nconst recordedOutcome = (exit: Exit.Exit<ToolResult>, durationMs: number): CallOutcome => {\n if (Exit.isFailure(exit)) {\n return Cause.hasInterruptsOnly(exit.cause)\n ? { status: 'cancelled', durationMs }\n : { status: 'error', durationMs, error: 'The tool call failed unexpectedly.' }\n }\n\n const result = exit.value\n\n const outcome: CallOutcome =\n result.isError === true\n ? { status: 'error', durationMs, error: nestedText(result.content) }\n : { status: 'ok', durationMs }\n\n if (result.usage !== undefined) {\n outcome.usage = result.usage\n }\n\n return outcome\n}\n\nconst hookOutcome = (exit: Exit.Exit<ToolResult>): CodeModeNestedCallOutcome =>\n Exit.isSuccess(exit)\n ? exit.value.isError === true\n ? 'failure'\n : 'success'\n : Cause.hasInterruptsOnly(exit.cause)\n ? 'interrupted'\n : 'failure'\n\n/** What `describeNamespace(name)` resolves to. */\ntype CodeModeNamespaceDescription = {\n readonly name: string\n description?: string\n readonly tools: ReadonlyArray<{ readonly name: string; readonly description: string }>\n}\n\nconst SearchOptions = Schema.Struct({\n limit: Schema.optionalKey(Schema.Finite),\n namespace: Schema.optionalKey(Schema.String)\n})\n\nconst decodeSearchOptions = Schema.decodeUnknownOption(SearchOptions)\n\n/** `searchTools`, `describeTool`, and `describeNamespace` over every nested tool. */\nconst discoveryGlobals = (\n catalog: ReadonlyArray<CodeModeCatalogTool>\n): ReadonlyArray<CodeModeExecutorGlobal> => [\n {\n name: 'searchTools',\n spread: true,\n execute: args => {\n const [query, options] = Array.isArray(args) ? args : []\n\n return Promise.resolve(\n searchCodeModeTools(\n catalog,\n Predicate.isString(query) ? query : '',\n Option.getOrElse(decodeSearchOptions(options ?? {}), () => ({}))\n )\n )\n }\n },\n {\n name: 'describeTool',\n execute: name => {\n const tool = Predicate.isString(name) ? findCodeModeTool(catalog, name) : undefined\n\n return Promise.resolve(tool === undefined ? undefined : describeCodeModeTool(tool))\n }\n },\n {\n name: 'describeNamespace',\n execute: name => {\n const tools = catalog.filter(tool => tool.namespace === name)\n\n if (!Predicate.isString(name) || tools.length === 0) return Promise.resolve(undefined)\n\n const description = tools.find(\n tool => tool.namespaceDescription !== undefined\n )?.namespaceDescription\n\n const namespace: CodeModeNamespaceDescription = {\n name,\n tools: tools.map(tool => ({ name: tool.identifier, description: tool.description }))\n }\n\n if (description !== undefined) namespace.description = description\n\n return Promise.resolve(namespace)\n }\n }\n]\n\nconst hasStoreWrites = (result: CodeModeExecutionResult) =>\n result.storeWrites !== undefined &&\n (Object.keys(result.storeWrites.set).length > 0 || result.storeWrites.delete.length > 0)\n\nconst timeoutFor = (limit: number, deadline: number | undefined, now: number) =>\n deadline === undefined\n ? limit\n : Math.max(codeModeMinimumTimeoutMs, Math.min(limit, deadline - now - codeModeDeadlineMarginMs))\n\n/**\n * Waits at most `codeModeDeadlineMarginMs` for an effect (interrupting nested fibers) to finish; it\n * keeps running detached when a nested call does not respond to interruption in time.\n */\nconst boundedWait = (effect: Effect.Effect<void>) =>\n Effect.forkDetach(effect).pipe(\n Effect.flatMap(fiber =>\n Fiber.await(fiber).pipe(\n Effect.timeoutOption(Duration.millis(codeModeDeadlineMarginMs)),\n Effect.interruptible\n )\n ),\n Effect.asVoid\n )\n\ntype RunInput<Context> = {\n readonly options: MakeCodeModeToolOptions<Context>\n readonly limits: Required<CodeModeLimits>\n readonly call: ToolCall\n readonly context: Context\n readonly code: string\n readonly nested: NestedToolExecutor\n}\n\nconst runScript = <Context>(input: RunInput<Context>): Effect.Effect<ToolResult, ToolError> =>\n Effect.gen(function* () {\n const { call, limits, nested, options } = input\n const store = options.loadStore === undefined ? {} : yield* options.loadStore(input.context)\n const startedAt = yield* Clock.currentTimeMillis\n const timeoutMs = timeoutFor(limits.timeoutMs, options.deadline?.(input.context), startedAt)\n const catalog = codeModeCatalog(nested.tools)\n\n return yield* Effect.acquireUseRelease(\n Scope.make(),\n scope =>\n Effect.gen(function* () {\n // Nested calls run as fibers of this tool call (same services, interrupted with it).\n const fibers = yield* FiberSet.make<ToolResult>().pipe(Scope.provide(scope))\n const runFork = yield* FiberSet.runtime(fibers)<never>()\n const records: Array<CallRecord> = []\n\n const callTool =\n (tool: CodeModeCatalogTool): CodeModeExecutorTool['execute'] =>\n (args, { signal }) => {\n if (signal.aborted) {\n return Promise.reject(new Error(`${tool.callLabel} was cancelled.`))\n }\n\n if (records.length >= limits.maxNestedCalls) {\n return Promise.reject(\n new Error(\n `${tool.callLabel}: Nested call limit reached: a script may make at most ${limits.maxNestedCalls} tool calls. Batch the work or return partial results.`\n )\n )\n }\n\n const params = args === undefined ? {} : args\n\n const record: CallRecord = {\n id: `${call.id}/${records.length + 1}`,\n name: tool.name,\n args: params\n }\n\n records.push(record)\n\n const nestedCall = ToolCall.make({ id: record.id, name: tool.name, params })\n const afterNestedCall = options.afterNestedCall\n\n const run =\n afterNestedCall === undefined\n ? nested.execute(nestedCall)\n : Effect.gen(function* () {\n const started = yield* Clock.currentTimeMillis\n\n // Host observability never changes the call: a hook failure, defects and\n // synchronous throws included, is only logged.\n const afterNestedCallSafely = (\n hookInput: CodeModeAfterNestedCallInput<Context>\n ) =>\n Effect.suspend(() => afterNestedCall(hookInput)).pipe(\n Effect.exit,\n Effect.flatMap(hookExit =>\n Exit.isSuccess(hookExit)\n ? Effect.void\n : Effect.logWarning(\n `Code mode afterNestedCall failed for ${nestedCall.id}; ignored: ${Cause.pretty(hookExit.cause)}`\n )\n )\n )\n\n return yield* nested.execute(nestedCall).pipe(\n Effect.onExit(exit =>\n Effect.flatMap(Clock.currentTimeMillis, finished =>\n afterNestedCallSafely(\n Exit.isSuccess(exit)\n ? {\n call: nestedCall,\n outcome: hookOutcome(exit),\n durationMs: finished - started,\n context: input.context,\n result: exit.value\n }\n : {\n call: nestedCall,\n outcome: hookOutcome(exit),\n durationMs: finished - started,\n context: input.context\n }\n )\n )\n )\n )\n })\n\n const admitted =\n options.beforeNestedCall === undefined\n ? run\n : options.beforeNestedCall({ call: nestedCall, context: input.context }).pipe(\n Effect.matchEffect({\n onFailure: message =>\n Effect.succeed(\n ToolResult.make({\n toolCallId: nestedCall.id,\n content: message,\n isError: true\n })\n ),\n onSuccess: () => run\n })\n )\n\n const execution = Effect.gen(function* () {\n const started = yield* Clock.currentTimeMillis\n\n return yield* admitted.pipe(\n Effect.onExit(exit =>\n Effect.map(Clock.currentTimeMillis, finished => {\n record.outcome = recordedOutcome(exit, finished - started)\n })\n )\n )\n })\n\n return new Promise((resolve, reject) => {\n runFork(execution, { signal }).addObserver(exit => {\n if (Exit.isFailure(exit)) {\n reject(\n new Error(\n Cause.hasInterruptsOnly(exit.cause)\n ? `${tool.callLabel} was cancelled.`\n : `${tool.callLabel} failed unexpectedly.`\n )\n )\n\n return\n }\n\n const resolution = resolveNestedResult(tool, exit.value)\n\n if (resolution.ok) {\n resolve(resolution.value)\n } else {\n // The script sees which call failed; the nested-call record keeps the raw text.\n reject(new Error(`${tool.callLabel}: ${resolution.message}`))\n }\n })\n })\n }\n\n const tools: ReadonlyArray<CodeModeExecutorTool> = catalog.map(tool => ({\n name: tool.name,\n description: tool.description,\n inputSchema: tool.inputSchema,\n outputSchema: tool.outputSchema,\n callLabel: tool.callLabel,\n execute: callTool(tool)\n }))\n\n const result = yield* Effect.tryPromise({\n try: signal =>\n options.executor.execute(input.code, {\n tools,\n globals: discoveryGlobals(catalog),\n timeoutMs,\n memoryLimitBytes: limits.memoryLimitBytes,\n store,\n signal\n }),\n catch: error => new CodeModeExecutorRejected({ message: errorMessage(error) })\n }).pipe(\n Effect.catch(error =>\n Effect.succeed<CodeModeExecutionResult>({\n ok: false,\n error: {\n kind: 'sandbox',\n message: `The code mode executor failed: ${error.message}`\n },\n output: []\n })\n ),\n // Backstop for executors that miss their own deadline: interrupting aborts the signal.\n Effect.timeoutOption(Duration.millis(timeoutMs + codeModeDeadlineMarginMs)),\n Effect.map(\n Option.getOrElse((): CodeModeExecutionResult => ({\n ok: false,\n error: {\n kind: 'timeout',\n message: `Execution timed out after ${timeoutMs} ms (the executor did not stop in time)`\n },\n output: []\n }))\n )\n )\n\n // Calls still running when the script ended are cancelled and recorded as such.\n yield* boundedWait(FiberSet.clear(fibers))\n\n const finishedAt = yield* Clock.currentTimeMillis\n\n const recorded = records.map((record): NestedToolCallInput => ({\n id: record.id,\n name: record.name,\n args: record.args,\n // A call interrupted before it started has no outcome.\n ...(record.outcome ?? { status: 'cancelled' })\n }))\n\n const recorder = recorded.reduce(\n recordNestedToolCall,\n makeNestedToolCallRecorder({ maxCalls: limits.maxNestedCalls })\n )\n\n const { nestedCalls, usage } = nestedToolCallResultFields(recorder)\n\n const content = codeModeSegmentsContent(\n codeModeResultSegments({\n result,\n wallTimeMs: finishedAt - startedAt,\n calls: recorded,\n maxChars: limits.maxOutputChars,\n maxImages: limits.maxImages,\n maxImageBytes: limits.maxImageBytes\n })\n )\n\n const structuredContent: CodeModeStructuredContent = {\n codemode:\n result.ok && result.storeWrites !== undefined && hasStoreWrites(result)\n ? { ok: true, storeWrites: result.storeWrites }\n : { ok: result.ok }\n }\n\n type ResultFields = {\n toolCallId: string\n content: Content\n isError?: boolean\n structuredContent: CodeModeStructuredContent\n nestedCalls: typeof nestedCalls\n usage?: AgentUsage\n }\n\n const fields: ResultFields = {\n toolCallId: call.id,\n content,\n structuredContent,\n nestedCalls\n }\n\n if (!result.ok) {\n fields.isError = true\n }\n\n if (usage !== undefined) {\n fields.usage = usage\n }\n\n return ToolResult.make(fields)\n }),\n (scope, exit) => boundedWait(Scope.close(scope, exit))\n )\n })\n\nconst argsPreviewChars = 200\n\nconst interruptedCallStatus = (entry: ToolLedgerEntry): CodeModeInterruptedCallStatus => {\n const outcome = entry.outcome\n\n if (outcome === undefined) return 'unknown'\n\n if (Predicate.isTagged(outcome, 'Failed')) return 'failed'\n\n return outcome.result.isError === true ? 'failed' : 'applied'\n}\n\nconst nestedEntryState = (entry: ToolLedgerEntry) => {\n const outcome = entry.outcome\n\n if (outcome === undefined) {\n return 'unknown: it started but never recorded a result, so it may have been applied'\n }\n\n if (Predicate.isTagged(outcome, 'Failed'))\n return `completed with an error: ${truncateCodePoints(outcome.error.message, nestedToolCallMaxErrorChars)}`\n\n return outcome.result.isError === true ? 'completed with an error result' : 'applied'\n}\n\n/** The bounded, wire-safe `interruptedCalls` of an abandoned execution's nested ledger entries:\n * the `nestedCalls` record bounds (`maxCalls`, 8 KiB of arguments per call, 32 KiB in total).\n */\nconst interruptedCalls = (\n nested: ReadonlyArray<ToolLedgerEntry>,\n maxCalls: number\n): CodeModeInterruptedCalls => {\n const counts = { applied: 0, failed: 0, unknown: 0 }\n const calls: Array<CodeModeInterruptedCall> = []\n let argsBytes = 0\n let complete = true\n\n for (const entry of nested) {\n const status = interruptedCallStatus(entry)\n\n counts[status]++\n\n if (calls.length >= maxCalls) {\n complete = false\n continue\n }\n\n const { args, bytes, truncated } = boundNestedToolCallArgs(entry.args, argsBytes)\n\n // `argsTruncated`: the ledger already cut the arguments when the call was claimed.\n if (truncated || entry.argsTruncated) complete = false\n\n argsBytes += bytes\n calls.push({ key: entry.key, toolName: entry.toolName, args, status })\n }\n\n return { calls, complete, counts }\n}\n\n/**\n * The result of a code mode call whose earlier execution was abandoned (claimed, lease expired,\n * no result recorded). The script never runs again: the result lists that execution's ledgered\n * nested calls as applied, failed, or unknown, and says they were not undone. The text is bounded\n * by `maxOutputChars`; `structuredContent.codemode.interruptedCalls` carries the same entries for\n * hosts, bounded like `nestedCalls`. When the ledger cannot list them, the result stays\n * interrupted, says they may have been applied, and sets `interruptedCallsUnavailable` instead.\n */\nconst abandonedScriptResult =\n (limits: Required<CodeModeLimits>) =>\n ({ call, nested }: ToolLedgerAbandonedInput): ToolResult => {\n const maxChars = limits.maxOutputChars\n\n const listing =\n nested === undefined\n ? 'Its ledgered nested tool calls could not be listed (the tool ledger is unavailable), so any of them may already have been applied.'\n : nested.length === 0\n ? 'No ledgered nested tool calls were recorded for that execution.'\n : [\n `Ledgered nested tool calls of that execution (they were not undone):`,\n ...nested.map(\n entry =>\n `- ${entry.key} ${entry.toolName} ${truncateCodePoints(entry.args, argsPreviewChars)}: ${nestedEntryState(entry)}`\n )\n ].join('\\n')\n\n const text = [\n `Script interrupted: an earlier execution of this ${call.name} call (${call.id}) started but never recorded a result. The script was not run again.`,\n listing,\n 'Calls the ledger policy skips (by default read-only calls) are not listed. Verify the state these calls affect before repeating any work.'\n ].join('\\n\\n')\n\n const structuredContent: CodeModeStructuredContent = {\n codemode:\n nested === undefined\n ? { ok: false, interrupted: true, interruptedCallsUnavailable: true }\n : {\n ok: false,\n interrupted: true,\n interruptedCalls: interruptedCalls(\n nested,\n nestedToolCallRecordLimit(limits.maxNestedCalls)\n )\n }\n }\n\n return ToolResult.make({\n toolCallId: call.id,\n content: codeModeSegmentsContent(boundCodeModeSegments([{ type: 'text', text }], maxChars)),\n isError: true,\n structuredContent\n })\n }\n\n/**\n * The code mode tool: one registration (default name `codemode`, input `{ code }`) whose scripts\n * call the other code-mode-callable tools of the same `resolveTools` resolution. Nested calls run\n * through the resolved execute path with the same host context (input decoding, enablement,\n * registration wrappers), get ids `<toolCallId>/<seq>`, and are recorded on the result's\n * `nestedCalls`. The resolved description lists the callable tools (see\n * `renderCodeModeDescription`).\n *\n * Access is `write`: scripts can call any write tool the resolution exposes to code mode; each\n * nested call keeps its own access metadata and host wrappers.\n *\n * With a tool ledger (`resolveTools(..., { ledger })`) the code mode call itself is ledgered, so a\n * re-executed call never runs its script again: a completed call returns its stored result, an\n * in-flight one is awaited, and an abandoned one returns an interrupted result listing its\n * ledgered nested calls. Nested write calls are ledgered under `<toolCallId>/<seq>`.\n */\nexport const makeCodeModeTool = <Context>(\n options: MakeCodeModeToolOptions<Context>\n): ToolRegistration<Context> => {\n const name = options.name ?? codeModeToolName\n const limits: Required<CodeModeLimits> = { ...defaultCodeModeLimits, ...options.limits }\n const inlineBudget = options.inlineBudget ?? defaultCodeModeInlineBudget\n const store = options.loadStore !== undefined\n\n return makeTool<Context, typeof CodeModeParams>({\n name,\n description: renderCodeModeDescription({ tools: [], inlineBudget, store }),\n parameters: CodeModeParams,\n access: 'write',\n nestedToolAccess: true,\n describe: ({ tools }) => renderCodeModeDescription({ tools, inlineBudget, store }),\n abandonedResult: abandonedScriptResult(limits),\n execute: ({\n call,\n context,\n params,\n nested\n }: ToolExecutionInput<Context> & { readonly params: CodeModeParams }) =>\n nested === undefined\n ? Effect.fail(\n new ToolError({\n tool: name,\n cause: 'unavailable',\n message: `${name} requires nested tool access; resolve it with resolveTools.`\n })\n )\n : runScript({ options, limits, call, context, code: params.code, nested })\n })\n}\n"],"mappings":";;;;;;;;;;AAsEA,MAAa,mBAAmB;AAuBhC,MAAa,wBAAwB;CACnC,WAAW;CACX,kBAAkB,KAAK,OAAO;CAC9B,gBAAgB;CAChB,gBAAgB;CAChB,WAAA;CACA,eAAe;AACjB;;AAGA,MAAa,2BAA2B;;AAGxC,MAAa,2BAA2B;AAqDxC,MAAM,iBAAiB,OAAO,OAAO,EACnC,MAAM,OAAO,OAAO,SAAS,EAC3B,aACE,+HACJ,CAAC,EACH,CAAC;AAID,IAAM,2BAAN,cAAuC,KAAK,YAAY,0BAA0B,EAE/E,CAAC;AAEJ,MAAM,gBAAgB,UACpB,iBAAiB,QAAQ,MAAM,UAAU,UAAU,SAAS,KAAK,IAAI,QAAQ;AAE/E,MAAM,cAAc,YAClB,UAAU,SAAS,OAAO,IACtB,UACA,QACG,IAAI,eAAe,EACnB,QAAO,SAAQ,KAAK,SAAS,CAAC,EAC9B,KAAK,IAAI;;;;;;AAWlB,MAAM,uBAAuB,MAA2B,WAAyC;CAC/F,MAAM,OAAO,WAAW,OAAO,OAAO;CAEtC,IAAI,OAAO,YAAY,MACrB,OAAO;EAAE,IAAI;EAAO,SAAS,KAAK,SAAS,IAAI,OAAO,QAAQ,KAAK,KAAK;CAAU;CAGpF,OAAO,KAAK,cAAc,OAAO,sBAAsB,KAAA,IACnD;EAAE,IAAI;EAAM,OAAO,OAAO;CAAkB,IAC5C;EAAE,IAAI;EAAM,OAAO;CAAK;AAC9B;AAgBA,MAAM,mBAAmB,MAA6B,eAAoC;CACxF,IAAI,KAAK,UAAU,IAAI,GACrB,OAAO,MAAM,kBAAkB,KAAK,KAAK,IACrC;EAAE,QAAQ;EAAa;CAAW,IAClC;EAAE,QAAQ;EAAS;EAAY,OAAO;CAAqC;CAGjF,MAAM,SAAS,KAAK;CAEpB,MAAM,UACJ,OAAO,YAAY,OACf;EAAE,QAAQ;EAAS;EAAY,OAAO,WAAW,OAAO,OAAO;CAAE,IACjE;EAAE,QAAQ;EAAM;CAAW;CAEjC,IAAI,OAAO,UAAU,KAAA,GACnB,QAAQ,QAAQ,OAAO;CAGzB,OAAO;AACT;AAEA,MAAM,eAAe,SACnB,KAAK,UAAU,IAAI,IACf,KAAK,MAAM,YAAY,OACrB,YACA,YACF,MAAM,kBAAkB,KAAK,KAAK,IAChC,gBACA;AASR,MAAM,gBAAgB,OAAO,OAAO;CAClC,OAAO,OAAO,YAAY,OAAO,MAAM;CACvC,WAAW,OAAO,YAAY,OAAO,MAAM;AAC7C,CAAC;AAED,MAAM,sBAAsB,OAAO,oBAAoB,aAAa;;AAGpE,MAAM,oBACJ,YAC0C;CAC1C;EACE,MAAM;EACN,QAAQ;EACR,UAAS,SAAQ;GACf,MAAM,CAAC,OAAO,WAAW,MAAM,QAAQ,IAAI,IAAI,OAAO,CAAC;GAEvD,OAAO,QAAQ,QACb,oBACE,SACA,UAAU,SAAS,KAAK,IAAI,QAAQ,IACpC,OAAO,UAAU,oBAAoB,WAAW,CAAC,CAAC,UAAU,CAAC,EAAE,CACjE,CACF;EACF;CACF;CACA;EACE,MAAM;EACN,UAAS,SAAQ;GACf,MAAM,OAAO,UAAU,SAAS,IAAI,IAAI,iBAAiB,SAAS,IAAI,IAAI,KAAA;GAE1E,OAAO,QAAQ,QAAQ,SAAS,KAAA,IAAY,KAAA,IAAY,qBAAqB,IAAI,CAAC;EACpF;CACF;CACA;EACE,MAAM;EACN,UAAS,SAAQ;GACf,MAAM,QAAQ,QAAQ,QAAO,SAAQ,KAAK,cAAc,IAAI;GAE5D,IAAI,CAAC,UAAU,SAAS,IAAI,KAAK,MAAM,WAAW,GAAG,OAAO,QAAQ,QAAQ,KAAA,CAAS;GAErF,MAAM,cAAc,MAAM,MACxB,SAAQ,KAAK,yBAAyB,KAAA,CACxC,GAAG;GAEH,MAAM,YAA0C;IAC9C;IACA,OAAO,MAAM,KAAI,UAAS;KAAE,MAAM,KAAK;KAAY,aAAa,KAAK;IAAY,EAAE;GACrF;GAEA,IAAI,gBAAgB,KAAA,GAAW,UAAU,cAAc;GAEvD,OAAO,QAAQ,QAAQ,SAAS;EAClC;CACF;AACF;AAEA,MAAM,kBAAkB,WACtB,OAAO,gBAAgB,KAAA,MACtB,OAAO,KAAK,OAAO,YAAY,GAAG,EAAE,SAAS,KAAK,OAAO,YAAY,OAAO,SAAS;AAExF,MAAM,cAAc,OAAe,UAA8B,QAC/D,aAAa,KAAA,IACT,QACA,KAAK,IAAI,0BAA0B,KAAK,IAAI,OAAO,WAAW,MAAM,wBAAwB,CAAC;;;;;AAMnG,MAAM,eAAe,WACnB,OAAO,WAAW,MAAM,EAAE,KACxB,OAAO,SAAQ,UACb,MAAM,MAAM,KAAK,EAAE,KACjB,OAAO,cAAc,SAAS,OAAO,wBAAwB,CAAC,GAC9D,OAAO,aACT,CACF,GACA,OAAO,MACT;AAWF,MAAM,aAAsB,UAC1B,OAAO,IAAI,aAAa;CACtB,MAAM,EAAE,MAAM,QAAQ,QAAQ,YAAY;CAC1C,MAAM,QAAQ,QAAQ,cAAc,KAAA,IAAY,CAAC,IAAI,OAAO,QAAQ,UAAU,MAAM,OAAO;CAC3F,MAAM,YAAY,OAAO,MAAM;CAC/B,MAAM,YAAY,WAAW,OAAO,WAAW,QAAQ,WAAW,MAAM,OAAO,GAAG,SAAS;CAC3F,MAAM,UAAU,gBAAgB,OAAO,KAAK;CAE5C,OAAO,OAAO,OAAO,kBACnB,MAAM,KAAK,IACX,UACE,OAAO,IAAI,aAAa;EAEtB,MAAM,SAAS,OAAO,SAAS,KAAiB,EAAE,KAAK,MAAM,QAAQ,KAAK,CAAC;EAC3E,MAAM,UAAU,OAAO,SAAS,QAAQ,MAAM,EAAS;EACvD,MAAM,UAA6B,CAAC;EAEpC,MAAM,YACH,UACA,MAAM,EAAE,aAAa;GACpB,IAAI,OAAO,SACT,OAAO,QAAQ,uBAAO,IAAI,MAAM,GAAG,KAAK,UAAU,gBAAgB,CAAC;GAGrE,IAAI,QAAQ,UAAU,OAAO,gBAC3B,OAAO,QAAQ,uBACb,IAAI,MACF,GAAG,KAAK,UAAU,yDAAyD,OAAO,eAAe,uDACnG,CACF;GAGF,MAAM,SAAS,SAAS,KAAA,IAAY,CAAC,IAAI;GAEzC,MAAM,SAAqB;IACzB,IAAI,GAAG,KAAK,GAAG,GAAG,QAAQ,SAAS;IACnC,MAAM,KAAK;IACX,MAAM;GACR;GAEA,QAAQ,KAAK,MAAM;GAEnB,MAAM,aAAa,SAAS,KAAK;IAAE,IAAI,OAAO;IAAI,MAAM,KAAK;IAAM;GAAO,CAAC;GAC3E,MAAM,kBAAkB,QAAQ;GAEhC,MAAM,MACJ,oBAAoB,KAAA,IAChB,OAAO,QAAQ,UAAU,IACzB,OAAO,IAAI,aAAa;IACtB,MAAM,UAAU,OAAO,MAAM;IAI7B,MAAM,yBACJ,cAEA,OAAO,cAAc,gBAAgB,SAAS,CAAC,EAAE,KAC/C,OAAO,MACP,OAAO,SAAQ,aACb,KAAK,UAAU,QAAQ,IACnB,OAAO,OACP,OAAO,WACL,wCAAwC,WAAW,GAAG,aAAa,MAAM,OAAO,SAAS,KAAK,GAChG,CACN,CACF;IAEF,OAAO,OAAO,OAAO,QAAQ,UAAU,EAAE,KACvC,OAAO,QAAO,SACZ,OAAO,QAAQ,MAAM,oBAAmB,aACtC,sBACE,KAAK,UAAU,IAAI,IACf;KACE,MAAM;KACN,SAAS,YAAY,IAAI;KACzB,YAAY,WAAW;KACvB,SAAS,MAAM;KACf,QAAQ,KAAK;IACf,IACA;KACE,MAAM;KACN,SAAS,YAAY,IAAI;KACzB,YAAY,WAAW;KACvB,SAAS,MAAM;IACjB,CACN,CACF,CACF,CACF;GACF,CAAC;GAEP,MAAM,WACJ,QAAQ,qBAAqB,KAAA,IACzB,MACA,QAAQ,iBAAiB;IAAE,MAAM;IAAY,SAAS,MAAM;GAAQ,CAAC,EAAE,KACrE,OAAO,YAAY;IACjB,YAAW,YACT,OAAO,QACL,WAAW,KAAK;KACd,YAAY,WAAW;KACvB,SAAS;KACT,SAAS;IACX,CAAC,CACH;IACF,iBAAiB;GACnB,CAAC,CACH;GAEN,MAAM,YAAY,OAAO,IAAI,aAAa;IACxC,MAAM,UAAU,OAAO,MAAM;IAE7B,OAAO,OAAO,SAAS,KACrB,OAAO,QAAO,SACZ,OAAO,IAAI,MAAM,oBAAmB,aAAY;KAC9C,OAAO,UAAU,gBAAgB,MAAM,WAAW,OAAO;IAC3D,CAAC,CACH,CACF;GACF,CAAC;GAED,OAAO,IAAI,SAAS,SAAS,WAAW;IACtC,QAAQ,WAAW,EAAE,OAAO,CAAC,EAAE,aAAY,SAAQ;KACjD,IAAI,KAAK,UAAU,IAAI,GAAG;MACxB,uBACE,IAAI,MACF,MAAM,kBAAkB,KAAK,KAAK,IAC9B,GAAG,KAAK,UAAU,mBAClB,GAAG,KAAK,UAAU,sBACxB,CACF;MAEA;KACF;KAEA,MAAM,aAAa,oBAAoB,MAAM,KAAK,KAAK;KAEvD,IAAI,WAAW,IACb,QAAQ,WAAW,KAAK;UAGxB,uBAAO,IAAI,MAAM,GAAG,KAAK,UAAU,IAAI,WAAW,SAAS,CAAC;IAEhE,CAAC;GACH,CAAC;EACH;EAEF,MAAM,QAA6C,QAAQ,KAAI,UAAS;GACtE,MAAM,KAAK;GACX,aAAa,KAAK;GAClB,aAAa,KAAK;GAClB,cAAc,KAAK;GACnB,WAAW,KAAK;GAChB,SAAS,SAAS,IAAI;EACxB,EAAE;EAEF,MAAM,SAAS,OAAO,OAAO,WAAW;GACtC,MAAK,WACH,QAAQ,SAAS,QAAQ,MAAM,MAAM;IACnC;IACA,SAAS,iBAAiB,OAAO;IACjC;IACA,kBAAkB,OAAO;IACzB;IACA;GACF,CAAC;GACH,QAAO,UAAS,IAAI,yBAAyB,EAAE,SAAS,aAAa,KAAK,EAAE,CAAC;EAC/E,CAAC,EAAE,KACD,OAAO,OAAM,UACX,OAAO,QAAiC;GACtC,IAAI;GACJ,OAAO;IACL,MAAM;IACN,SAAS,kCAAkC,MAAM;GACnD;GACA,QAAQ,CAAC;EACX,CAAC,CACH,GAEA,OAAO,cAAc,SAAS,OAAO,YAAY,wBAAwB,CAAC,GAC1E,OAAO,IACL,OAAO,iBAA0C;GAC/C,IAAI;GACJ,OAAO;IACL,MAAM;IACN,SAAS,6BAA6B,UAAU;GAClD;GACA,QAAQ,CAAC;EACX,EAAE,CACJ,CACF;EAGA,OAAO,YAAY,SAAS,MAAM,MAAM,CAAC;EAEzC,MAAM,aAAa,OAAO,MAAM;EAEhC,MAAM,WAAW,QAAQ,KAAK,YAAiC;GAC7D,IAAI,OAAO;GACX,MAAM,OAAO;GACb,MAAM,OAAO;GAEb,GAAI,OAAO,WAAW,EAAE,QAAQ,YAAY;EAC9C,EAAE;EAOF,MAAM,EAAE,aAAa,UAAU,2BALd,SAAS,OACxB,sBACA,2BAA2B,EAAE,UAAU,OAAO,eAAe,CAAC,CAGC,CAAC;EAElE,MAAM,UAAU,wBACd,uBAAuB;GACrB;GACA,YAAY,aAAa;GACzB,OAAO;GACP,UAAU,OAAO;GACjB,WAAW,OAAO;GAClB,eAAe,OAAO;EACxB,CAAC,CACH;EAEA,MAAM,oBAA+C,EACnD,UACE,OAAO,MAAM,OAAO,gBAAgB,KAAA,KAAa,eAAe,MAAM,IAClE;GAAE,IAAI;GAAM,aAAa,OAAO;EAAY,IAC5C,EAAE,IAAI,OAAO,GAAG,EACxB;EAWA,MAAM,SAAuB;GAC3B,YAAY,KAAK;GACjB;GACA;GACA;EACF;EAEA,IAAI,CAAC,OAAO,IACV,OAAO,UAAU;EAGnB,IAAI,UAAU,KAAA,GACZ,OAAO,QAAQ;EAGjB,OAAO,WAAW,KAAK,MAAM;CAC/B,CAAC,IACF,OAAO,SAAS,YAAY,MAAM,MAAM,OAAO,IAAI,CAAC,CACvD;AACF,CAAC;AAEH,MAAM,mBAAmB;AAEzB,MAAM,yBAAyB,UAA0D;CACvF,MAAM,UAAU,MAAM;CAEtB,IAAI,YAAY,KAAA,GAAW,OAAO;CAElC,IAAI,UAAU,SAAS,SAAS,QAAQ,GAAG,OAAO;CAElD,OAAO,QAAQ,OAAO,YAAY,OAAO,WAAW;AACtD;AAEA,MAAM,oBAAoB,UAA2B;CACnD,MAAM,UAAU,MAAM;CAEtB,IAAI,YAAY,KAAA,GACd,OAAO;CAGT,IAAI,UAAU,SAAS,SAAS,QAAQ,GACtC,OAAO,4BAA4B,mBAAmB,QAAQ,MAAM,SAAS,2BAA2B;CAE1G,OAAO,QAAQ,OAAO,YAAY,OAAO,mCAAmC;AAC9E;;;;AAKA,MAAM,oBACJ,QACA,aAC6B;CAC7B,MAAM,SAAS;EAAE,SAAS;EAAG,QAAQ;EAAG,SAAS;CAAE;CACnD,MAAM,QAAwC,CAAC;CAC/C,IAAI,YAAY;CAChB,IAAI,WAAW;CAEf,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,SAAS,sBAAsB,KAAK;EAE1C,OAAO;EAEP,IAAI,MAAM,UAAU,UAAU;GAC5B,WAAW;GACX;EACF;EAEA,MAAM,EAAE,MAAM,OAAO,cAAc,wBAAwB,MAAM,MAAM,SAAS;EAGhF,IAAI,aAAa,MAAM,eAAe,WAAW;EAEjD,aAAa;EACb,MAAM,KAAK;GAAE,KAAK,MAAM;GAAK,UAAU,MAAM;GAAU;GAAM;EAAO,CAAC;CACvE;CAEA,OAAO;EAAE;EAAO;EAAU;CAAO;AACnC;;;;;;;;;AAUA,MAAM,yBACH,YACA,EAAE,MAAM,aAAmD;CAC1D,MAAM,WAAW,OAAO;CAExB,MAAM,UACJ,WAAW,KAAA,IACP,uIACA,OAAO,WAAW,IAChB,oEACA,CACE,wEACA,GAAG,OAAO,KACR,UACE,KAAK,MAAM,IAAI,GAAG,MAAM,SAAS,GAAG,mBAAmB,MAAM,MAAM,gBAAgB,EAAE,IAAI,iBAAiB,KAAK,GACnH,CACF,EAAE,KAAK,IAAI;CAEnB,MAAM,OAAO;EACX,oDAAoD,KAAK,KAAK,SAAS,KAAK,GAAG;EAC/E;EACA;CACF,EAAE,KAAK,MAAM;CAEb,MAAM,oBAA+C,EACnD,UACE,WAAW,KAAA,IACP;EAAE,IAAI;EAAO,aAAa;EAAM,6BAA6B;CAAK,IAClE;EACE,IAAI;EACJ,aAAa;EACb,kBAAkB,iBAChB,QACA,0BAA0B,OAAO,cAAc,CACjD;CACF,EACR;CAEA,OAAO,WAAW,KAAK;EACrB,YAAY,KAAK;EACjB,SAAS,wBAAwB,sBAAsB,CAAC;GAAE,MAAM;GAAQ;EAAK,CAAC,GAAG,QAAQ,CAAC;EAC1F,SAAS;EACT;CACF,CAAC;AACH;;;;;;;;;;;;;;;;;AAkBF,MAAa,oBACX,YAC8B;CAC9B,MAAM,OAAO,QAAQ,QAAA;CACrB,MAAM,SAAmC;EAAE,GAAG;EAAuB,GAAG,QAAQ;CAAO;CACvF,MAAM,eAAe,QAAQ,gBAAA;CAC7B,MAAM,QAAQ,QAAQ,cAAc,KAAA;CAEpC,OAAO,SAAyC;EAC9C;EACA,aAAa,0BAA0B;GAAE,OAAO,CAAC;GAAG;GAAc;EAAM,CAAC;EACzE,YAAY;EACZ,QAAQ;EACR,kBAAkB;EAClB,WAAW,EAAE,YAAY,0BAA0B;GAAE;GAAO;GAAc;EAAM,CAAC;EACjF,iBAAiB,sBAAsB,MAAM;EAC7C,UAAU,EACR,MACA,SACA,QACA,aAEA,WAAW,KAAA,IACP,OAAO,KACL,IAAI,UAAU;GACZ,MAAM;GACN,OAAO;GACP,SAAS,GAAG,KAAK;EACnB,CAAC,CACH,IACA,UAAU;GAAE;GAAS;GAAQ;GAAM;GAAS,MAAM,OAAO;GAAM;EAAO,CAAC;CAC/E,CAAC;AACH"}
1
+ {"version":3,"file":"tool.mjs","names":[],"sources":["../src/tool.ts"],"sourcesContent":["import {\n Cause,\n Clock,\n Data,\n Duration,\n Effect,\n Exit,\n Fiber,\n FiberSet,\n Option,\n Predicate,\n Result,\n Scope\n} from 'effect'\nimport * as Schema from 'effect/Schema'\nimport { ToolError } from '@yolk-sdk/agent/loop'\nimport {\n boundNestedToolCallArgs,\n makeNestedToolCallRecorder,\n nestedToolCallMaxErrorChars,\n nestedToolCallRecordLimit,\n nestedToolCallResultFields,\n recordNestedToolCall,\n truncateCodePoints,\n contentPartText,\n ToolCall,\n ToolResult,\n type AgentUsage,\n type Content,\n type NestedToolCallInput,\n type NestedToolCallStatus\n} from '@yolk-sdk/agent/protocol'\nimport {\n makeTool,\n type NestedToolExecutor,\n type StagedCallReceipt,\n type ToolExecutionInput,\n type ToolLedgerAbandonedInput,\n type ToolLedgerEntry,\n type ToolPlanBuilder,\n type ToolPlanStageError,\n type ToolRegistration\n} from '@yolk-sdk/agent/tools'\nimport {\n codeModeCatalog,\n defaultCodeModeInlineBudget,\n describeCodeModeTool,\n findCodeModeTool,\n renderCodeModeDescription,\n type CodeModeCatalogTool\n} from './catalog.ts'\nimport type {\n CodeModeExecutionResult,\n CodeModeExecutor,\n CodeModeExecutorGlobal,\n CodeModeExecutorTool,\n CodeModeStore\n} from './executor.ts'\nimport {\n boundCodeModeSegments,\n codeModeResultSegments,\n codeModeSegmentsContent,\n defaultCodeModeMaxImageBytes,\n defaultCodeModeMaxImages\n} from './output.ts'\nimport { searchCodeModeTools } from './search.ts'\nimport type {\n CodeModeInterruptedCall,\n CodeModeInterruptedCalls,\n CodeModeInterruptedCallStatus,\n CodeModeStagedPlan,\n CodeModeStructuredContent\n} from './store.ts'\n\n/** Default name of the code mode tool. */\nexport const codeModeToolName = 'codemode'\n\n/** Host-owned limits of one script. */\nexport type CodeModeLimits = {\n /** Overall deadline including nested calls. Default 120000 (clamped by `deadline`). */\n readonly timeoutMs?: number\n /** VM heap cap. Default 64 MiB. */\n readonly memoryLimitBytes?: number\n /** Nested tool calls per script; further calls reject with an Error. Default 256. The\n * `nestedCalls` record keeps up to this many calls, at most `nestedToolCallMaxRecordedCalls`\n * (4096) whatever the limit (argument byte budgets still apply).\n */\n readonly maxNestedCalls?: number\n /** Model-visible result characters, cut head and tail with an omission marker. Default 40000. */\n readonly maxOutputChars?: number\n /** Images kept in the result, in order; later images are dropped with a note. Default 8. */\n readonly maxImages?: number\n /** Base64 characters of images kept in the result in total; an image that would exceed it is\n * dropped with a note. Default 4 MiB.\n */\n readonly maxImageBytes?: number\n}\n\nexport const defaultCodeModeLimits = {\n timeoutMs: 120_000,\n memoryLimitBytes: 64 * 1024 * 1024,\n maxNestedCalls: 256,\n maxOutputChars: 40_000,\n maxImages: defaultCodeModeMaxImages,\n maxImageBytes: defaultCodeModeMaxImageBytes\n} as const satisfies Required<CodeModeLimits>\n\n/** Kept between a host deadline and the script deadline, for result handling. */\nexport const codeModeDeadlineMarginMs = 5_000\n\n/** A deadline never clamps a script below this timeout. */\nexport const codeModeMinimumTimeoutMs = 1_000\n\nexport type MakeCodeModeToolOptions<Context> = {\n readonly executor: CodeModeExecutor\n /** Default `codemode`. */\n readonly name?: string\n /** Estimated tokens (four characters each) for listing nested tools. Default 3000. */\n readonly inlineBudget?: number\n readonly limits?: CodeModeLimits\n /**\n * Epoch milliseconds by which the tool call must end, for example the remaining function budget\n * of a Workflow step. The script timeout is clamped to it minus 5 s, but never below 1 s.\n */\n readonly deadline?: (context: Context) => number | undefined\n /**\n * The store scripts read with `load(key)`, usually rebuilt from the transcript with\n * `codeModeStoreFromToolResults`. Without it every script starts with an empty store.\n */\n readonly loadStore?: (context: Context) => Effect.Effect<CodeModeStore, ToolError>\n /**\n * Runs before each nested call executes, with the nested call (id `<toolCallId>/<seq>`) and the\n * host context. A failure rejects that call in the script with the message and records it as an\n * `error` without executing it. Host decorators around the `ToolExecutor` (outside\n * `ResolvedToolSet.execute`) never see nested calls: put per-call run-authority checks here or in\n * registration-level wrappers.\n */\n readonly beforeNestedCall?: (input: {\n readonly call: ToolCall\n readonly context: Context\n }) => Effect.Effect<void, string>\n /**\n * Runs after each nested call that `beforeNestedCall` admitted, with its outcome: `success` (a\n * result without `isError`), `failure` (an error result or an unexpected failure), or\n * `interrupted` (cancelled, for example still running when the script ended). `durationMs`\n * covers the execution; `result` is present when the call produced one. Calls rejected by\n * `beforeNestedCall` or the call limit never ran and are not reported. The script waits for the\n * hook before it sees the call settle. A failing hook (a failure, a defect, or a synchronous\n * throw) is logged and never changes the call's result.\n */\n readonly afterNestedCall?: (input: CodeModeAfterNestedCallInput<Context>) => Effect.Effect<void>\n /**\n * Staged tool plans (ADR 0005). When the resolution offers staging (`resolveTools(..., { plans,\n * interactionHost })` with a plan review tool), scripts get `stage(name, args)` for stageable\n * approval-gated tools. `false` turns it off for this tool; `maxCalls` may only lower the\n * resolution's cap.\n */\n readonly staging?: CodeModeStagingOptions | false\n}\n\nexport type CodeModeStagingOptions = {\n /** Staged calls per script; `stage` rejects past it. At most the resolution's `maxCalls`. */\n readonly maxCalls?: number\n}\n\n/** Outcome of a nested call for `afterNestedCall`. */\nexport type CodeModeNestedCallOutcome = 'success' | 'failure' | 'interrupted'\n\nexport type CodeModeAfterNestedCallInput<Context> = {\n readonly call: ToolCall\n readonly outcome: CodeModeNestedCallOutcome\n readonly durationMs: number\n readonly context: Context\n readonly result?: ToolResult\n}\n\nconst CodeModeParams = Schema.Struct({\n code: Schema.String.annotate({\n description:\n 'Raw JavaScript: the body of an async function. Top-level await and return work. Not a function declaration and not a module.'\n })\n})\n\ntype CodeModeParams = typeof CodeModeParams.Type\n\nclass CodeModeExecutorRejected extends Data.TaggedError('CodeModeExecutorRejected')<{\n readonly message: string\n}> {}\n\nconst errorMessage = (error: unknown) =>\n error instanceof Error ? error.message : Predicate.isString(error) ? error : 'unknown error'\n\nconst nestedText = (content: Content) =>\n Predicate.isString(content)\n ? content\n : content\n .map(contentPartText)\n .filter(text => text.length > 0)\n .join('\\n')\n\ntype NestedResolution =\n | { readonly ok: true; readonly value: unknown }\n | { readonly ok: false; readonly message: string }\n\n/** What a nested call resolves to: `structuredContent` for tools with an output schema (when\n * present), text content otherwise; error results reject with their text. A tool that declares an\n * output schema but returns no `structuredContent` resolves to its text: that is a tool bug, kept\n * readable rather than turned into a failure.\n */\nconst resolveNestedResult = (tool: CodeModeCatalogTool, result: ToolResult): NestedResolution => {\n const text = nestedText(result.content)\n\n if (result.isError === true) {\n return { ok: false, message: text.length > 0 ? text : `Tool ${tool.name} failed.` }\n }\n\n return tool.structured && result.structuredContent !== undefined\n ? { ok: true, value: result.structuredContent }\n : { ok: true, value: text }\n}\n\ntype CallOutcome = {\n status: NestedToolCallStatus\n durationMs: number\n error?: string\n usage?: AgentUsage\n}\n\ntype CallRecord = {\n readonly name: string\n readonly args: unknown\n readonly id: string\n outcome?: CallOutcome\n}\n\nconst recordedOutcome = (exit: Exit.Exit<ToolResult>, durationMs: number): CallOutcome => {\n if (Exit.isFailure(exit)) {\n return Cause.hasInterruptsOnly(exit.cause)\n ? { status: 'cancelled', durationMs }\n : { status: 'error', durationMs, error: 'The tool call failed unexpectedly.' }\n }\n\n const result = exit.value\n\n const outcome: CallOutcome =\n result.isError === true\n ? { status: 'error', durationMs, error: nestedText(result.content) }\n : { status: 'ok', durationMs }\n\n if (result.usage !== undefined) {\n outcome.usage = result.usage\n }\n\n return outcome\n}\n\nconst hookOutcome = (exit: Exit.Exit<ToolResult>): CodeModeNestedCallOutcome =>\n Exit.isSuccess(exit)\n ? exit.value.isError === true\n ? 'failure'\n : 'success'\n : Cause.hasInterruptsOnly(exit.cause)\n ? 'interrupted'\n : 'failure'\n\n/** What `describeNamespace(name)` resolves to. */\ntype CodeModeNamespaceDescription = {\n readonly name: string\n description?: string\n readonly tools: ReadonlyArray<{ readonly name: string; readonly description: string }>\n}\n\nconst SearchOptions = Schema.Struct({\n limit: Schema.optionalKey(Schema.Finite),\n namespace: Schema.optionalKey(Schema.String)\n})\n\nconst decodeSearchOptions = Schema.decodeUnknownOption(SearchOptions)\n\n/** `searchTools`, `describeTool`, and `describeNamespace` over every nested tool. */\nconst discoveryGlobals = (\n catalog: ReadonlyArray<CodeModeCatalogTool>\n): ReadonlyArray<CodeModeExecutorGlobal> => [\n {\n name: 'searchTools',\n spread: true,\n execute: args => {\n const [query, options] = Array.isArray(args) ? args : []\n\n return Promise.resolve(\n searchCodeModeTools(\n catalog,\n Predicate.isString(query) ? query : '',\n Option.getOrElse(decodeSearchOptions(options ?? {}), () => ({}))\n )\n )\n }\n },\n {\n name: 'describeTool',\n execute: name => {\n const tool = Predicate.isString(name) ? findCodeModeTool(catalog, name) : undefined\n\n return Promise.resolve(tool === undefined ? undefined : describeCodeModeTool(tool))\n }\n },\n {\n name: 'describeNamespace',\n execute: name => {\n const tools = catalog.filter(tool => tool.namespace === name)\n\n if (!Predicate.isString(name) || tools.length === 0) return Promise.resolve(undefined)\n\n const description = tools.find(\n tool => tool.namespaceDescription !== undefined\n )?.namespaceDescription\n\n const namespace: CodeModeNamespaceDescription = {\n name,\n tools: tools.map(tool => ({ name: tool.identifier, description: tool.description }))\n }\n\n if (description !== undefined) namespace.description = description\n\n return Promise.resolve(namespace)\n }\n }\n]\n\ntype StageFork = (\n effect: Effect.Effect<StagedCallReceipt, ToolPlanStageError>,\n options: { readonly signal: AbortSignal }\n) => Fiber.Fiber<StagedCallReceipt, ToolPlanStageError>\n\n/** `stage(name, args)`: records one call into the script's plan (never runs it). */\nconst stageGlobal = (plan: ToolPlanBuilder, fork: StageFork): CodeModeExecutorGlobal => ({\n name: 'stage',\n spread: true,\n signature: '(name: string, args: object): Promise<{ staged: true; key: string; index: number }>',\n description:\n 'Record a call of a stageable tool into this script’s plan for a person to review; it does not run.',\n execute: (args, { signal }) => {\n const [name, params] = Array.isArray(args) ? args : []\n\n if (signal.aborted) return Promise.reject(new Error('stage was cancelled.'))\n\n if (!Predicate.isString(name))\n return Promise.reject(new Error('stage: the first argument must be a tool name.'))\n\n return new Promise((resolve, reject) => {\n fork(plan.stage({ name, params: params === undefined ? {} : params }), {\n signal\n }).addObserver(exit => {\n if (Exit.isSuccess(exit)) {\n resolve(exit.value)\n\n return\n }\n\n const error = Cause.findErrorOption(exit.cause)\n\n reject(\n new Error(\n Option.isSome(error)\n ? `stage: ${error.value.message}`\n : Cause.hasInterruptsOnly(exit.cause)\n ? 'stage was cancelled.'\n : 'stage failed unexpectedly.'\n )\n )\n })\n })\n }\n})\n\nconst planNotice = (plan: CodeModeStagedPlan) =>\n `\\n\\nStaged ${plan.count} call${plan.count === 1 ? '' : 's'} as plan ${plan.id}; nothing was applied yet. To apply them, call ${plan.reviewToolName}(${JSON.stringify({ planId: plan.id, planDigest: plan.digest })}): a person reviews the plan and selects the calls to apply.`\n\nconst hasStoreWrites = (result: CodeModeExecutionResult) =>\n result.storeWrites !== undefined &&\n (Object.keys(result.storeWrites.set).length > 0 || result.storeWrites.delete.length > 0)\n\nconst timeoutFor = (limit: number, deadline: number | undefined, now: number) =>\n deadline === undefined\n ? limit\n : Math.max(codeModeMinimumTimeoutMs, Math.min(limit, deadline - now - codeModeDeadlineMarginMs))\n\n/**\n * Waits at most `codeModeDeadlineMarginMs` for an effect (interrupting nested fibers) to finish; it\n * keeps running detached when a nested call does not respond to interruption in time.\n */\nconst boundedWait = (effect: Effect.Effect<void>) =>\n Effect.forkDetach(effect).pipe(\n Effect.flatMap(fiber =>\n Fiber.await(fiber).pipe(\n Effect.timeoutOption(Duration.millis(codeModeDeadlineMarginMs)),\n Effect.interruptible\n )\n ),\n Effect.asVoid\n )\n\ntype RunInput<Context> = {\n readonly options: MakeCodeModeToolOptions<Context>\n readonly limits: Required<CodeModeLimits>\n readonly call: ToolCall\n readonly context: Context\n readonly code: string\n readonly nested: NestedToolExecutor\n}\n\nconst runScript = <Context>(input: RunInput<Context>): Effect.Effect<ToolResult, ToolError> =>\n Effect.gen(function* () {\n const { call, limits, nested, options } = input\n const store = options.loadStore === undefined ? {} : yield* options.loadStore(input.context)\n const startedAt = yield* Clock.currentTimeMillis\n const timeoutMs = timeoutFor(limits.timeoutMs, options.deadline?.(input.context), startedAt)\n const catalog = codeModeCatalog(nested.tools)\n\n return yield* Effect.acquireUseRelease(\n Scope.make(),\n scope =>\n Effect.gen(function* () {\n // Nested calls run as fibers of this tool call (same services, interrupted with it).\n const fibers = yield* FiberSet.make<ToolResult>().pipe(Scope.provide(scope))\n const runFork = yield* FiberSet.runtime(fibers)<never>()\n const records: Array<CallRecord> = []\n\n // Staged tool plans: only when the resolution offers staging and the host keeps it on.\n const stagingOptions = options.staging === false ? undefined : options.staging\n const staging = options.staging === false ? undefined : nested.staging\n const maxStagedCalls = stagingOptions?.maxCalls\n\n const plan =\n staging === undefined\n ? undefined\n : staging.begin(\n maxStagedCalls === undefined ? undefined : { maxCalls: maxStagedCalls }\n )\n\n const stageFibers = yield* FiberSet.make<StagedCallReceipt, ToolPlanStageError>().pipe(\n Scope.provide(scope)\n )\n\n const stageFork = yield* FiberSet.runtime(stageFibers)<never>()\n\n const callTool =\n (tool: CodeModeCatalogTool): CodeModeExecutorTool['execute'] =>\n (args, { signal }) => {\n if (signal.aborted) {\n return Promise.reject(new Error(`${tool.callLabel} was cancelled.`))\n }\n\n if (records.length >= limits.maxNestedCalls) {\n return Promise.reject(\n new Error(\n `${tool.callLabel}: Nested call limit reached: a script may make at most ${limits.maxNestedCalls} tool calls. Batch the work or return partial results.`\n )\n )\n }\n\n const params = args === undefined ? {} : args\n\n const record: CallRecord = {\n id: `${call.id}/${records.length + 1}`,\n name: tool.name,\n args: params\n }\n\n records.push(record)\n\n const nestedCall = ToolCall.make({ id: record.id, name: tool.name, params })\n const afterNestedCall = options.afterNestedCall\n\n const run =\n afterNestedCall === undefined\n ? nested.execute(nestedCall)\n : Effect.gen(function* () {\n const started = yield* Clock.currentTimeMillis\n\n // Host observability never changes the call: a hook failure, defects and\n // synchronous throws included, is only logged.\n const afterNestedCallSafely = (\n hookInput: CodeModeAfterNestedCallInput<Context>\n ) =>\n Effect.suspend(() => afterNestedCall(hookInput)).pipe(\n Effect.exit,\n Effect.flatMap(hookExit =>\n Exit.isSuccess(hookExit)\n ? Effect.void\n : Effect.logWarning(\n `Code mode afterNestedCall failed for ${nestedCall.id}; ignored: ${Cause.pretty(hookExit.cause)}`\n )\n )\n )\n\n return yield* nested.execute(nestedCall).pipe(\n Effect.onExit(exit =>\n Effect.flatMap(Clock.currentTimeMillis, finished =>\n afterNestedCallSafely(\n Exit.isSuccess(exit)\n ? {\n call: nestedCall,\n outcome: hookOutcome(exit),\n durationMs: finished - started,\n context: input.context,\n result: exit.value\n }\n : {\n call: nestedCall,\n outcome: hookOutcome(exit),\n durationMs: finished - started,\n context: input.context\n }\n )\n )\n )\n )\n })\n\n // The plan's ordering guardrail: once a script stages, only read tools run.\n const guarded =\n plan === undefined\n ? run\n : plan.admit(nestedCall).pipe(\n Effect.matchEffect({\n onFailure: error =>\n Effect.succeed(\n ToolResult.make({\n toolCallId: nestedCall.id,\n content: error.message,\n isError: true\n })\n ),\n onSuccess: () => run\n })\n )\n\n const admitted =\n options.beforeNestedCall === undefined\n ? guarded\n : options.beforeNestedCall({ call: nestedCall, context: input.context }).pipe(\n Effect.matchEffect({\n onFailure: message =>\n Effect.succeed(\n ToolResult.make({\n toolCallId: nestedCall.id,\n content: message,\n isError: true\n })\n ),\n onSuccess: () => guarded\n })\n )\n\n const execution = Effect.gen(function* () {\n const started = yield* Clock.currentTimeMillis\n\n return yield* admitted.pipe(\n Effect.onExit(exit =>\n Effect.map(Clock.currentTimeMillis, finished => {\n record.outcome = recordedOutcome(exit, finished - started)\n })\n )\n )\n })\n\n return new Promise((resolve, reject) => {\n runFork(execution, { signal }).addObserver(exit => {\n if (Exit.isFailure(exit)) {\n reject(\n new Error(\n Cause.hasInterruptsOnly(exit.cause)\n ? `${tool.callLabel} was cancelled.`\n : `${tool.callLabel} failed unexpectedly.`\n )\n )\n\n return\n }\n\n const resolution = resolveNestedResult(tool, exit.value)\n\n if (resolution.ok) {\n resolve(resolution.value)\n } else {\n // The script sees which call failed; the nested-call record keeps the raw text.\n reject(new Error(`${tool.callLabel}: ${resolution.message}`))\n }\n })\n })\n }\n\n const tools: ReadonlyArray<CodeModeExecutorTool> = catalog.map(tool => ({\n name: tool.name,\n description: tool.description,\n inputSchema: tool.inputSchema,\n outputSchema: tool.outputSchema,\n callLabel: tool.callLabel,\n execute: callTool(tool)\n }))\n\n const result = yield* Effect.tryPromise({\n try: signal =>\n options.executor.execute(input.code, {\n tools,\n globals:\n plan === undefined\n ? discoveryGlobals(catalog)\n : [...discoveryGlobals(catalog), stageGlobal(plan, stageFork)],\n timeoutMs,\n memoryLimitBytes: limits.memoryLimitBytes,\n store,\n signal\n }),\n catch: error => new CodeModeExecutorRejected({ message: errorMessage(error) })\n }).pipe(\n Effect.catch(error =>\n Effect.succeed<CodeModeExecutionResult>({\n ok: false,\n error: {\n kind: 'sandbox',\n message: `The code mode executor failed: ${error.message}`\n },\n output: []\n })\n ),\n // Backstop for executors that miss their own deadline: interrupting aborts the signal.\n Effect.timeoutOption(Duration.millis(timeoutMs + codeModeDeadlineMarginMs)),\n Effect.map(\n Option.getOrElse((): CodeModeExecutionResult => ({\n ok: false,\n error: {\n kind: 'timeout',\n message: `Execution timed out after ${timeoutMs} ms (the executor did not stop in time)`\n },\n output: []\n }))\n )\n )\n\n // Calls still running when the script ended are cancelled and recorded as such.\n yield* boundedWait(FiberSet.clear(fibers))\n yield* boundedWait(FiberSet.clear(stageFibers))\n\n const finishedAt = yield* Clock.currentTimeMillis\n\n // A successful script's staged calls are saved once as a plan; a failed script's are\n // discarded (a partial plan is never offered for review). Nothing staged ever ran.\n const stagedCount = plan === undefined ? 0 : (yield* plan.staged).length\n\n const saved =\n plan === undefined || stagedCount === 0 || !result.ok\n ? undefined\n : yield* plan.finish.pipe(Effect.result)\n\n const savedPlan: CodeModeStagedPlan | undefined =\n staging === undefined ||\n saved === undefined ||\n Result.isFailure(saved) ||\n saved.success === undefined\n ? undefined\n : {\n id: saved.success.id,\n digest: saved.success.digest,\n count: saved.success.calls.length,\n reviewToolName: staging.reviewToolName\n }\n\n const planFailure =\n saved !== undefined && Result.isFailure(saved) ? saved.failure : undefined\n\n const planFailed = planFailure !== undefined\n const plural = stagedCount === 1 ? '' : 's'\n\n const stagingNotice =\n stagedCount === 0\n ? undefined\n : !result.ok\n ? `\\n\\nThe script failed, so its ${stagedCount} staged call${plural} ${stagedCount === 1 ? 'was' : 'were'} discarded; nothing was applied.`\n : planFailure !== undefined\n ? `\\n\\nThe script staged ${stagedCount} call${plural}, but the plan could not be saved (${planFailure.message}); nothing was applied. Run the script again.`\n : savedPlan === undefined\n ? undefined\n : planNotice(savedPlan)\n\n const recorded = records.map((record): NestedToolCallInput => ({\n id: record.id,\n name: record.name,\n args: record.args,\n // A call interrupted before it started has no outcome.\n ...(record.outcome ?? { status: 'cancelled' })\n }))\n\n const recorder = recorded.reduce(\n recordNestedToolCall,\n makeNestedToolCallRecorder({ maxCalls: limits.maxNestedCalls })\n )\n\n const { nestedCalls, usage } = nestedToolCallResultFields(recorder)\n\n const segments = codeModeResultSegments({\n result,\n wallTimeMs: finishedAt - startedAt,\n calls: recorded,\n maxChars: limits.maxOutputChars,\n maxImages: limits.maxImages,\n maxImageBytes: limits.maxImageBytes\n })\n\n // The staging notice follows the bounded output so a cut never hides the plan.\n const content = codeModeSegmentsContent(\n stagingNotice === undefined\n ? segments\n : [...segments, { type: 'text', text: stagingNotice }]\n )\n\n const ok = result.ok && !planFailed\n\n const codemode: CodeModeStructuredContent['codemode'] =\n ok && result.storeWrites !== undefined && hasStoreWrites(result)\n ? { ok: true, storeWrites: result.storeWrites }\n : { ok }\n\n const structuredContent: CodeModeStructuredContent = {\n codemode: savedPlan === undefined ? codemode : { ...codemode, plan: savedPlan }\n }\n\n type ResultFields = {\n toolCallId: string\n content: Content\n isError?: boolean\n structuredContent: CodeModeStructuredContent\n nestedCalls: typeof nestedCalls\n usage?: AgentUsage\n }\n\n const fields: ResultFields = {\n toolCallId: call.id,\n content,\n structuredContent,\n nestedCalls\n }\n\n if (!ok) {\n fields.isError = true\n }\n\n if (usage !== undefined) {\n fields.usage = usage\n }\n\n return ToolResult.make(fields)\n }),\n (scope, exit) => boundedWait(Scope.close(scope, exit))\n )\n })\n\nconst argsPreviewChars = 200\n\nconst interruptedCallStatus = (entry: ToolLedgerEntry): CodeModeInterruptedCallStatus => {\n const outcome = entry.outcome\n\n if (outcome === undefined) return 'unknown'\n\n if (Predicate.isTagged(outcome, 'Failed')) return 'failed'\n\n return outcome.result.isError === true ? 'failed' : 'applied'\n}\n\nconst nestedEntryState = (entry: ToolLedgerEntry) => {\n const outcome = entry.outcome\n\n if (outcome === undefined) {\n return 'unknown: it started but never recorded a result, so it may have been applied'\n }\n\n if (Predicate.isTagged(outcome, 'Failed'))\n return `completed with an error: ${truncateCodePoints(outcome.error.message, nestedToolCallMaxErrorChars)}`\n\n return outcome.result.isError === true ? 'completed with an error result' : 'applied'\n}\n\n/** The bounded, wire-safe `interruptedCalls` of an abandoned execution's nested ledger entries:\n * the `nestedCalls` record bounds (`maxCalls`, 8 KiB of arguments per call, 32 KiB in total).\n */\nconst interruptedCalls = (\n nested: ReadonlyArray<ToolLedgerEntry>,\n maxCalls: number\n): CodeModeInterruptedCalls => {\n const counts = { applied: 0, failed: 0, unknown: 0 }\n const calls: Array<CodeModeInterruptedCall> = []\n let argsBytes = 0\n let complete = true\n\n for (const entry of nested) {\n const status = interruptedCallStatus(entry)\n\n counts[status]++\n\n if (calls.length >= maxCalls) {\n complete = false\n continue\n }\n\n const { args, bytes, truncated } = boundNestedToolCallArgs(entry.args, argsBytes)\n\n // `argsTruncated`: the ledger already cut the arguments when the call was claimed.\n if (truncated || entry.argsTruncated) complete = false\n\n argsBytes += bytes\n calls.push({ key: entry.key, toolName: entry.toolName, args, status })\n }\n\n return { calls, complete, counts }\n}\n\n/**\n * The result of a code mode call whose earlier execution was abandoned (claimed, lease expired,\n * no result recorded). The script never runs again: the result lists that execution's ledgered\n * nested calls as applied, failed, or unknown, and says they were not undone. The text is bounded\n * by `maxOutputChars`; `structuredContent.codemode.interruptedCalls` carries the same entries for\n * hosts, bounded like `nestedCalls`. When the ledger cannot list them, the result stays\n * interrupted, says they may have been applied, and sets `interruptedCallsUnavailable` instead.\n */\nconst abandonedScriptResult =\n (limits: Required<CodeModeLimits>) =>\n ({ call, nested }: ToolLedgerAbandonedInput): ToolResult => {\n const maxChars = limits.maxOutputChars\n\n const listing =\n nested === undefined\n ? 'Its ledgered nested tool calls could not be listed (the tool ledger is unavailable), so any of them may already have been applied.'\n : nested.length === 0\n ? 'No ledgered nested tool calls were recorded for that execution.'\n : [\n `Ledgered nested tool calls of that execution (they were not undone):`,\n ...nested.map(\n entry =>\n `- ${entry.key} ${entry.toolName} ${truncateCodePoints(entry.args, argsPreviewChars)}: ${nestedEntryState(entry)}`\n )\n ].join('\\n')\n\n const text = [\n `Script interrupted: an earlier execution of this ${call.name} call (${call.id}) started but never recorded a result. The script was not run again.`,\n listing,\n 'Calls the ledger policy skips (by default read-only calls) are not listed. Verify the state these calls affect before repeating any work.'\n ].join('\\n\\n')\n\n const structuredContent: CodeModeStructuredContent = {\n codemode:\n nested === undefined\n ? { ok: false, interrupted: true, interruptedCallsUnavailable: true }\n : {\n ok: false,\n interrupted: true,\n interruptedCalls: interruptedCalls(\n nested,\n nestedToolCallRecordLimit(limits.maxNestedCalls)\n )\n }\n }\n\n return ToolResult.make({\n toolCallId: call.id,\n content: codeModeSegmentsContent(boundCodeModeSegments([{ type: 'text', text }], maxChars)),\n isError: true,\n structuredContent\n })\n }\n\n/**\n * The code mode tool: one registration (default name `codemode`, input `{ code }`) whose scripts\n * call the other code-mode-callable tools of the same `resolveTools` resolution. Nested calls run\n * through the resolved execute path with the same host context (input decoding, enablement,\n * registration wrappers), get ids `<toolCallId>/<seq>`, and are recorded on the result's\n * `nestedCalls`. The resolved description lists the callable tools (see\n * `renderCodeModeDescription`).\n *\n * Access is `write`: scripts can call any write tool the resolution exposes to code mode; each\n * nested call keeps its own access metadata and host wrappers.\n *\n * With a tool ledger (`resolveTools(..., { ledger })`) the code mode call itself is ledgered, so a\n * re-executed call never runs its script again: a completed call returns its stored result, an\n * in-flight one is awaited, and an abandoned one returns an interrupted result listing its\n * ledgered nested calls. Nested write calls are ledgered under `<toolCallId>/<seq>`.\n */\nexport const makeCodeModeTool = <Context>(\n options: MakeCodeModeToolOptions<Context>\n): ToolRegistration<Context> => {\n const name = options.name ?? codeModeToolName\n const limits: Required<CodeModeLimits> = { ...defaultCodeModeLimits, ...options.limits }\n const inlineBudget = options.inlineBudget ?? defaultCodeModeInlineBudget\n const store = options.loadStore !== undefined\n\n return makeTool<Context, typeof CodeModeParams>({\n name,\n description: renderCodeModeDescription({ tools: [], inlineBudget, store }),\n parameters: CodeModeParams,\n access: 'write',\n nestedToolAccess: true,\n describe: ({ tools, staging }) =>\n renderCodeModeDescription(\n staging === undefined || options.staging === false\n ? { tools, inlineBudget, store }\n : { tools, inlineBudget, store, staging }\n ),\n abandonedResult: abandonedScriptResult(limits),\n execute: ({\n call,\n context,\n params,\n nested\n }: ToolExecutionInput<Context> & { readonly params: CodeModeParams }) =>\n nested === undefined\n ? Effect.fail(\n new ToolError({\n tool: name,\n cause: 'unavailable',\n message: `${name} requires nested tool access; resolve it with resolveTools.`\n })\n )\n : runScript({ options, limits, call, context, code: params.code, nested })\n })\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yolk-sdk/codemode",
3
- "version": "0.1.0-canary.102",
3
+ "version": "0.1.0-canary.103",
4
4
  "description": "Code mode for Yolk agents: one tool that runs model-written JavaScript calling the host's resolved tools, plus a Node executor on a confined QuickJS sandbox.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -52,7 +52,7 @@
52
52
  "dependencies": {
53
53
  "@earendil-works/pi-codemode": "1.0.0",
54
54
  "effect": "4.0.0",
55
- "@yolk-sdk/agent": "^0.1.0-canary.102"
55
+ "@yolk-sdk/agent": "^0.1.0-canary.103"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsdown",
package/src/catalog.ts CHANGED
@@ -221,6 +221,26 @@ const intro = [
221
221
  'Only the script output and its return value come back to you: filter and aggregate inside the script and return a small JSON value.'
222
222
  ].join('\n')
223
223
 
224
+ const stageLine = (reviewToolName: string) =>
225
+ `- \`await stage(name, args)\`: record a call of a stageable tool for review instead of running it; resolves to \`{ staged: true, key, index }\`. Nothing runs until a person approves the plan through the \`${reviewToolName}\` tool, which the result tells you how to call. Once a script stages, it may only run read tools; a script that already ran a write tool cannot stage.`
226
+
227
+ /** Stageable tool names listed in the description; the rest are summarized with a count. */
228
+ const maxListedStageableTools = 20
229
+
230
+ const stageableSection = (staging: CodeModeStagingDescription) => {
231
+ const names = staging.tools.map(tool => tool.def.name)
232
+ const hidden = names.length - maxListedStageableTools
233
+
234
+ return [
235
+ '## Stageable tools',
236
+ 'Approval-gated tools a script cannot call through `tools`, only stage with `await stage("<name>", args)` (arguments as for the tool of the same name):',
237
+ ...names.slice(0, maxListedStageableTools).map(name => `- \`${name}\``),
238
+ ...(hidden > 0
239
+ ? [`- … and ${hidden} more (an unknown name rejects with the stageable tools).`]
240
+ : [])
241
+ ].join('\n')
242
+ }
243
+
224
244
  const globalLines = (store: boolean) => [
225
245
  '- `text(value)` and `console.log(...values)`: append text to the output.',
226
246
  '- `image(dataUrl)`: append a base64 image (a `data:` URL or `{ type: "image", data, mimeType }`).',
@@ -256,11 +276,19 @@ const namespaceSection = (namespace: string, tools: ReadonlyArray<CodeModeCatalo
256
276
  const unlistedToolsLine =
257
277
  'More tools may be callable than are listed here: find them with `await searchTools(query, { namespace? })` and read one with `await describeTool(name)` or `await describeNamespace(name)` before calling it.'
258
278
 
279
+ /** Staged tool plans as the description shows them (`NestedToolStagingDescription`). */
280
+ export type CodeModeStagingDescription = {
281
+ readonly tools: ReadonlyArray<NestedTool>
282
+ readonly reviewToolName: string
283
+ }
284
+
259
285
  export type CodeModeDescriptionInput = {
260
286
  readonly tools: ReadonlyArray<NestedTool>
261
287
  readonly inlineBudget?: number
262
288
  /** Mention `store()`/`load()` persistence. */
263
289
  readonly store?: boolean
290
+ /** Document `stage()` and list the stageable tools (only when some exist). */
291
+ readonly staging?: CodeModeStagingDescription
264
292
  }
265
293
 
266
294
  /**
@@ -268,7 +296,8 @@ export type CodeModeDescriptionInput = {
268
296
  * namespace, then one fixed line pointing to `searchTools`/`describeTool`/`describeNamespace`.
269
297
  * `all` tools get one line each outside the budget; `listed` tools are declared within the inline
270
298
  * budget; `search` tools never contribute, so adding or removing them (even whole namespaces of
271
- * them) leaves the text byte-identical.
299
+ * them) leaves the text byte-identical. With `staging` (and stageable tools), `stage()` is listed
300
+ * with the globals and the stageable tool names follow the nested tools.
272
301
  */
273
302
  export const renderCodeModeDescription = (input: CodeModeDescriptionInput): string => {
274
303
  const catalog = codeModeCatalog(input.tools)
@@ -283,12 +312,20 @@ export const renderCodeModeDescription = (input: CodeModeDescriptionInput): stri
283
312
  )
284
313
  )
285
314
 
315
+ const staging =
316
+ input.staging !== undefined && input.staging.tools.length > 0 ? input.staging : undefined
317
+
286
318
  return [
287
319
  intro,
288
- ['Globals:', ...globalLines(input.store === true)].join('\n'),
320
+ [
321
+ 'Globals:',
322
+ ...globalLines(input.store === true),
323
+ ...(staging === undefined ? [] : [stageLine(staging.reviewToolName)])
324
+ ].join('\n'),
289
325
  sections.length === 0
290
326
  ? 'Nested tools: none are listed here.'
291
327
  : ['## Nested tools by namespace', ...sections].join('\n\n'),
328
+ ...(staging === undefined ? [] : [stageableSection(staging)]),
292
329
  unlistedToolsLine
293
330
  ].join('\n\n')
294
331
  }
package/src/index.ts CHANGED
@@ -27,6 +27,7 @@ export type {
27
27
  CodeModeCatalogTool,
28
28
  CodeModeDescriptionInput,
29
29
  CodeModeListing,
30
+ CodeModeStagingDescription,
30
31
  CodeModeToolExposure
31
32
  } from './catalog.ts'
32
33
 
@@ -55,6 +56,7 @@ export type {
55
56
  CodeModeInterruptedCall,
56
57
  CodeModeInterruptedCalls,
57
58
  CodeModeInterruptedCallStatus,
59
+ CodeModeStagedPlan,
58
60
  CodeModeStructuredContent,
59
61
  CodeModeToolResultEntry
60
62
  } from './store.ts'
@@ -71,6 +73,7 @@ export type {
71
73
  CodeModeAfterNestedCallInput,
72
74
  CodeModeLimits,
73
75
  CodeModeNestedCallOutcome,
76
+ CodeModeStagingOptions,
74
77
  MakeCodeModeToolOptions
75
78
  } from './tool.ts'
76
79
 
package/src/store.ts CHANGED
@@ -18,9 +18,21 @@ export type CodeModeStructuredContent = {
18
18
  * (the ledger's `list` failed), so any of them may have been applied.
19
19
  */
20
20
  readonly interruptedCallsUnavailable?: true
21
+ /** The staged tool plan a successful script saved (ADR 0005); nothing of it ran yet. */
22
+ readonly plan?: CodeModeStagedPlan
21
23
  }
22
24
  }
23
25
 
26
+ /** A saved staged tool plan: review it with the `reviewToolName` tool and `{ planId: id,
27
+ * planDigest: digest }`.
28
+ */
29
+ export type CodeModeStagedPlan = {
30
+ readonly id: string
31
+ readonly digest: string
32
+ readonly count: number
33
+ readonly reviewToolName: string
34
+ }
35
+
24
36
  /** What an abandoned execution's ledgered nested call is known to have done: `applied` (recorded
25
37
  * a result), `failed` (recorded a `ToolError` or an `isError` result), or `unknown` (started, no
26
38
  * recorded outcome, so it may have been applied).
package/src/tool.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  FiberSet,
10
10
  Option,
11
11
  Predicate,
12
+ Result,
12
13
  Scope
13
14
  } from 'effect'
14
15
  import * as Schema from 'effect/Schema'
@@ -32,9 +33,12 @@ import {
32
33
  import {
33
34
  makeTool,
34
35
  type NestedToolExecutor,
36
+ type StagedCallReceipt,
35
37
  type ToolExecutionInput,
36
38
  type ToolLedgerAbandonedInput,
37
39
  type ToolLedgerEntry,
40
+ type ToolPlanBuilder,
41
+ type ToolPlanStageError,
38
42
  type ToolRegistration
39
43
  } from '@yolk-sdk/agent/tools'
40
44
  import {
@@ -64,6 +68,7 @@ import type {
64
68
  CodeModeInterruptedCall,
65
69
  CodeModeInterruptedCalls,
66
70
  CodeModeInterruptedCallStatus,
71
+ CodeModeStagedPlan,
67
72
  CodeModeStructuredContent
68
73
  } from './store.ts'
69
74
 
@@ -144,6 +149,18 @@ export type MakeCodeModeToolOptions<Context> = {
144
149
  * throw) is logged and never changes the call's result.
145
150
  */
146
151
  readonly afterNestedCall?: (input: CodeModeAfterNestedCallInput<Context>) => Effect.Effect<void>
152
+ /**
153
+ * Staged tool plans (ADR 0005). When the resolution offers staging (`resolveTools(..., { plans,
154
+ * interactionHost })` with a plan review tool), scripts get `stage(name, args)` for stageable
155
+ * approval-gated tools. `false` turns it off for this tool; `maxCalls` may only lower the
156
+ * resolution's cap.
157
+ */
158
+ readonly staging?: CodeModeStagingOptions | false
159
+ }
160
+
161
+ export type CodeModeStagingOptions = {
162
+ /** Staged calls per script; `stage` rejects past it. At most the resolution's `maxCalls`. */
163
+ readonly maxCalls?: number
147
164
  }
148
165
 
149
166
  /** Outcome of a nested call for `afterNestedCall`. */
@@ -310,6 +327,55 @@ const discoveryGlobals = (
310
327
  }
311
328
  ]
312
329
 
330
+ type StageFork = (
331
+ effect: Effect.Effect<StagedCallReceipt, ToolPlanStageError>,
332
+ options: { readonly signal: AbortSignal }
333
+ ) => Fiber.Fiber<StagedCallReceipt, ToolPlanStageError>
334
+
335
+ /** `stage(name, args)`: records one call into the script's plan (never runs it). */
336
+ const stageGlobal = (plan: ToolPlanBuilder, fork: StageFork): CodeModeExecutorGlobal => ({
337
+ name: 'stage',
338
+ spread: true,
339
+ signature: '(name: string, args: object): Promise<{ staged: true; key: string; index: number }>',
340
+ description:
341
+ 'Record a call of a stageable tool into this script’s plan for a person to review; it does not run.',
342
+ execute: (args, { signal }) => {
343
+ const [name, params] = Array.isArray(args) ? args : []
344
+
345
+ if (signal.aborted) return Promise.reject(new Error('stage was cancelled.'))
346
+
347
+ if (!Predicate.isString(name))
348
+ return Promise.reject(new Error('stage: the first argument must be a tool name.'))
349
+
350
+ return new Promise((resolve, reject) => {
351
+ fork(plan.stage({ name, params: params === undefined ? {} : params }), {
352
+ signal
353
+ }).addObserver(exit => {
354
+ if (Exit.isSuccess(exit)) {
355
+ resolve(exit.value)
356
+
357
+ return
358
+ }
359
+
360
+ const error = Cause.findErrorOption(exit.cause)
361
+
362
+ reject(
363
+ new Error(
364
+ Option.isSome(error)
365
+ ? `stage: ${error.value.message}`
366
+ : Cause.hasInterruptsOnly(exit.cause)
367
+ ? 'stage was cancelled.'
368
+ : 'stage failed unexpectedly.'
369
+ )
370
+ )
371
+ })
372
+ })
373
+ }
374
+ })
375
+
376
+ const planNotice = (plan: CodeModeStagedPlan) =>
377
+ `\n\nStaged ${plan.count} call${plan.count === 1 ? '' : 's'} as plan ${plan.id}; nothing was applied yet. To apply them, call ${plan.reviewToolName}(${JSON.stringify({ planId: plan.id, planDigest: plan.digest })}): a person reviews the plan and selects the calls to apply.`
378
+
313
379
  const hasStoreWrites = (result: CodeModeExecutionResult) =>
314
380
  result.storeWrites !== undefined &&
315
381
  (Object.keys(result.storeWrites.set).length > 0 || result.storeWrites.delete.length > 0)
@@ -360,6 +426,24 @@ const runScript = <Context>(input: RunInput<Context>): Effect.Effect<ToolResult,
360
426
  const runFork = yield* FiberSet.runtime(fibers)<never>()
361
427
  const records: Array<CallRecord> = []
362
428
 
429
+ // Staged tool plans: only when the resolution offers staging and the host keeps it on.
430
+ const stagingOptions = options.staging === false ? undefined : options.staging
431
+ const staging = options.staging === false ? undefined : nested.staging
432
+ const maxStagedCalls = stagingOptions?.maxCalls
433
+
434
+ const plan =
435
+ staging === undefined
436
+ ? undefined
437
+ : staging.begin(
438
+ maxStagedCalls === undefined ? undefined : { maxCalls: maxStagedCalls }
439
+ )
440
+
441
+ const stageFibers = yield* FiberSet.make<StagedCallReceipt, ToolPlanStageError>().pipe(
442
+ Scope.provide(scope)
443
+ )
444
+
445
+ const stageFork = yield* FiberSet.runtime(stageFibers)<never>()
446
+
363
447
  const callTool =
364
448
  (tool: CodeModeCatalogTool): CodeModeExecutorTool['execute'] =>
365
449
  (args, { signal }) => {
@@ -434,9 +518,27 @@ const runScript = <Context>(input: RunInput<Context>): Effect.Effect<ToolResult,
434
518
  )
435
519
  })
436
520
 
521
+ // The plan's ordering guardrail: once a script stages, only read tools run.
522
+ const guarded =
523
+ plan === undefined
524
+ ? run
525
+ : plan.admit(nestedCall).pipe(
526
+ Effect.matchEffect({
527
+ onFailure: error =>
528
+ Effect.succeed(
529
+ ToolResult.make({
530
+ toolCallId: nestedCall.id,
531
+ content: error.message,
532
+ isError: true
533
+ })
534
+ ),
535
+ onSuccess: () => run
536
+ })
537
+ )
538
+
437
539
  const admitted =
438
540
  options.beforeNestedCall === undefined
439
- ? run
541
+ ? guarded
440
542
  : options.beforeNestedCall({ call: nestedCall, context: input.context }).pipe(
441
543
  Effect.matchEffect({
442
544
  onFailure: message =>
@@ -447,7 +549,7 @@ const runScript = <Context>(input: RunInput<Context>): Effect.Effect<ToolResult,
447
549
  isError: true
448
550
  })
449
551
  ),
450
- onSuccess: () => run
552
+ onSuccess: () => guarded
451
553
  })
452
554
  )
453
555
 
@@ -502,7 +604,10 @@ const runScript = <Context>(input: RunInput<Context>): Effect.Effect<ToolResult,
502
604
  try: signal =>
503
605
  options.executor.execute(input.code, {
504
606
  tools,
505
- globals: discoveryGlobals(catalog),
607
+ globals:
608
+ plan === undefined
609
+ ? discoveryGlobals(catalog)
610
+ : [...discoveryGlobals(catalog), stageGlobal(plan, stageFork)],
506
611
  timeoutMs,
507
612
  memoryLimitBytes: limits.memoryLimitBytes,
508
613
  store,
@@ -536,9 +641,49 @@ const runScript = <Context>(input: RunInput<Context>): Effect.Effect<ToolResult,
536
641
 
537
642
  // Calls still running when the script ended are cancelled and recorded as such.
538
643
  yield* boundedWait(FiberSet.clear(fibers))
644
+ yield* boundedWait(FiberSet.clear(stageFibers))
539
645
 
540
646
  const finishedAt = yield* Clock.currentTimeMillis
541
647
 
648
+ // A successful script's staged calls are saved once as a plan; a failed script's are
649
+ // discarded (a partial plan is never offered for review). Nothing staged ever ran.
650
+ const stagedCount = plan === undefined ? 0 : (yield* plan.staged).length
651
+
652
+ const saved =
653
+ plan === undefined || stagedCount === 0 || !result.ok
654
+ ? undefined
655
+ : yield* plan.finish.pipe(Effect.result)
656
+
657
+ const savedPlan: CodeModeStagedPlan | undefined =
658
+ staging === undefined ||
659
+ saved === undefined ||
660
+ Result.isFailure(saved) ||
661
+ saved.success === undefined
662
+ ? undefined
663
+ : {
664
+ id: saved.success.id,
665
+ digest: saved.success.digest,
666
+ count: saved.success.calls.length,
667
+ reviewToolName: staging.reviewToolName
668
+ }
669
+
670
+ const planFailure =
671
+ saved !== undefined && Result.isFailure(saved) ? saved.failure : undefined
672
+
673
+ const planFailed = planFailure !== undefined
674
+ const plural = stagedCount === 1 ? '' : 's'
675
+
676
+ const stagingNotice =
677
+ stagedCount === 0
678
+ ? undefined
679
+ : !result.ok
680
+ ? `\n\nThe script failed, so its ${stagedCount} staged call${plural} ${stagedCount === 1 ? 'was' : 'were'} discarded; nothing was applied.`
681
+ : planFailure !== undefined
682
+ ? `\n\nThe script staged ${stagedCount} call${plural}, but the plan could not be saved (${planFailure.message}); nothing was applied. Run the script again.`
683
+ : savedPlan === undefined
684
+ ? undefined
685
+ : planNotice(savedPlan)
686
+
542
687
  const recorded = records.map((record): NestedToolCallInput => ({
543
688
  id: record.id,
544
689
  name: record.name,
@@ -554,22 +699,31 @@ const runScript = <Context>(input: RunInput<Context>): Effect.Effect<ToolResult,
554
699
 
555
700
  const { nestedCalls, usage } = nestedToolCallResultFields(recorder)
556
701
 
702
+ const segments = codeModeResultSegments({
703
+ result,
704
+ wallTimeMs: finishedAt - startedAt,
705
+ calls: recorded,
706
+ maxChars: limits.maxOutputChars,
707
+ maxImages: limits.maxImages,
708
+ maxImageBytes: limits.maxImageBytes
709
+ })
710
+
711
+ // The staging notice follows the bounded output so a cut never hides the plan.
557
712
  const content = codeModeSegmentsContent(
558
- codeModeResultSegments({
559
- result,
560
- wallTimeMs: finishedAt - startedAt,
561
- calls: recorded,
562
- maxChars: limits.maxOutputChars,
563
- maxImages: limits.maxImages,
564
- maxImageBytes: limits.maxImageBytes
565
- })
713
+ stagingNotice === undefined
714
+ ? segments
715
+ : [...segments, { type: 'text', text: stagingNotice }]
566
716
  )
567
717
 
718
+ const ok = result.ok && !planFailed
719
+
720
+ const codemode: CodeModeStructuredContent['codemode'] =
721
+ ok && result.storeWrites !== undefined && hasStoreWrites(result)
722
+ ? { ok: true, storeWrites: result.storeWrites }
723
+ : { ok }
724
+
568
725
  const structuredContent: CodeModeStructuredContent = {
569
- codemode:
570
- result.ok && result.storeWrites !== undefined && hasStoreWrites(result)
571
- ? { ok: true, storeWrites: result.storeWrites }
572
- : { ok: result.ok }
726
+ codemode: savedPlan === undefined ? codemode : { ...codemode, plan: savedPlan }
573
727
  }
574
728
 
575
729
  type ResultFields = {
@@ -588,7 +742,7 @@ const runScript = <Context>(input: RunInput<Context>): Effect.Effect<ToolResult,
588
742
  nestedCalls
589
743
  }
590
744
 
591
- if (!result.ok) {
745
+ if (!ok) {
592
746
  fields.isError = true
593
747
  }
594
748
 
@@ -745,7 +899,12 @@ export const makeCodeModeTool = <Context>(
745
899
  parameters: CodeModeParams,
746
900
  access: 'write',
747
901
  nestedToolAccess: true,
748
- describe: ({ tools }) => renderCodeModeDescription({ tools, inlineBudget, store }),
902
+ describe: ({ tools, staging }) =>
903
+ renderCodeModeDescription(
904
+ staging === undefined || options.staging === false
905
+ ? { tools, inlineBudget, store }
906
+ : { tools, inlineBudget, store, staging }
907
+ ),
749
908
  abandonedResult: abandonedScriptResult(limits),
750
909
  execute: ({
751
910
  call,