@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.
- package/LICENSE +21 -0
- package/README.md +132 -0
- package/dist/catalog.d.mts +60 -0
- package/dist/catalog.d.mts.map +1 -0
- package/dist/catalog.mjs +143 -0
- package/dist/catalog.mjs.map +1 -0
- package/dist/classifier-tool.d.mts +78 -0
- package/dist/classifier-tool.d.mts.map +1 -0
- package/dist/classifier-tool.mjs +125 -0
- package/dist/classifier-tool.mjs.map +1 -0
- package/dist/executor.d.mts +89 -0
- package/dist/executor.d.mts.map +1 -0
- package/dist/executor.mjs +1 -0
- package/dist/index.d.mts +8 -0
- package/dist/index.mjs +7 -0
- package/dist/node.d.mts +34 -0
- package/dist/node.d.mts.map +1 -0
- package/dist/node.mjs +215 -0
- package/dist/node.mjs.map +1 -0
- package/dist/output.d.mts +42 -0
- package/dist/output.d.mts.map +1 -0
- package/dist/output.mjs +184 -0
- package/dist/output.mjs.map +1 -0
- package/dist/search.d.mts +23 -0
- package/dist/search.d.mts.map +1 -0
- package/dist/search.mjs +58 -0
- package/dist/search.mjs.map +1 -0
- package/dist/store.d.mts +40 -0
- package/dist/store.d.mts.map +1 -0
- package/dist/store.mjs +61 -0
- package/dist/store.mjs.map +1 -0
- package/dist/tool.d.mts +75 -0
- package/dist/tool.d.mts.map +1 -0
- package/dist/tool.mjs +282 -0
- package/dist/tool.mjs.map +1 -0
- package/package.json +63 -0
- package/src/catalog.ts +274 -0
- package/src/classifier-tool.ts +227 -0
- package/src/executor.ts +103 -0
- package/src/index.ts +76 -0
- package/src/node.ts +334 -0
- package/src/output.ts +287 -0
- package/src/search.ts +113 -0
- package/src/store.ts +93 -0
- 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"}
|
package/dist/catalog.mjs
ADDED
|
@@ -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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"}
|