@yolk-sdk/codemode 0.1.0-canary.96

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +132 -0
  3. package/dist/catalog.d.mts +60 -0
  4. package/dist/catalog.d.mts.map +1 -0
  5. package/dist/catalog.mjs +143 -0
  6. package/dist/catalog.mjs.map +1 -0
  7. package/dist/classifier-tool.d.mts +78 -0
  8. package/dist/classifier-tool.d.mts.map +1 -0
  9. package/dist/classifier-tool.mjs +125 -0
  10. package/dist/classifier-tool.mjs.map +1 -0
  11. package/dist/executor.d.mts +89 -0
  12. package/dist/executor.d.mts.map +1 -0
  13. package/dist/executor.mjs +1 -0
  14. package/dist/index.d.mts +8 -0
  15. package/dist/index.mjs +7 -0
  16. package/dist/node.d.mts +34 -0
  17. package/dist/node.d.mts.map +1 -0
  18. package/dist/node.mjs +215 -0
  19. package/dist/node.mjs.map +1 -0
  20. package/dist/output.d.mts +42 -0
  21. package/dist/output.d.mts.map +1 -0
  22. package/dist/output.mjs +184 -0
  23. package/dist/output.mjs.map +1 -0
  24. package/dist/search.d.mts +23 -0
  25. package/dist/search.d.mts.map +1 -0
  26. package/dist/search.mjs +58 -0
  27. package/dist/search.mjs.map +1 -0
  28. package/dist/store.d.mts +40 -0
  29. package/dist/store.d.mts.map +1 -0
  30. package/dist/store.mjs +61 -0
  31. package/dist/store.mjs.map +1 -0
  32. package/dist/tool.d.mts +75 -0
  33. package/dist/tool.d.mts.map +1 -0
  34. package/dist/tool.mjs +282 -0
  35. package/dist/tool.mjs.map +1 -0
  36. package/package.json +63 -0
  37. package/src/catalog.ts +274 -0
  38. package/src/classifier-tool.ts +227 -0
  39. package/src/executor.ts +103 -0
  40. package/src/index.ts +76 -0
  41. package/src/node.ts +334 -0
  42. package/src/output.ts +287 -0
  43. package/src/search.ts +113 -0
  44. package/src/store.ts +93 -0
  45. package/src/tool.ts +547 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yolk SDK contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # @yolk-sdk/codemode
2
+
3
+ Code mode for Yolk agents: one tool whose input is a short JavaScript program that calls the host's
4
+ resolved tools, filters and aggregates their results, and returns only what matters.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ pnpm add @yolk-sdk/codemode@canary @yolk-sdk/agent@canary effect@4.0.0-rc.115
10
+ ```
11
+
12
+ Canary APIs are unstable. Keep all `@yolk-sdk/*` packages on the same version.
13
+ Use the SDK's matching Effect version (`4.0.0-rc.115`) in host code.
14
+ Requires Node.js 22.19+ (the pi engine's minimum). `@yolk-sdk/codemode/node` is server-only.
15
+
16
+ ## Subpaths
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 |
22
+
23
+ ## Example
24
+
25
+ ```ts
26
+ import { Effect } from 'effect'
27
+ import { resolveTools } from '@yolk-sdk/agent/tools'
28
+ import { codeModeStoreFromToolResults, makeCodeModeTool } from '@yolk-sdk/codemode'
29
+ import { makePiCodeModeExecutor } from '@yolk-sdk/codemode/node'
30
+
31
+ // One executor per process: its concurrency cap is per executor.
32
+ const executor = makePiCodeModeExecutor({ maxConcurrentExecutions: 4 })
33
+
34
+ const codemode = makeCodeModeTool<HostContext>({
35
+ executor,
36
+ deadline: context => context.stepDeadlineMs,
37
+ // Prior results paired with their tool names, oldest first (host-owned lookup).
38
+ loadStore: context => Effect.succeed(codeModeStoreFromToolResults(context.priorToolResults))
39
+ })
40
+
41
+ const program = Effect.gen(function* () {
42
+ const toolSet = yield* resolveTools(
43
+ [{ id: 'codemode', tools: [codemode] }, ...hostModules],
44
+ hostContext
45
+ )
46
+
47
+ return toolSet
48
+ })
49
+ ```
50
+
51
+ `HostContext`, `hostModules`, and `hostContext` are host-owned placeholders.
52
+
53
+ ## How it works
54
+
55
+ - Scripts call tools as `await tools.<id>(args)`. Every nested call runs through the resolution's
56
+ normal execute path (input decoding, enablement, registration wrappers, same host context), gets
57
+ the id `<toolCallId>/<seq>`, and is recorded on the result's `nestedCalls` with status, duration,
58
+ truncated error, and usage. Nested results never reach the model; only the script output and
59
+ return value do.
60
+ - A call resolves to `structuredContent` for tools with an output schema and to the text content
61
+ otherwise; error results reject with an `Error` carrying their text. Calls still running when
62
+ the script ends are cancelled and recorded as `cancelled`.
63
+ - The tool description lists the globals and the nested tools by namespace, with the module's
64
+ `ToolModule.description` under each heading. `codemode` + `listed` tools get TypeScript
65
+ declarations within `inlineBudget` (default 3,000 estimated tokens, filled fairly across
66
+ namespaces); `callableBy: 'all'` tools get one line each outside the budget; `codemode` +
67
+ `search` tools contribute nothing (no headings or hints), so the description stays
68
+ byte-identical when they, or whole namespaces of them, change. One fixed line tells scripts to
69
+ find unlisted tools with `searchTools(query, { limit?, namespace? })` (BM25 over names,
70
+ descriptions, namespaces, and module descriptions), `describeTool(name)`, and
71
+ `describeNamespace(name)`.
72
+ - Results start with `Script completed` or `Script failed`, include the wall time, output, and the
73
+ JSON return value, and are cut head and tail at `maxOutputChars`. Images beyond `maxImages` or
74
+ `maxImageBytes` are dropped with a note. Failed scripts keep partial output and list the tool
75
+ calls already made (they are not undone).
76
+ - `store(key, value)` writes of successful scripts are returned in
77
+ `structuredContent.codemode.storeWrites`. `codeModeStoreFromToolResults(entries, { toolName? })`
78
+ rebuilds the store for `loadStore` from `{ toolName, result }` entries, oldest first; it applies
79
+ only results of the code mode tool (default `codemode`), so other tools cannot spoof writes, and
80
+ keeps the 256 KiB-per-value and 1 MiB-total bounds by dropping offending writes. Transcript
81
+ `ToolResultMessage`s carry no tool name: pair each with its assistant tool call's name.
82
+ - `beforeNestedCall({ call, context })` runs before each nested call; a failure rejects that call in
83
+ the script with the message and records it as `error` without executing it.
84
+ - A nested call that is interrupted rejects with `was cancelled`; a defect rejects with `failed
85
+ unexpectedly`. If an executor misses its deadline, the tool returns a `timeout` failure
86
+ `timeoutMs` + 5 s after the start and aborts it.
87
+
88
+ ## Limits
89
+
90
+ | Limit | Default | Notes |
91
+ | ------------------ | ------- | --------------------------------------------------------- |
92
+ | `timeoutMs` | 120000 | Clamped to `deadline(context)` minus 5 s, never below 1 s |
93
+ | `memoryLimitBytes` | 64 MiB | QuickJS heap cap |
94
+ | `maxNestedCalls` | 256 | Further calls reject with an `Error` |
95
+ | `maxOutputChars` | 40000 | Head-and-tail cut with an omission marker |
96
+ | `maxImages` | 8 | Later images are dropped with a note |
97
+ | `maxImageBytes` | 4 MiB | Base64 characters of images in total |
98
+
99
+ ## Classifier tool
100
+
101
+ `makeClassifierTool({ classify })` wraps a `ClassifierModel` from `@yolk-sdk/agent/classification`
102
+ as a `codemode` + `listed` read tool (default name `classify`). Scripts classify one item per call;
103
+ calls beyond `maxConcurrency` (default 100 per script, keyed by the parent tool call id) queue.
104
+ Each call then takes a permit from `processLimiter`, shared across scripts and registrations:
105
+ `defaultClassifierProcessLimiter` (200 per process) unless you pass one built with
106
+ `makeClassifierConcurrencyLimiter(max)` or `false`. An interrupted waiting call releases its
107
+ permits. Nested-call records carry token usage; cost stays in the result's
108
+ `structuredContent.usage`.
109
+
110
+ ## Host responsibilities
111
+
112
+ - Which tools are callable from scripts (`callableBy`, `discovery`), approvals, and policy.
113
+ - Run authority for nested calls: decorators around the `ToolExecutor` (outside
114
+ `ResolvedToolSet.execute`) do not see nested calls. Use `beforeNestedCall` or registration-level
115
+ wrappers for per-call checks.
116
+ Approval, input, interaction, background, `question`, and `subagent` tools never run from code
117
+ mode.
118
+ - Where the tool is advertised (voice sessions do not get it in v1), deadlines, and limits.
119
+ - Next.js: `serverExternalPackages: ['@yolk-sdk/codemode', '@earendil-works/pi-codemode', 'quickjs-wasi']`.
120
+ - Vercel Workflow: run the code mode call inside the tool-batch step (`'use step'`), never inside a
121
+ `'use workflow'` function.
122
+
123
+ ## Boundaries
124
+
125
+ - The root is runtime-neutral (no Node builtins) and depends only on `@yolk-sdk/agent` and the pure
126
+ declaration renderer of `@earendil-works/pi-codemode`; the worker-thread engine lives behind
127
+ `./node`. `@yolk-sdk/agent` never imports code mode.
128
+ - The pi worker inherits a copy of `process.env`; scripts cannot read it (the VM has no `process`).
129
+ - pi buffers script text and image output on the host thread without a limit while the script
130
+ runs; only the timeout bounds it. Results are bounded afterwards (`maxOutputChars`,
131
+ `maxImages`, `maxImageBytes`).
132
+ - `stripTypeScriptTypes` is experimental in Node and prints one `ExperimentalWarning` per process.
@@ -0,0 +1,60 @@
1
+ import { CodeModeJsonSchema } from "./executor.mjs";
2
+ import { NestedTool } from "@yolk-sdk/agent/tools";
3
+
4
+ //#region src/catalog.d.ts
5
+ /** How a nested tool reaches scripts: `all` tools are also direct model tools; `listed` and
6
+ * `search` are the two discovery modes of `codemode`-only tools.
7
+ */
8
+ type CodeModeToolExposure = 'all' | 'listed' | 'search';
9
+ /** One nested tool as scripts see it. */
10
+ type CodeModeCatalogTool = {
11
+ /** Raw tool name; scripts may call `tools["<name>"](args)`. */readonly name: string; /** Identifier for `tools.<identifier>(args)` (invalid identifier characters become `_`). */
12
+ readonly identifier: string; /** The tool's `ToolModule.id`. */
13
+ readonly namespace: string; /** The module's `ToolModule.description`, when set. */
14
+ readonly namespaceDescription?: string;
15
+ readonly description: string;
16
+ readonly inputSchema: CodeModeJsonSchema; /** The declared output schema, or `{ type: 'string' }` for text results. */
17
+ readonly outputSchema: CodeModeJsonSchema; /** True when the tool declares an output schema (calls resolve to `structuredContent`). */
18
+ readonly structured: boolean;
19
+ readonly exposure: CodeModeToolExposure;
20
+ };
21
+ /** Catalog of nested tools in resolution order. */
22
+ declare const codeModeCatalog: (tools: ReadonlyArray<NestedTool>) => ReadonlyArray<CodeModeCatalogTool>;
23
+ /** Default inline budget of the nested tool listing, in estimated tokens. */
24
+ declare const defaultCodeModeInlineBudget = 3000;
25
+ /** Estimated tokens of a text: four characters per token, rounded up. */
26
+ declare const estimateCodeModeTokens: (text: string) => number;
27
+ type CodeModeListing = {
28
+ /** Tools placed in the description, in catalog order: every `all` tool, and the `listed` tools
29
+ * that fit the budget.
30
+ */
31
+ readonly listed: ReadonlyArray<CodeModeCatalogTool>; /** Namespaces (catalog order) with callable tools the description does not list. */
32
+ readonly unlistedNamespaces: ReadonlyArray<string>;
33
+ };
34
+ /**
35
+ * Chooses the tools the description lists. `all` tools are always placed, outside the budget
36
+ * (the model already has their declarations). `listed` tools fill `budget` estimated tokens fairly
37
+ * across namespaces: each round, every namespace still in play places its cheapest remaining tool;
38
+ * a namespace whose next tool does not fit drops out. `search` tools are never candidates.
39
+ */
40
+ declare const selectCodeModeListing: (catalog: ReadonlyArray<CodeModeCatalogTool>, budget: number) => CodeModeListing;
41
+ type CodeModeDescriptionInput = {
42
+ readonly tools: ReadonlyArray<NestedTool>;
43
+ readonly inlineBudget?: number; /** Mention `store()`/`load()` persistence. */
44
+ readonly store?: boolean;
45
+ };
46
+ /**
47
+ * The code mode tool description: intro, globals one per line, then nested tools grouped by
48
+ * namespace, then one fixed line pointing to `searchTools`/`describeTool`/`describeNamespace`.
49
+ * `all` tools get one line each outside the budget; `listed` tools are declared within the inline
50
+ * budget; `search` tools never contribute, so adding or removing them (even whole namespaces of
51
+ * them) leaves the text byte-identical.
52
+ */
53
+ declare const renderCodeModeDescription: (input: CodeModeDescriptionInput) => string;
54
+ /** `describeTool` text: the description and TypeScript declaration of one tool. */
55
+ declare const describeCodeModeTool: (tool: CodeModeCatalogTool) => string;
56
+ /** Finds a tool by identifier or raw name. */
57
+ declare const findCodeModeTool: (catalog: ReadonlyArray<CodeModeCatalogTool>, name: string) => CodeModeCatalogTool | undefined;
58
+ //#endregion
59
+ export { CodeModeCatalogTool, CodeModeDescriptionInput, CodeModeListing, CodeModeToolExposure, codeModeCatalog, defaultCodeModeInlineBudget, describeCodeModeTool, estimateCodeModeTokens, findCodeModeTool, renderCodeModeDescription, selectCodeModeListing };
60
+ //# sourceMappingURL=catalog.d.mts.map
@@ -0,0 +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,UAMa;EAAA,SAJb,SAAA,UASU;EAAA,SAPV,oBAAA;EAAA,SACA,WAAA;EAAA,SACA,WAAA,EAAa,kBAAA,EANb;EAAA,SAQA,YAAA,EAAc,kBAAA,EAJd;EAAA,SAMA,UAAA;EAAA,SACA,QAAA,EAAU,oBAAA;AAAA;;cAMR,eAAA,GACX,KAAA,EAAO,aAAA,CAAc,UAAA,MACpB,aAAA,CAAc,mBAAA;;cAmBJ,2BAAA;;cAGA,sBAAA,GAA0B,IAAY;AAAA,KA4DvC,eAAA;EApFC;;;EAAA,SAwFF,MAAA,EAAQ,aAAA,CAAc,mBAAA,GAvFxB;EAAA,SAyFE,kBAAA,EAAoB,aAAA;AAAA;;;;;;;cASlB,qBAAA,GACX,OAAA,EAAS,aAAA,CAAc,mBAAA,GACvB,MAAA,aACC,eAAA;AAAA,KAuFS,wBAAA;EAAA,SACD,KAAA,EAAO,aAAa,CAAC,UAAA;EAAA,SACrB,YAAA,WA1K6B;EAAA,SA4K7B,KAAA;AAAA;AA5K6B;AAGxC;;;;AAAmD;AA4DnD;AA/DwC,cAsL3B,yBAAA,GAA6B,KAA+B,EAAxB,wBAAwB;;cAwB5D,oBAAA,GAAwB,IAAyB,EAAnB,mBAAmB;;cAIjD,gBAAA,GACX,OAAA,EAAS,aAAA,CAAc,mBAAA,GACvB,IAAA,aACC,mBAAA"}
@@ -0,0 +1,143 @@
1
+ import { Predicate } from "effect";
2
+ import { renderDeclarations, renderToolOutputType, renderToolSample, toCodemodeIdentifier } from "@earendil-works/pi-codemode/declarations";
3
+ import { toolDiscovery } from "@yolk-sdk/agent/protocol";
4
+ //#region src/catalog.ts
5
+ const textOutputSchema = { type: "string" };
6
+ /** Catalog of nested tools in resolution order. */
7
+ const codeModeCatalog = (tools) => tools.map(({ def, moduleId, moduleDescription }) => {
8
+ const tool = {
9
+ name: def.name,
10
+ identifier: toCodemodeIdentifier(def.name),
11
+ namespace: moduleId,
12
+ description: def.description,
13
+ inputSchema: def.parameters,
14
+ outputSchema: def.outputSchema ?? textOutputSchema,
15
+ structured: def.outputSchema !== void 0,
16
+ exposure: toolDiscovery(def) ?? "all"
17
+ };
18
+ return moduleDescription === void 0 ? tool : {
19
+ ...tool,
20
+ namespaceDescription: moduleDescription
21
+ };
22
+ });
23
+ /** Default inline budget of the nested tool listing, in estimated tokens. */
24
+ const defaultCodeModeInlineBudget = 3e3;
25
+ /** Estimated tokens of a text: four characters per token, rounded up. */
26
+ const estimateCodeModeTokens = (text) => Math.ceil(text.length / 4);
27
+ const maxShortTypeChars = 60;
28
+ const shortType = (schema) => {
29
+ const rendered = renderToolOutputType(schema);
30
+ if (rendered.length <= maxShortTypeChars) return rendered;
31
+ if (!Predicate.isBoolean(schema)) {
32
+ if (schema.type === "array") return "Array<unknown>";
33
+ if (schema.type === "object") return "object";
34
+ }
35
+ return "unknown";
36
+ };
37
+ const declarationTool = (tool) => ({
38
+ name: tool.name,
39
+ description: tool.description,
40
+ inputSchema: tool.inputSchema,
41
+ outputSchema: tool.outputSchema,
42
+ execute: () => void 0
43
+ });
44
+ const directToolLine = (tool) => `- \`tools.${tool.identifier}(args)\` takes the arguments of the \`${tool.name}\` tool and resolves to \`${shortType(tool.outputSchema)}\`.`;
45
+ const declarationMembers = (tools) => renderDeclarations({ tools: tools.map(declarationTool) });
46
+ const candidateCost = (tool) => estimateCodeModeTokens(declarationMembers([tool]));
47
+ const groupByNamespace = (items) => {
48
+ const groups = /* @__PURE__ */ new Map();
49
+ for (const item of items) {
50
+ const group = groups.get(item.tool.namespace);
51
+ if (group === void 0) groups.set(item.tool.namespace, [item]);
52
+ else group.push(item);
53
+ }
54
+ return groups;
55
+ };
56
+ /**
57
+ * Chooses the tools the description lists. `all` tools are always placed, outside the budget
58
+ * (the model already has their declarations). `listed` tools fill `budget` estimated tokens fairly
59
+ * across namespaces: each round, every namespace still in play places its cheapest remaining tool;
60
+ * a namespace whose next tool does not fit drops out. `search` tools are never candidates.
61
+ */
62
+ const selectCodeModeListing = (catalog, budget) => {
63
+ const candidates = catalog.flatMap((tool, index) => tool.exposure === "listed" ? [{
64
+ tool,
65
+ index,
66
+ cost: candidateCost(tool)
67
+ }] : []);
68
+ const queues = new Map([...groupByNamespace(candidates)].map(([namespace, group]) => [namespace, [...group].sort((left, right) => left.cost - right.cost || left.index - right.index)]));
69
+ const placed = new Set(catalog.flatMap((tool, index) => tool.exposure === "all" ? [index] : []));
70
+ const inPlay = new Set(queues.keys());
71
+ let remaining = budget;
72
+ while (inPlay.size > 0) for (const [namespace, queue] of queues) {
73
+ if (!inPlay.has(namespace)) continue;
74
+ const next = queue.shift();
75
+ if (next === void 0 || next.cost > remaining) {
76
+ inPlay.delete(namespace);
77
+ continue;
78
+ }
79
+ placed.add(next.index);
80
+ remaining -= next.cost;
81
+ }
82
+ const unlisted = new Set(catalog.flatMap((tool, index) => placed.has(index) ? [] : [tool.namespace]));
83
+ return {
84
+ listed: catalog.filter((_, index) => placed.has(index)),
85
+ unlistedNamespaces: [...new Set(catalog.map((tool) => tool.namespace))].filter((namespace) => unlisted.has(namespace))
86
+ };
87
+ };
88
+ const intro = [
89
+ "Run a JavaScript script that calls tools and returns only what matters.",
90
+ "`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.",
91
+ "Call tools as `await tools.<id>(args)`. A call resolves to the declared result type and rejects with an Error when the tool fails. Calls still running when the script ends are cancelled, so await every call (use `Promise.all` for parallel calls).",
92
+ "Only the script output and its return value come back to you: filter and aggregate inside the script and return a small JSON value."
93
+ ].join("\n");
94
+ const globalLines = (store) => [
95
+ "- `text(value)` and `console.log(...values)`: append text to the output.",
96
+ "- `image(dataUrl)`: append a base64 image (a `data:` URL or `{ type: \"image\", data, mimeType }`).",
97
+ "- `exit()`: end the script successfully.",
98
+ "- `ALL_TOOLS`: `{ name, description }` of every callable tool.",
99
+ "- `await searchTools(query, { limit?, namespace? })`: find tools by topic; resolves to `Array<{ name: string; description: string }>` (default limit 8).",
100
+ "- `await describeTool(name)`: the description and TypeScript declaration of a tool, or `undefined`.",
101
+ "- `await describeNamespace(name)`: `{ name, description?, tools: Array<{ name, description }> }` for a namespace, or `undefined`.",
102
+ ...store ? ["- `store(key, value)` and `load(key)`: keep small JSON values for later scripts; writes are saved only when the script succeeds."] : []
103
+ ];
104
+ const namespaceSection = (namespace, tools) => {
105
+ const declared = tools.filter((tool) => tool.exposure !== "all");
106
+ const direct = tools.filter((tool) => tool.exposure === "all");
107
+ const description = tools.find((tool) => tool.namespaceDescription !== void 0)?.namespaceDescription;
108
+ return [
109
+ `### ${namespace}`,
110
+ ...description === void 0 ? [] : [description],
111
+ ...declared.length > 0 ? [
112
+ "```ts",
113
+ declarationMembers(declared),
114
+ "```"
115
+ ] : [],
116
+ ...direct.map(directToolLine)
117
+ ].join("\n");
118
+ };
119
+ const unlistedToolsLine = "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.";
120
+ /**
121
+ * The code mode tool description: intro, globals one per line, then nested tools grouped by
122
+ * namespace, then one fixed line pointing to `searchTools`/`describeTool`/`describeNamespace`.
123
+ * `all` tools get one line each outside the budget; `listed` tools are declared within the inline
124
+ * budget; `search` tools never contribute, so adding or removing them (even whole namespaces of
125
+ * them) leaves the text byte-identical.
126
+ */
127
+ const renderCodeModeDescription = (input) => {
128
+ const sections = [...groupByNamespace(selectCodeModeListing(codeModeCatalog(input.tools), input.inlineBudget ?? 3e3).listed.map((tool) => ({ tool })))].map(([namespace, items]) => namespaceSection(namespace, items.map((item) => item.tool)));
129
+ return [
130
+ intro,
131
+ ["Globals:", ...globalLines(input.store === true)].join("\n"),
132
+ sections.length === 0 ? "Nested tools: none are listed here." : ["## Nested tools by namespace", ...sections].join("\n\n"),
133
+ unlistedToolsLine
134
+ ].join("\n\n");
135
+ };
136
+ /** `describeTool` text: the description and TypeScript declaration of one tool. */
137
+ const describeCodeModeTool = (tool) => renderToolSample(declarationTool(tool));
138
+ /** Finds a tool by identifier or raw name. */
139
+ const findCodeModeTool = (catalog, name) => catalog.find((tool) => tool.identifier === name) ?? catalog.find((tool) => tool.name === name);
140
+ //#endregion
141
+ export { codeModeCatalog, defaultCodeModeInlineBudget, describeCodeModeTool, estimateCodeModeTokens, findCodeModeTool, renderCodeModeDescription, selectCodeModeListing };
142
+
143
+ //# sourceMappingURL=catalog.mjs.map
@@ -0,0 +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 /** 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/** Catalog of nested tools in resolution order. */\nexport const codeModeCatalog = (\n tools: ReadonlyArray<NestedTool>\n): ReadonlyArray<CodeModeCatalogTool> =>\n tools.map(({ def, moduleId, moduleDescription }) => {\n const tool: CodeModeCatalogTool = {\n name: def.name,\n identifier: toCodemodeIdentifier(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/** 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)`. A call resolves to the declared result type and rejects with an Error 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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"}
@@ -0,0 +1,78 @@
1
+ import { Effect } from "effect";
2
+ import * as Schema from "effect/Schema";
3
+ import { ClassifierModel } from "@yolk-sdk/agent/classification";
4
+ import { ToolRegistration } from "@yolk-sdk/agent/tools";
5
+
6
+ //#region src/classifier-tool.d.ts
7
+ /** Default name of the classifier tool. */
8
+ declare const classifierToolName = "classify";
9
+ /** Default cap of concurrent classifications per script. */
10
+ declare const defaultClassifierToolMaxConcurrency = 100;
11
+ /** Default cap of concurrent classifications per process, across scripts and registrations. */
12
+ declare const defaultClassifierProcessMaxConcurrency = 200;
13
+ /** A concurrency cap shared by every classifier tool registration that uses it. */
14
+ type ClassifierConcurrencyLimiter = {
15
+ /** Maximum concurrent classifications. */readonly max: number;
16
+ /** Runs `effect` holding one permit. Waiting is interruptible and never leaks a permit; the
17
+ * permit is released when `effect` ends, however it ends.
18
+ */
19
+ readonly withPermit: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
20
+ };
21
+ /** A classifier concurrency limiter allowing `max` (at least 1) classifications at once. */
22
+ declare const makeClassifierConcurrencyLimiter: (max: number) => ClassifierConcurrencyLimiter;
23
+ /** The process-wide limiter every classifier tool uses unless given `processLimiter`. Default
24
+ * 200 concurrent classifications (200 concurrent AI Gateway classifications finished in about
25
+ * 1.2 s without rate-limit errors in a live probe).
26
+ */
27
+ declare const defaultClassifierProcessLimiter: ClassifierConcurrencyLimiter;
28
+ type ClassifierModelService = (typeof ClassifierModel)['Service'];
29
+ type Classify = ClassifierModelService['classify'];
30
+ type MakeClassifierToolOptions = {
31
+ /** `ClassifierModel`'s `classify`, or the service value itself. */readonly classify: Classify | ClassifierModelService; /** Default `classify`. */
32
+ readonly name?: string;
33
+ /** Concurrent classifications per script (keyed by the parent tool call id of the nested call
34
+ * id `<parentToolCallId>/<seq>`, or the call id itself); default 100.
35
+ */
36
+ readonly maxConcurrency?: number;
37
+ /** Limiter shared across scripts and registrations; default `defaultClassifierProcessLimiter`
38
+ * (200 per process). `false` disables the process cap (the per-script cap still applies).
39
+ */
40
+ readonly processLimiter?: ClassifierConcurrencyLimiter | false;
41
+ readonly description?: string;
42
+ };
43
+ /** The classifier tool's input: one state and its named questions. Provider options stay
44
+ * host-owned and are never taken from scripts.
45
+ */
46
+ declare const ClassifierToolParams: Schema.Struct<{
47
+ readonly state: Schema.Union<readonly [Schema.String, Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>, Schema.$Array<Schema.Codec<Schema.Json, Schema.Json, never, never>>]>;
48
+ readonly questions: Schema.$Record<Schema.NonEmptyString, Schema.Union<readonly [Schema.Struct<{
49
+ readonly type: Schema.Literal<"boolean">;
50
+ readonly instructions: Schema.NonEmptyString;
51
+ readonly criteria: Schema.optionalKey<Schema.Struct<{
52
+ readonly true: Schema.String;
53
+ readonly false: Schema.String;
54
+ }>>;
55
+ }>, Schema.Struct<{
56
+ readonly type: Schema.Literal<"choice">;
57
+ readonly instructions: Schema.NonEmptyString;
58
+ readonly criteria: Schema.$Record<Schema.NonEmptyString, Schema.Codec<Schema.Json, Schema.Json, never, never>>;
59
+ }>, Schema.Struct<{
60
+ readonly type: Schema.Literal<"score">;
61
+ readonly instructions: Schema.NonEmptyString;
62
+ readonly criteria: Schema.NonEmptyArray<Schema.Codec<Schema.Json, Schema.Json, never, never>>;
63
+ }>]>>;
64
+ }>;
65
+ /**
66
+ * A code-mode-only (`callableBy: 'codemode'`, `discovery: 'listed'`) read tool that classifies one
67
+ * item per call with a classifier model. Its content is the compact JSON of the answers, its
68
+ * `structuredContent` the full `ClassificationResult` (which nested calls resolve to), and its
69
+ * token usage is reported on `ToolResult.usage`. Classifier errors become model-visible error
70
+ * results; usage billed before a response error is kept.
71
+ *
72
+ * Each call takes a per-script permit (`maxConcurrency`) and then a process permit
73
+ * (`processLimiter`), so one script's queue never holds process permits it cannot use.
74
+ */
75
+ declare const makeClassifierTool: <Context>(options: MakeClassifierToolOptions) => ToolRegistration<Context>;
76
+ //#endregion
77
+ export { ClassifierConcurrencyLimiter, ClassifierToolParams, MakeClassifierToolOptions, classifierToolName, defaultClassifierProcessLimiter, defaultClassifierProcessMaxConcurrency, defaultClassifierToolMaxConcurrency, makeClassifierConcurrencyLimiter, makeClassifierTool };
78
+ //# sourceMappingURL=classifier-tool.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classifier-tool.d.mts","names":[],"sources":["../src/classifier-tool.ts"],"mappings":";;;;;;;cA2Ba,kBAAA;AAAb;AAAA,cAGa,mCAAA;;cAGA,sCAAA;AANkB;AAAA,KASnB,4BAAA;EANoC,mDAQrC,GAAA;EARqC;AAAA;AAGhD;EAHgD,SAYrC,UAAA,YAAsB,MAAA,EAAQ,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,CAAA,EAAG,CAAA,MAAO,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,CAAA,EAAG,CAAA;AAAA;;cAM3E,gCAAA,GAAoC,GAAA,aAAc,4BAK9D;AAjBD;;;;AAAA,cAuBa,+BAAA,EAA+B,4BAE3C;AAAA,KAEI,sBAAA,WAAiC,eAAe;AAAA,KAEhD,QAAA,GAAW,sBAAsB;AAAA,KAE1B,yBAAA;EAzByE,4EA2B1E,QAAA,EAAU,QAAA,GAAW,sBAAA,EA3BoC;EAAA,SA6BzD,IAAA;EA7BsE;;;EAAA,SAiCtE,cAAA;EAjCgB;;;EAAA,SAqChB,cAAA,GAAiB,4BAAA;EAAA,SACjB,WAAA;AAAA;;;;cAME,oBAAA,EAAoB,MAAA,CAAA,MAAA;EAAA;;;;;;;;;;;;;;;;;;;AAvBoB;AAAA;;;;AAEf;AAEtC;;;cAkGa,kBAAA,YACX,OAAA,EAAS,yBAAA,KACR,gBAAA,CAAiB,OAAA"}
@@ -0,0 +1,125 @@
1
+ import { Effect, Predicate, Semaphore } from "effect";
2
+ import { AgentInputUsage, AgentOutputUsage, AgentUsage, ToolResult } from "@yolk-sdk/agent/protocol";
3
+ import * as Schema from "effect/Schema";
4
+ import { ClassificationResult, ClassifierQuestions, ClassifierState } from "@yolk-sdk/agent/classification";
5
+ import { makeTool, modelVisibleToolError, modelVisibleToolErrorStructuredContent } from "@yolk-sdk/agent/tools";
6
+ //#region src/classifier-tool.ts
7
+ /** Default name of the classifier tool. */
8
+ const classifierToolName = "classify";
9
+ /** Default cap of concurrent classifications per script. */
10
+ const defaultClassifierToolMaxConcurrency = 100;
11
+ /** Default cap of concurrent classifications per process, across scripts and registrations. */
12
+ const defaultClassifierProcessMaxConcurrency = 200;
13
+ const permitCount = (max) => Math.max(1, Math.floor(max));
14
+ /** A classifier concurrency limiter allowing `max` (at least 1) classifications at once. */
15
+ const makeClassifierConcurrencyLimiter = (max) => {
16
+ const permits = permitCount(max);
17
+ const semaphore = Semaphore.makeUnsafe(permits);
18
+ return {
19
+ max: permits,
20
+ withPermit: (effect) => semaphore.withPermits(1)(effect)
21
+ };
22
+ };
23
+ /** The process-wide limiter every classifier tool uses unless given `processLimiter`. Default
24
+ * 200 concurrent classifications (200 concurrent AI Gateway classifications finished in about
25
+ * 1.2 s without rate-limit errors in a live probe).
26
+ */
27
+ const defaultClassifierProcessLimiter = makeClassifierConcurrencyLimiter(200);
28
+ /** The classifier tool's input: one state and its named questions. Provider options stay
29
+ * host-owned and are never taken from scripts.
30
+ */
31
+ const ClassifierToolParams = Schema.Struct({
32
+ state: ClassifierState,
33
+ questions: ClassifierQuestions
34
+ });
35
+ const defaultDescription = (maxConcurrency) => [
36
+ "Classify one item with a classifier model: answer named typed questions about one `state` (a string, JSON object, or JSON array) with probabilities.",
37
+ "Question types: `boolean` (`probability` of true), `choice` (one of the `criteria` keys, with `probabilities`), and `score` (an ordinal level from the `criteria` list, lowest first).",
38
+ `Call it once per item; at most ${maxConcurrency} classifications of one script run at once and further calls queue.`
39
+ ].join(" ");
40
+ /** Token usage of a classification as agent usage. `costUsd` has no agent usage field; it stays
41
+ * in the result's `structuredContent.usage`, so nested-call records carry token usage only.
42
+ */
43
+ const agentUsage = (usage) => AgentUsage.make({
44
+ input: AgentInputUsage.make({ total: usage.inputTokens }),
45
+ output: AgentOutputUsage.make({ total: usage.outputTokens })
46
+ });
47
+ const errorReason = (error) => Predicate.isTagged(error, "ClassificationRequestInvalid") ? "invalid_input" : "unavailable";
48
+ const billedUsage = (error) => Predicate.isTagged(error, "ClassificationResponseInvalid") ? error.usage : void 0;
49
+ /** The parent tool call id of a nested call id `<parentToolCallId>/<seq>`, else the id itself. */
50
+ const scriptKey = (call) => {
51
+ const separator = call.id.lastIndexOf("/");
52
+ return separator > 0 ? call.id.slice(0, separator) : call.id;
53
+ };
54
+ const withUsage = (fields, usage) => {
55
+ if (usage !== void 0) fields.usage = agentUsage(usage);
56
+ return ToolResult.make(fields);
57
+ };
58
+ const successResult = (call, result) => withUsage({
59
+ toolCallId: call.id,
60
+ content: JSON.stringify(result.answers),
61
+ structuredContent: result
62
+ }, result.usage);
63
+ const errorResult = (call, name, error) => withUsage({
64
+ toolCallId: call.id,
65
+ content: error.message,
66
+ isError: true,
67
+ structuredContent: modelVisibleToolErrorStructuredContent(modelVisibleToolError({
68
+ tool: name,
69
+ reason: errorReason(error),
70
+ message: error.message
71
+ }))
72
+ }, billedUsage(error));
73
+ /**
74
+ * A code-mode-only (`callableBy: 'codemode'`, `discovery: 'listed'`) read tool that classifies one
75
+ * item per call with a classifier model. Its content is the compact JSON of the answers, its
76
+ * `structuredContent` the full `ClassificationResult` (which nested calls resolve to), and its
77
+ * token usage is reported on `ToolResult.usage`. Classifier errors become model-visible error
78
+ * results; usage billed before a response error is kept.
79
+ *
80
+ * Each call takes a per-script permit (`maxConcurrency`) and then a process permit
81
+ * (`processLimiter`), so one script's queue never holds process permits it cannot use.
82
+ */
83
+ const makeClassifierTool = (options) => {
84
+ const name = options.name ?? "classify";
85
+ const maxConcurrency = permitCount(options.maxConcurrency ?? 100);
86
+ const processLimiter = options.processLimiter === void 0 ? defaultClassifierProcessLimiter : options.processLimiter;
87
+ const withProcessPermit = (effect) => processLimiter === false ? effect : processLimiter.withPermit(effect);
88
+ const classify = Predicate.isFunction(options.classify) ? options.classify : options.classify.classify;
89
+ const scripts = /* @__PURE__ */ new Map();
90
+ const withScriptPermit = (call, effect) => Effect.acquireUseRelease(Effect.sync(() => {
91
+ const key = scriptKey(call);
92
+ const entry = scripts.get(key) ?? {
93
+ semaphore: Semaphore.makeUnsafe(maxConcurrency),
94
+ users: 0
95
+ };
96
+ entry.users++;
97
+ scripts.set(key, entry);
98
+ return {
99
+ key,
100
+ entry
101
+ };
102
+ }), ({ entry }) => entry.semaphore.withPermits(1)(withProcessPermit(effect)), ({ key, entry }) => Effect.sync(() => {
103
+ entry.users--;
104
+ if (entry.users === 0 && scripts.get(key) === entry) scripts.delete(key);
105
+ }));
106
+ return makeTool({
107
+ name,
108
+ description: options.description ?? defaultDescription(maxConcurrency),
109
+ parameters: ClassifierToolParams,
110
+ output: ClassificationResult,
111
+ access: "read",
112
+ callableBy: "codemode",
113
+ discovery: "listed",
114
+ execute: ({ call, params }) => {
115
+ return withScriptPermit(call, classify({
116
+ state: params.state,
117
+ questions: params.questions
118
+ })).pipe(Effect.map((result) => successResult(call, result)), Effect.catch((error) => Effect.succeed(errorResult(call, name, error))));
119
+ }
120
+ });
121
+ };
122
+ //#endregion
123
+ export { ClassifierToolParams, classifierToolName, defaultClassifierProcessLimiter, defaultClassifierProcessMaxConcurrency, defaultClassifierToolMaxConcurrency, makeClassifierConcurrencyLimiter, makeClassifierTool };
124
+
125
+ //# sourceMappingURL=classifier-tool.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classifier-tool.mjs","names":[],"sources":["../src/classifier-tool.ts"],"sourcesContent":["import { Effect, Predicate, Semaphore } from 'effect'\nimport * as Schema from 'effect/Schema'\nimport {\n ClassificationResult,\n ClassifierQuestions,\n ClassifierState,\n type ClassificationError,\n type ClassificationRequest,\n type ClassificationUsage,\n type ClassifierModel\n} from '@yolk-sdk/agent/classification'\nimport {\n AgentInputUsage,\n AgentOutputUsage,\n AgentUsage,\n ToolResult,\n type ToolCall\n} from '@yolk-sdk/agent/protocol'\nimport {\n makeTool,\n modelVisibleToolError,\n modelVisibleToolErrorStructuredContent,\n type ModelVisibleToolErrorReason,\n type ToolRegistration\n} from '@yolk-sdk/agent/tools'\n\n/** Default name of the classifier tool. */\nexport const classifierToolName = 'classify'\n\n/** Default cap of concurrent classifications per script. */\nexport const defaultClassifierToolMaxConcurrency = 100\n\n/** Default cap of concurrent classifications per process, across scripts and registrations. */\nexport const defaultClassifierProcessMaxConcurrency = 200\n\n/** A concurrency cap shared by every classifier tool registration that uses it. */\nexport type ClassifierConcurrencyLimiter = {\n /** Maximum concurrent classifications. */\n readonly max: number\n /** Runs `effect` holding one permit. Waiting is interruptible and never leaks a permit; the\n * permit is released when `effect` ends, however it ends.\n */\n readonly withPermit: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>\n}\n\nconst permitCount = (max: number) => Math.max(1, Math.floor(max))\n\n/** A classifier concurrency limiter allowing `max` (at least 1) classifications at once. */\nexport const makeClassifierConcurrencyLimiter = (max: number): ClassifierConcurrencyLimiter => {\n const permits = permitCount(max)\n const semaphore = Semaphore.makeUnsafe(permits)\n\n return { max: permits, withPermit: effect => semaphore.withPermits(1)(effect) }\n}\n\n/** The process-wide limiter every classifier tool uses unless given `processLimiter`. Default\n * 200 concurrent classifications (200 concurrent AI Gateway classifications finished in about\n * 1.2 s without rate-limit errors in a live probe).\n */\nexport const defaultClassifierProcessLimiter = makeClassifierConcurrencyLimiter(\n defaultClassifierProcessMaxConcurrency\n)\n\ntype ClassifierModelService = (typeof ClassifierModel)['Service']\n\ntype Classify = ClassifierModelService['classify']\n\nexport type MakeClassifierToolOptions = {\n /** `ClassifierModel`'s `classify`, or the service value itself. */\n readonly classify: Classify | ClassifierModelService\n /** Default `classify`. */\n readonly name?: string\n /** Concurrent classifications per script (keyed by the parent tool call id of the nested call\n * id `<parentToolCallId>/<seq>`, or the call id itself); default 100.\n */\n readonly maxConcurrency?: number\n /** Limiter shared across scripts and registrations; default `defaultClassifierProcessLimiter`\n * (200 per process). `false` disables the process cap (the per-script cap still applies).\n */\n readonly processLimiter?: ClassifierConcurrencyLimiter | false\n readonly description?: string\n}\n\n/** The classifier tool's input: one state and its named questions. Provider options stay\n * host-owned and are never taken from scripts.\n */\nexport const ClassifierToolParams = Schema.Struct({\n state: ClassifierState,\n questions: ClassifierQuestions\n})\n\nconst defaultDescription = (maxConcurrency: number) =>\n [\n 'Classify one item with a classifier model: answer named typed questions about one `state` (a string, JSON object, or JSON array) with probabilities.',\n 'Question types: `boolean` (`probability` of true), `choice` (one of the `criteria` keys, with `probabilities`), and `score` (an ordinal level from the `criteria` list, lowest first).',\n `Call it once per item; at most ${maxConcurrency} classifications of one script run at once and further calls queue.`\n ].join(' ')\n\n/** Token usage of a classification as agent usage. `costUsd` has no agent usage field; it stays\n * in the result's `structuredContent.usage`, so nested-call records carry token usage only.\n */\nconst agentUsage = (usage: ClassificationUsage): AgentUsage =>\n AgentUsage.make({\n input: AgentInputUsage.make({ total: usage.inputTokens }),\n output: AgentOutputUsage.make({ total: usage.outputTokens })\n })\n\nconst errorReason = (error: ClassificationError): ModelVisibleToolErrorReason =>\n Predicate.isTagged(error, 'ClassificationRequestInvalid') ? 'invalid_input' : 'unavailable'\n\nconst billedUsage = (error: ClassificationError) =>\n Predicate.isTagged(error, 'ClassificationResponseInvalid') ? error.usage : undefined\n\n/** The parent tool call id of a nested call id `<parentToolCallId>/<seq>`, else the id itself. */\nconst scriptKey = (call: ToolCall) => {\n const separator = call.id.lastIndexOf('/')\n\n return separator > 0 ? call.id.slice(0, separator) : call.id\n}\n\ntype ResultFields = {\n toolCallId: string\n content: string\n isError?: boolean\n structuredContent: unknown\n usage?: AgentUsage\n}\n\nconst withUsage = (fields: ResultFields, usage: ClassificationUsage | undefined) => {\n if (usage !== undefined) {\n fields.usage = agentUsage(usage)\n }\n\n return ToolResult.make(fields)\n}\n\nconst successResult = (call: ToolCall, result: ClassificationResult) =>\n withUsage(\n { toolCallId: call.id, content: JSON.stringify(result.answers), structuredContent: result },\n result.usage\n )\n\nconst errorResult = (call: ToolCall, name: string, error: ClassificationError) =>\n withUsage(\n {\n toolCallId: call.id,\n content: error.message,\n isError: true,\n structuredContent: modelVisibleToolErrorStructuredContent(\n modelVisibleToolError({ tool: name, reason: errorReason(error), message: error.message })\n )\n },\n billedUsage(error)\n )\n\n/**\n * A code-mode-only (`callableBy: 'codemode'`, `discovery: 'listed'`) read tool that classifies one\n * item per call with a classifier model. Its content is the compact JSON of the answers, its\n * `structuredContent` the full `ClassificationResult` (which nested calls resolve to), and its\n * token usage is reported on `ToolResult.usage`. Classifier errors become model-visible error\n * results; usage billed before a response error is kept.\n *\n * Each call takes a per-script permit (`maxConcurrency`) and then a process permit\n * (`processLimiter`), so one script's queue never holds process permits it cannot use.\n */\nexport const makeClassifierTool = <Context>(\n options: MakeClassifierToolOptions\n): ToolRegistration<Context> => {\n const name = options.name ?? classifierToolName\n\n const maxConcurrency = permitCount(options.maxConcurrency ?? defaultClassifierToolMaxConcurrency)\n\n const processLimiter =\n options.processLimiter === undefined ? defaultClassifierProcessLimiter : options.processLimiter\n\n const withProcessPermit = <A, E>(effect: Effect.Effect<A, E>) =>\n processLimiter === false ? effect : processLimiter.withPermit(effect)\n\n const classify = Predicate.isFunction(options.classify)\n ? options.classify\n : options.classify.classify\n\n // One semaphore per script (parent tool call), removed when its last classification ends.\n const scripts = new Map<string, { readonly semaphore: Semaphore.Semaphore; users: number }>()\n\n const withScriptPermit = <A, E>(call: ToolCall, effect: Effect.Effect<A, E>) =>\n Effect.acquireUseRelease(\n Effect.sync(() => {\n const key = scriptKey(call)\n\n const entry = scripts.get(key) ?? {\n semaphore: Semaphore.makeUnsafe(maxConcurrency),\n users: 0\n }\n\n entry.users++\n scripts.set(key, entry)\n\n return { key, entry }\n }),\n ({ entry }) => entry.semaphore.withPermits(1)(withProcessPermit(effect)),\n ({ key, entry }) =>\n Effect.sync(() => {\n entry.users--\n\n if (entry.users === 0 && scripts.get(key) === entry) scripts.delete(key)\n })\n )\n\n return makeTool<Context, typeof ClassifierToolParams>({\n name,\n description: options.description ?? defaultDescription(maxConcurrency),\n parameters: ClassifierToolParams,\n output: ClassificationResult,\n access: 'read',\n callableBy: 'codemode',\n discovery: 'listed',\n execute: ({ call, params }) => {\n const request: ClassificationRequest = { state: params.state, questions: params.questions }\n\n return withScriptPermit(call, classify(request)).pipe(\n Effect.map(result => successResult(call, result)),\n Effect.catch(error => Effect.succeed(errorResult(call, name, error)))\n )\n }\n })\n}\n"],"mappings":"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"}