@yolk-sdk/codemode 0.1.0-canary.100

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 +174 -0
  3. package/dist/catalog.d.mts +74 -0
  4. package/dist/catalog.d.mts.map +1 -0
  5. package/dist/catalog.mjs +164 -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 +108 -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 +35 -0
  17. package/dist/node.d.mts.map +1 -0
  18. package/dist/node.mjs +393 -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 +77 -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 +103 -0
  33. package/dist/tool.d.mts.map +1 -0
  34. package/dist/tool.mjs +392 -0
  35. package/dist/tool.mjs.map +1 -0
  36. package/package.json +63 -0
  37. package/src/catalog.ts +305 -0
  38. package/src/classifier-tool.ts +227 -0
  39. package/src/executor.ts +122 -0
  40. package/src/index.ts +87 -0
  41. package/src/node.ts +546 -0
  42. package/src/output.ts +287 -0
  43. package/src/search.ts +113 -0
  44. package/src/store.ts +135 -0
  45. package/src/tool.ts +766 -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,174 @@
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
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`) 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. The record keeps up to `maxNestedCalls` calls, at most 4096
59
+ (`nestedToolCallMaxRecordedCalls`; argument byte budgets still apply), and per-status `counts`
60
+ over every call. Nested results never reach the model; only the script output and return value do.
61
+ - A call resolves to `structuredContent` for tools with an output schema and to the text content
62
+ otherwise; error results, `beforeNestedCall` failures, and calls past `maxNestedCalls` reject
63
+ with an `Error` whose message is `tools.<id>: <text>`, or `tools["<name>"]: <text>` for a tool
64
+ whose identifier an earlier tool already uses (the nested-call record keeps the raw text). Calls
65
+ still running when the script ends are cancelled and recorded as `cancelled`.
66
+ - Arguments make a JSON round trip: `undefined` keys and `null` optionals are absent, unknown keys
67
+ reject with a hint naming the allowed keys, and values JSON would silently change (`NaN`,
68
+ `Infinity`, invalid `Date`s, `undefined` array items, `Map`, `Set`, functions, class instances)
69
+ reject in the script before the call, with messages such as
70
+ `tools.search: argument at limit is NaN; pass a finite number or omit the key`. The pi executor
71
+ reads the argument once (getters and `toJSON` run once) into a checked JSON copy and sends that
72
+ copy; a valid `Date` passes as its ISO string. Arguments with more than 100,000 values or more
73
+ than 64 levels of nesting are rejected, never sent unchecked. Custom executors follow the same
74
+ rules (`CodeModeExecutorTool`) and label rejections with `CodeModeExecutorTool.callLabel`, which
75
+ `makeCodeModeTool` sets.
76
+ - The tool description lists the globals and the nested tools by namespace, with the module's
77
+ `ToolModule.description` under each heading. `codemode` + `listed` tools get TypeScript
78
+ declarations within `inlineBudget` (default 3,000 estimated tokens, filled fairly across
79
+ namespaces); `callableBy: 'all'` tools get one line each outside the budget; `codemode` +
80
+ `search` tools contribute nothing (no headings or hints), so the description stays
81
+ byte-identical when they, or whole namespaces of them, change. One fixed line tells scripts to
82
+ find unlisted tools with `searchTools(query, { limit?, namespace? })` (BM25 over names,
83
+ descriptions, namespaces, and module descriptions), `describeTool(name)`, and
84
+ `describeNamespace(name)`.
85
+ - Results start with `Script completed` or `Script failed`, include the wall time, output, and the
86
+ JSON return value, and are cut head and tail at `maxOutputChars`. Images beyond `maxImages` or
87
+ `maxImageBytes` are dropped with a note. Failed scripts keep partial output and list the tool
88
+ calls already made (they are not undone).
89
+ - `store(key, value)` writes of successful scripts are returned in
90
+ `structuredContent.codemode.storeWrites`. `codeModeStoreFromToolResults(entries, { toolName? })`
91
+ rebuilds the store for `loadStore` from `{ toolName, result }` entries, oldest first; it applies
92
+ only results of the code mode tool (default `codemode`), so other tools cannot spoof writes, and
93
+ keeps the 256 KiB-per-value and 1 MiB-total bounds by dropping offending writes. Transcript
94
+ `ToolResultMessage`s carry no tool name: pair each with its assistant tool call's name.
95
+ - `beforeNestedCall({ call, context })` runs before each nested call; a failure rejects that call in
96
+ the script with `<label>: <message>` and records it as `error` without executing it.
97
+ - `afterNestedCall({ call, outcome, durationMs, context, result? })` runs after each admitted
98
+ nested call with `success`, `failure` (error result or unexpected failure), or `interrupted`
99
+ (cancelled, for example still running when the script ended). Calls rejected before running are
100
+ not reported. A failing hook (a failure, a defect, or a synchronous throw) is logged and never
101
+ changes the call's result.
102
+ - A nested call that is interrupted rejects with `<label> was cancelled.`; a defect rejects with
103
+ `<label> failed unexpectedly.` (the same label as above). If an executor misses its deadline, the tool returns a `timeout` failure
104
+ `timeoutMs` + 5 s after the start and aborts it.
105
+
106
+ ## Re-execution and the tool ledger
107
+
108
+ Hosts that re-execute steps (Vercel Workflow's queue is at-least-once) pass a durable ledger to
109
+ `resolveTools(modules, context, { ledger: { store } })` (see `@yolk-sdk/agent/tools`). The
110
+ `codemode` call is itself ledgered, so a re-executed call never runs its script again:
111
+
112
+ - completed: the stored result is returned;
113
+ - still running elsewhere: the call waits for it (up to `maxWaitMs`/`deadline`);
114
+ - abandoned (the earlier execution crashed): an interrupted error result lists that script's
115
+ ledgered nested calls as applied, failed, or unknown (started, no recorded result), states that
116
+ they were not undone, and asks the model to verify. Hosts get the same entries in
117
+ `structuredContent.codemode` without calling the store again:
118
+ `{ ok: false, interrupted: true, interruptedCalls: { calls, complete, counts } }`, where each call
119
+ is `{ key, toolName, args, status: 'applied' | 'failed' | 'unknown' }` (`args` compact JSON cut
120
+ with a trailing `…`), bounded like `nestedCalls` (`maxNestedCalls` calls, 8 KiB of arguments per
121
+ call, 32 KiB in total; `complete: false` when cut, here or by the ledger), and `counts` covers
122
+ every entry. Types: `CodeModeInterruptedCalls`, `CodeModeInterruptedCall`,
123
+ `CodeModeInterruptedCallStatus`. When the ledger cannot list the nested calls, the result stays
124
+ interrupted, says they may already have been applied, and sets
125
+ `interruptedCallsUnavailable: true` instead of `interruptedCalls`.
126
+
127
+ Nested write calls are ledgered under `<toolCallId>/<seq>` and receive a stable `idempotencyKey`.
128
+ Calls the ledger policy skips (by default read-only calls) are not listed.
129
+
130
+ ## Limits
131
+
132
+ | Limit | Default | Notes |
133
+ | ------------------ | ------- | ------------------------------------------------------------------------ |
134
+ | `timeoutMs` | 120000 | Clamped to `deadline(context)` minus 5 s, never below 1 s |
135
+ | `memoryLimitBytes` | 64 MiB | QuickJS heap cap |
136
+ | `maxNestedCalls` | 256 | Further calls reject; also sizes the `nestedCalls` record (at most 4096) |
137
+ | `maxOutputChars` | 40000 | Head-and-tail cut with an omission marker |
138
+ | `maxImages` | 8 | Later images are dropped with a note |
139
+ | `maxImageBytes` | 4 MiB | Base64 characters of images in total |
140
+
141
+ ## Classifier tool
142
+
143
+ `makeClassifierTool({ classify })` wraps a `ClassifierModel` from `@yolk-sdk/agent/classification`
144
+ as a `codemode` + `listed` read tool (default name `classify`). Scripts classify one item per call;
145
+ calls beyond `maxConcurrency` (default 100 per script, keyed by the parent tool call id) queue.
146
+ Each call then takes a permit from `processLimiter`, shared across scripts and registrations:
147
+ `defaultClassifierProcessLimiter` (200 per process) unless you pass one built with
148
+ `makeClassifierConcurrencyLimiter(max)` or `false`. An interrupted waiting call releases its
149
+ permits. Nested-call records carry token usage; cost stays in the result's
150
+ `structuredContent.usage`.
151
+
152
+ ## Host responsibilities
153
+
154
+ - Which tools are callable from scripts (`callableBy`, `discovery`), approvals, and policy.
155
+ - Run authority for nested calls: decorators around the `ToolExecutor` (outside
156
+ `ResolvedToolSet.execute`) do not see nested calls. Use `beforeNestedCall` or registration-level
157
+ wrappers for per-call checks.
158
+ Approval, input, interaction, background, `question`, and `subagent` tools never run from code
159
+ mode.
160
+ - Where the tool is advertised (voice sessions do not get it in v1), deadlines, and limits.
161
+ - Next.js: `serverExternalPackages: ['@yolk-sdk/codemode', '@earendil-works/pi-codemode', 'quickjs-wasi']`.
162
+ - Vercel Workflow: run the code mode call inside the tool-batch step (`'use step'`), never inside a
163
+ `'use workflow'` function, and supply a durable tool ledger: steps are at-least-once.
164
+
165
+ ## Boundaries
166
+
167
+ - The root is runtime-neutral (no Node builtins) and depends only on `@yolk-sdk/agent` and the pure
168
+ declaration renderer of `@earendil-works/pi-codemode`; the worker-thread engine lives behind
169
+ `./node`. `@yolk-sdk/agent` never imports code mode.
170
+ - The pi worker inherits a copy of `process.env`; scripts cannot read it (the VM has no `process`).
171
+ - pi buffers script text and image output on the host thread without a limit while the script
172
+ runs; only the timeout bounds it. Results are bounded afterwards (`maxOutputChars`,
173
+ `maxImages`, `maxImageBytes`).
174
+ - `stripTypeScriptTypes` is experimental in Node and prints one `ExperimentalWarning` per process.
@@ -0,0 +1,74 @@
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;
13
+ /**
14
+ * How scripts call this tool, used to prefix its rejections: `tools.<identifier>`, or
15
+ * `tools["<name>"]` when an earlier tool already has the identifier (see `codeModeCallLabels`;
16
+ * when the name is taken too, the tool is unreachable and the label only names it).
17
+ */
18
+ readonly callLabel: string; /** The tool's `ToolModule.id`. */
19
+ readonly namespace: string; /** The module's `ToolModule.description`, when set. */
20
+ readonly namespaceDescription?: string;
21
+ readonly description: string;
22
+ readonly inputSchema: CodeModeJsonSchema; /** The declared output schema, or `{ type: 'string' }` for text results. */
23
+ readonly outputSchema: CodeModeJsonSchema; /** True when the tool declares an output schema (calls resolve to `structuredContent`). */
24
+ readonly structured: boolean;
25
+ readonly exposure: CodeModeToolExposure;
26
+ };
27
+ /**
28
+ * The call label of each tool name, in order, as pi binds `tools`: each tool takes
29
+ * `tools[<identifier>]` unless an earlier tool holds that key (first tool wins), then
30
+ * `tools[<name>]` if still free. The label is `tools.<identifier>` for the tool that holds its
31
+ * identifier, otherwise `tools["<name>"]`. The executor guard and the host rejection prefix share
32
+ * it, so both name a call the same way.
33
+ */
34
+ declare const codeModeCallLabels: (names: ReadonlyArray<string>) => ReadonlyArray<string>;
35
+ /** Catalog of nested tools in resolution order. */
36
+ declare const codeModeCatalog: (tools: ReadonlyArray<NestedTool>) => ReadonlyArray<CodeModeCatalogTool>;
37
+ /** Default inline budget of the nested tool listing, in estimated tokens. */
38
+ declare const defaultCodeModeInlineBudget = 3000;
39
+ /** Estimated tokens of a text: four characters per token, rounded up. */
40
+ declare const estimateCodeModeTokens: (text: string) => number;
41
+ type CodeModeListing = {
42
+ /** Tools placed in the description, in catalog order: every `all` tool, and the `listed` tools
43
+ * that fit the budget.
44
+ */
45
+ readonly listed: ReadonlyArray<CodeModeCatalogTool>; /** Namespaces (catalog order) with callable tools the description does not list. */
46
+ readonly unlistedNamespaces: ReadonlyArray<string>;
47
+ };
48
+ /**
49
+ * Chooses the tools the description lists. `all` tools are always placed, outside the budget
50
+ * (the model already has their declarations). `listed` tools fill `budget` estimated tokens fairly
51
+ * across namespaces: each round, every namespace still in play places its cheapest remaining tool;
52
+ * a namespace whose next tool does not fit drops out. `search` tools are never candidates.
53
+ */
54
+ declare const selectCodeModeListing: (catalog: ReadonlyArray<CodeModeCatalogTool>, budget: number) => CodeModeListing;
55
+ type CodeModeDescriptionInput = {
56
+ readonly tools: ReadonlyArray<NestedTool>;
57
+ readonly inlineBudget?: number; /** Mention `store()`/`load()` persistence. */
58
+ readonly store?: boolean;
59
+ };
60
+ /**
61
+ * The code mode tool description: intro, globals one per line, then nested tools grouped by
62
+ * namespace, then one fixed line pointing to `searchTools`/`describeTool`/`describeNamespace`.
63
+ * `all` tools get one line each outside the budget; `listed` tools are declared within the inline
64
+ * budget; `search` tools never contribute, so adding or removing them (even whole namespaces of
65
+ * them) leaves the text byte-identical.
66
+ */
67
+ declare const renderCodeModeDescription: (input: CodeModeDescriptionInput) => string;
68
+ /** `describeTool` text: the description and TypeScript declaration of one tool. */
69
+ declare const describeCodeModeTool: (tool: CodeModeCatalogTool) => string;
70
+ /** Finds a tool by identifier or raw name. */
71
+ declare const findCodeModeTool: (catalog: ReadonlyArray<CodeModeCatalogTool>, name: string) => CodeModeCatalogTool | undefined;
72
+ //#endregion
73
+ export { CodeModeCatalogTool, CodeModeDescriptionInput, CodeModeListing, CodeModeToolExposure, codeModeCallLabels, codeModeCatalog, defaultCodeModeInlineBudget, describeCodeModeTool, estimateCodeModeTokens, findCodeModeTool, renderCodeModeDescription, selectCodeModeListing };
74
+ //# 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;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"}
@@ -0,0 +1,164 @@
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
+ /**
7
+ * The call label of each tool name, in order, as pi binds `tools`: each tool takes
8
+ * `tools[<identifier>]` unless an earlier tool holds that key (first tool wins), then
9
+ * `tools[<name>]` if still free. The label is `tools.<identifier>` for the tool that holds its
10
+ * identifier, otherwise `tools["<name>"]`. The executor guard and the host rejection prefix share
11
+ * it, so both name a call the same way.
12
+ */
13
+ const codeModeCallLabels = (names) => {
14
+ const bound = /* @__PURE__ */ new Set();
15
+ return names.map((name) => {
16
+ const identifier = toCodemodeIdentifier(name);
17
+ const ownsIdentifier = !bound.has(identifier);
18
+ bound.add(identifier);
19
+ bound.add(name);
20
+ return ownsIdentifier ? `tools.${identifier}` : `tools[${JSON.stringify(name)}]`;
21
+ });
22
+ };
23
+ /** Catalog of nested tools in resolution order. */
24
+ const codeModeCatalog = (tools) => {
25
+ const labels = codeModeCallLabels(tools.map(({ def }) => def.name));
26
+ return tools.map(({ def, moduleId, moduleDescription }, index) => {
27
+ const tool = {
28
+ name: def.name,
29
+ identifier: toCodemodeIdentifier(def.name),
30
+ callLabel: labels[index] ?? `tools[${JSON.stringify(def.name)}]`,
31
+ namespace: moduleId,
32
+ description: def.description,
33
+ inputSchema: def.parameters,
34
+ outputSchema: def.outputSchema ?? textOutputSchema,
35
+ structured: def.outputSchema !== void 0,
36
+ exposure: toolDiscovery(def) ?? "all"
37
+ };
38
+ return moduleDescription === void 0 ? tool : {
39
+ ...tool,
40
+ namespaceDescription: moduleDescription
41
+ };
42
+ });
43
+ };
44
+ /** Default inline budget of the nested tool listing, in estimated tokens. */
45
+ const defaultCodeModeInlineBudget = 3e3;
46
+ /** Estimated tokens of a text: four characters per token, rounded up. */
47
+ const estimateCodeModeTokens = (text) => Math.ceil(text.length / 4);
48
+ const maxShortTypeChars = 60;
49
+ const shortType = (schema) => {
50
+ const rendered = renderToolOutputType(schema);
51
+ if (rendered.length <= maxShortTypeChars) return rendered;
52
+ if (!Predicate.isBoolean(schema)) {
53
+ if (schema.type === "array") return "Array<unknown>";
54
+ if (schema.type === "object") return "object";
55
+ }
56
+ return "unknown";
57
+ };
58
+ const declarationTool = (tool) => ({
59
+ name: tool.name,
60
+ description: tool.description,
61
+ inputSchema: tool.inputSchema,
62
+ outputSchema: tool.outputSchema,
63
+ execute: () => void 0
64
+ });
65
+ const directToolLine = (tool) => `- \`tools.${tool.identifier}(args)\` takes the arguments of the \`${tool.name}\` tool and resolves to \`${shortType(tool.outputSchema)}\`.`;
66
+ const declarationMembers = (tools) => renderDeclarations({ tools: tools.map(declarationTool) });
67
+ const candidateCost = (tool) => estimateCodeModeTokens(declarationMembers([tool]));
68
+ const groupByNamespace = (items) => {
69
+ const groups = /* @__PURE__ */ new Map();
70
+ for (const item of items) {
71
+ const group = groups.get(item.tool.namespace);
72
+ if (group === void 0) groups.set(item.tool.namespace, [item]);
73
+ else group.push(item);
74
+ }
75
+ return groups;
76
+ };
77
+ /**
78
+ * Chooses the tools the description lists. `all` tools are always placed, outside the budget
79
+ * (the model already has their declarations). `listed` tools fill `budget` estimated tokens fairly
80
+ * across namespaces: each round, every namespace still in play places its cheapest remaining tool;
81
+ * a namespace whose next tool does not fit drops out. `search` tools are never candidates.
82
+ */
83
+ const selectCodeModeListing = (catalog, budget) => {
84
+ const candidates = catalog.flatMap((tool, index) => tool.exposure === "listed" ? [{
85
+ tool,
86
+ index,
87
+ cost: candidateCost(tool)
88
+ }] : []);
89
+ const queues = new Map([...groupByNamespace(candidates)].map(([namespace, group]) => [namespace, [...group].sort((left, right) => left.cost - right.cost || left.index - right.index)]));
90
+ const placed = new Set(catalog.flatMap((tool, index) => tool.exposure === "all" ? [index] : []));
91
+ const inPlay = new Set(queues.keys());
92
+ let remaining = budget;
93
+ while (inPlay.size > 0) for (const [namespace, queue] of queues) {
94
+ if (!inPlay.has(namespace)) continue;
95
+ const next = queue.shift();
96
+ if (next === void 0 || next.cost > remaining) {
97
+ inPlay.delete(namespace);
98
+ continue;
99
+ }
100
+ placed.add(next.index);
101
+ remaining -= next.cost;
102
+ }
103
+ const unlisted = new Set(catalog.flatMap((tool, index) => placed.has(index) ? [] : [tool.namespace]));
104
+ return {
105
+ listed: catalog.filter((_, index) => placed.has(index)),
106
+ unlistedNamespaces: [...new Set(catalog.map((tool) => tool.namespace))].filter((namespace) => unlisted.has(namespace))
107
+ };
108
+ };
109
+ const intro = [
110
+ "Run a JavaScript script that calls tools and returns only what matters.",
111
+ "`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.",
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
+ "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
+ ].join("\n");
115
+ const globalLines = (store) => [
116
+ "- `text(value)` and `console.log(...values)`: append text to the output.",
117
+ "- `image(dataUrl)`: append a base64 image (a `data:` URL or `{ type: \"image\", data, mimeType }`).",
118
+ "- `exit()`: end the script successfully.",
119
+ "- `ALL_TOOLS`: `{ name, description }` of every callable tool.",
120
+ "- `await searchTools(query, { limit?, namespace? })`: find tools by topic; resolves to `Array<{ name: string; description: string }>` (default limit 8).",
121
+ "- `await describeTool(name)`: the description and TypeScript declaration of a tool, or `undefined`.",
122
+ "- `await describeNamespace(name)`: `{ name, description?, tools: Array<{ name, description }> }` for a namespace, or `undefined`.",
123
+ ...store ? ["- `store(key, value)` and `load(key)`: keep small JSON values for later scripts; writes are saved only when the script succeeds."] : []
124
+ ];
125
+ const namespaceSection = (namespace, tools) => {
126
+ const declared = tools.filter((tool) => tool.exposure !== "all");
127
+ const direct = tools.filter((tool) => tool.exposure === "all");
128
+ const description = tools.find((tool) => tool.namespaceDescription !== void 0)?.namespaceDescription;
129
+ return [
130
+ `### ${namespace}`,
131
+ ...description === void 0 ? [] : [description],
132
+ ...declared.length > 0 ? [
133
+ "```ts",
134
+ declarationMembers(declared),
135
+ "```"
136
+ ] : [],
137
+ ...direct.map(directToolLine)
138
+ ].join("\n");
139
+ };
140
+ 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.";
141
+ /**
142
+ * The code mode tool description: intro, globals one per line, then nested tools grouped by
143
+ * namespace, then one fixed line pointing to `searchTools`/`describeTool`/`describeNamespace`.
144
+ * `all` tools get one line each outside the budget; `listed` tools are declared within the inline
145
+ * budget; `search` tools never contribute, so adding or removing them (even whole namespaces of
146
+ * them) leaves the text byte-identical.
147
+ */
148
+ const renderCodeModeDescription = (input) => {
149
+ const sections = [...groupByNamespace(selectCodeModeListing(codeModeCatalog(input.tools), input.inlineBudget ?? 3e3).listed.map((tool) => ({ tool })))].map(([namespace, items]) => namespaceSection(namespace, items.map((item) => item.tool)));
150
+ return [
151
+ intro,
152
+ ["Globals:", ...globalLines(input.store === true)].join("\n"),
153
+ sections.length === 0 ? "Nested tools: none are listed here." : ["## Nested tools by namespace", ...sections].join("\n\n"),
154
+ unlistedToolsLine
155
+ ].join("\n\n");
156
+ };
157
+ /** `describeTool` text: the description and TypeScript declaration of one tool. */
158
+ const describeCodeModeTool = (tool) => renderToolSample(declarationTool(tool));
159
+ /** Finds a tool by identifier or raw name. */
160
+ const findCodeModeTool = (catalog, name) => catalog.find((tool) => tool.identifier === name) ?? catalog.find((tool) => tool.name === name);
161
+ //#endregion
162
+ export { codeModeCallLabels, codeModeCatalog, defaultCodeModeInlineBudget, describeCodeModeTool, estimateCodeModeTokens, findCodeModeTool, renderCodeModeDescription, selectCodeModeListing };
163
+
164
+ //# 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 /**\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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"}
@@ -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"}