toolnexus 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -4
- package/dist/agents/loop.js +4 -1
- package/dist/agents/runtime.js +4 -1
- package/dist/client.d.ts +26 -1
- package/dist/client.js +56 -16
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/media.d.ts +126 -0
- package/dist/media.js +540 -0
- package/dist/toolconcurrency.d.ts +16 -0
- package/dist/toolconcurrency.js +59 -0
- package/dist/toolkit.d.ts +5 -2
- package/dist/toolkit.js +3 -3
- package/dist/types.d.ts +4 -1
- package/package.json +5 -3
- package/src/agents/loop.ts +4 -1
- package/src/agents/runtime.ts +4 -1
- package/src/client.ts +77 -19
- package/src/index.ts +2 -0
- package/src/media.ts +604 -0
- package/src/toolconcurrency.ts +67 -0
- package/src/toolkit.ts +6 -3
- package/src/types.ts +4 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ unified as one tool set, driving any LLM.
|
|
|
12
12
|
> loop, hooks, streaming, retries, memory — and nothing it doesn't.
|
|
13
13
|
|
|
14
14
|
The JS/TypeScript port of [toolnexus](https://github.com/muthuishere/toolnexus) — the same
|
|
15
|
-
library, byte-identical, also in **Python, Go, Java, C#, Elixir and
|
|
15
|
+
library, byte-identical, also in **Python, Go, Java, C#, Elixir, Clojure and Rust**. Built on
|
|
16
16
|
`@modelcontextprotocol/sdk` (the MCP SDK opencode uses).
|
|
17
17
|
|
|
18
18
|
## Install
|
|
@@ -28,11 +28,11 @@ Built-in tools are on by default, so an empty toolkit can already act:
|
|
|
28
28
|
```ts
|
|
29
29
|
import { createToolkit, createClient } from "toolnexus"
|
|
30
30
|
|
|
31
|
-
const tk = await createToolkit()
|
|
31
|
+
const tk = await createToolkit({}) // 10 built-in tools, on by default
|
|
32
32
|
const agent = createClient({
|
|
33
33
|
baseUrl: "https://openrouter.ai/api/v1", // any OpenAI- or Anthropic-style endpoint
|
|
34
34
|
style: "openai", // or "anthropic"
|
|
35
|
-
model: "openai/gpt-4o-mini",
|
|
35
|
+
model: "openai/gpt-4o-mini", // key: OPENROUTER_API_KEY (matches the host)
|
|
36
36
|
})
|
|
37
37
|
|
|
38
38
|
const { text } = await agent.run("What files are in this folder?", { toolkit: tk })
|
|
@@ -49,6 +49,52 @@ const tk = await createToolkit({ mcp: "mcp.json", skills: ["skills"] })
|
|
|
49
49
|
- `skills/` is a folder of `**/SKILL.md` files, loaded on demand through one `skill` tool.
|
|
50
50
|
- Remote MCP `headers` values expand `${ENV_VAR}` at call time and are never logged.
|
|
51
51
|
|
|
52
|
+
## API key by host (BREAKING in 0.22)
|
|
53
|
+
|
|
54
|
+
A key is only ever sent to the host it belongs to. `apiKey` wins; else `apiKeyEnv` (the NAME of
|
|
55
|
+
an env var); else only the env var of the `baseUrl` host, over https — `api.openai.com` →
|
|
56
|
+
`OPENAI_API_KEY`, `api.anthropic.com` → `ANTHROPIC_API_KEY`, `openrouter.ai` →
|
|
57
|
+
`OPENROUTER_API_KEY`. **A local server, a proxy or any other host needs `apiKey` or `apiKeyEnv`**
|
|
58
|
+
(before 0.22 the first provider key in the environment was sent to any `baseUrl`):
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
createClient({ baseUrl: "http://localhost:11434/v1", style: "openai", model: "llama3", apiKey: "YOUR_KEY_HERE" })
|
|
62
|
+
createClient({ baseUrl: "https://my-proxy.example/v1", style: "openai", model: "m", apiKeyEnv: "MY_PROXY_KEY" })
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Prompt cache and tool tiers
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
const agent = createClient({
|
|
69
|
+
baseUrl, style, model,
|
|
70
|
+
promptCache: { key: "session-42" }, // anthropic cache breakpoints / openai prompt_cache_key
|
|
71
|
+
toolTiers: { core: ["read", "grep"] }, // the rest load on demand via tool_search / call_tool
|
|
72
|
+
})
|
|
73
|
+
const res = await agent.run(prompt, { toolkit: tk })
|
|
74
|
+
res.usage.cacheReadTokens // cache hits; anthropic context = prompt + cacheRead + cacheWrite
|
|
75
|
+
agent.toolTierPlan(tk) // { read: "core", bash: "listed", … }, frozen per toolkit
|
|
76
|
+
agent.resetToolTiers(tk) // rebuild at session start
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`toolTiers` keeps the tools array identical on every request, so the cached prefix survives a
|
|
80
|
+
large or growing toolkit. `call_tool` is unwrapped before hooks, so permission gates see the real
|
|
81
|
+
tool. Runnable: `npm run example:prompt-cache` ([`examples/prompt_cache.ts`](examples/prompt_cache.ts)).
|
|
82
|
+
|
|
83
|
+
## Host additions (0.22)
|
|
84
|
+
|
|
85
|
+
- `loadMcp(cfg, { onServer })` — each MCP server reported the moment it connects or fails;
|
|
86
|
+
`mcpSource.resourceTools()` adds `list_mcp_resources` / `read_mcp_resource`.
|
|
87
|
+
- `waitFor(request, { signal })` — a pending question ends with its turn.
|
|
88
|
+
- Tool `hints` (`readOnly` / `destructive` / `openWorld`) on every builtin and MCP tool, passed to
|
|
89
|
+
`hooks.beforeTool` — allow, deny with an error result, or ask via `pending`.
|
|
90
|
+
- Opt-in builtins: `builtins: { background: new BackgroundShells(), webSearch, notebookEdit: true }`
|
|
91
|
+
(`bash_output` / `kill_shell`, `websearch`, `notebook_edit`); `grep` takes `context` / `before` /
|
|
92
|
+
`after`; `read` / `grep` report truncation in metadata.
|
|
93
|
+
- `agents.compactor({ onCompact })`, `agent.asToolWith(ctx => ({ hooks, waitFor }))`, runtime
|
|
94
|
+
`waitFor`, `ctx.callId` on tools, `listModels(opts)`.
|
|
95
|
+
|
|
96
|
+
ACP host hooks (permission choice, update stream, cancel, streaming) are Go-only for now.
|
|
97
|
+
|
|
52
98
|
## Simple judgments
|
|
53
99
|
|
|
54
100
|
A thin layer over any `Classifier` (SPEC.md §8B); the wire request is byte-identical to
|
|
@@ -89,6 +135,29 @@ const out = await gate(c, state, questions, [
|
|
|
89
135
|
filter, content and model batteries into `hooks`. A `beforeLLM` hook may return `{ model }` to
|
|
90
136
|
send another model for that turn only.
|
|
91
137
|
|
|
138
|
+
## Media generation
|
|
139
|
+
|
|
140
|
+
Generate images, video and speech (plus transcription, voice cloning and talking-head video)
|
|
141
|
+
from a **JSON style** instead of provider code. Async jobs are started and polled for you. The same
|
|
142
|
+
verbs become agent tools (`generate_image`, `generate_video`, `generate_speech`, …), and whatever
|
|
143
|
+
they produce lands in `RunResult.files`. `examples/media-styles/a2e.json` ships first and reads
|
|
144
|
+
`A2E_API_KEY` at call time. Its result URLs expire in 3–30 days, so save what you keep. SPEC §7G;
|
|
145
|
+
guide: <https://muthuishere.github.io/toolnexus/media/>.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { loadMediaStyle, createMedia, createToolkit } from "toolnexus"
|
|
149
|
+
|
|
150
|
+
const media = createMedia(loadMediaStyle("examples/media-styles/a2e.json"), {
|
|
151
|
+
onProgress: ({ verb, status }) => console.log(verb, status),
|
|
152
|
+
})
|
|
153
|
+
const img = await media.image({ prompt: "a red fox in fresh snow", width: 1024, height: 768 })
|
|
154
|
+
const vid = await media.video({ prompt: "the fox blinks", image: img.files[0].url, seconds: 5 })
|
|
155
|
+
|
|
156
|
+
const tk = await createToolkit({ media }) // the agent gets generate_image, …
|
|
157
|
+
const res = await agent.run("Draw a fox, then animate it.", { toolkit: tk })
|
|
158
|
+
for (const f of res.files) console.log(f.from, f.mimeType, f.url)
|
|
159
|
+
```
|
|
160
|
+
|
|
92
161
|
## Documentation
|
|
93
162
|
|
|
94
163
|
Everything else — the full surface, with runnable examples — lives on the docs site:
|
|
@@ -102,4 +171,4 @@ Everything else — the full surface, with runnable examples — lives on the do
|
|
|
102
171
|
| **API reference** | [JavaScript](https://muthuishere.github.io/toolnexus/api/javascript/) |
|
|
103
172
|
| **Cookbook** | [Zero to agent](https://muthuishere.github.io/toolnexus/cookbook/zero-to-agent/) · [MCP servers](https://muthuishere.github.io/toolnexus/cookbook/mcp-servers/) · [Agent skills](https://muthuishere.github.io/toolnexus/cookbook/agent-skills/) · [Judge](https://muthuishere.github.io/toolnexus/cookbook/judge/) |
|
|
104
173
|
|
|
105
|
-
Contract across all
|
|
174
|
+
Contract across all eight ports: [`SPEC.md`](https://github.com/muthuishere/toolnexus/blob/main/SPEC.md).
|
package/dist/agents/loop.js
CHANGED
|
@@ -76,6 +76,7 @@ export async function runGated(ask, prompt, completion) {
|
|
|
76
76
|
throw new Error("toolnexus: completion.verify is required");
|
|
77
77
|
}
|
|
78
78
|
let accumulated = [];
|
|
79
|
+
let accFiles = [];
|
|
79
80
|
let last;
|
|
80
81
|
let reason = "";
|
|
81
82
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
@@ -84,7 +85,9 @@ export async function runGated(ask, prompt, completion) {
|
|
|
84
85
|
// The gate judges the ACCUMULATED work, so an agent cannot escape it by
|
|
85
86
|
// declining to re-declare its plan on a retry.
|
|
86
87
|
accumulated = [...accumulated, ...r.toolCalls];
|
|
87
|
-
|
|
88
|
+
// RunResult.files is derived from toolCalls, so it accumulates with them (§8).
|
|
89
|
+
accFiles = [...accFiles, ...(r.files ?? [])];
|
|
90
|
+
const judged = { ...r, toolCalls: accumulated, files: accFiles };
|
|
88
91
|
last = judged;
|
|
89
92
|
if (r.status && r.status !== "done") {
|
|
90
93
|
// The run stopped for its own reason (suspension, budget). If the gate was
|
package/dist/agents/runtime.js
CHANGED
|
@@ -751,7 +751,7 @@ export class AgentRuntime {
|
|
|
751
751
|
const team = [...(parent.def.team ?? [])].sort();
|
|
752
752
|
const advertised = team.map((n) => (this.registry[n] ? `${n}: ${this.registry[n].does}` : n)).join("; ");
|
|
753
753
|
const rt = this;
|
|
754
|
-
|
|
754
|
+
const tool = defineTool({
|
|
755
755
|
name: "task",
|
|
756
756
|
description: `Delegate a subtask to an isolated subagent (it runs on a fresh transcript and returns only its final answer). Available agents — ${advertised}`,
|
|
757
757
|
inputSchema: {
|
|
@@ -813,5 +813,8 @@ export class AgentRuntime {
|
|
|
813
813
|
};
|
|
814
814
|
},
|
|
815
815
|
});
|
|
816
|
+
// Each subagent is isolated and the runtime's own gate schedules the children,
|
|
817
|
+
// so a fan-out of task calls runs concurrently (SPEC §8 "Tool scheduling").
|
|
818
|
+
return { ...tool, concurrencySafe: () => true };
|
|
816
819
|
}
|
|
817
820
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { Toolkit } from "./toolkit.js";
|
|
|
7
7
|
import type { ToolHints, ToolResult, Request, Answer } from "./types.js";
|
|
8
8
|
import { type PromptCacheOptions } from "./promptcache.js";
|
|
9
9
|
import { type ToolTier, type ToolTierOptions } from "./tooltiers.js";
|
|
10
|
-
import type { PromptInput, UnsupportedPartMode } from "./content.js";
|
|
10
|
+
import type { ContentPart, PromptInput, UnsupportedPartMode } from "./content.js";
|
|
11
11
|
import type { TranslateRequest, TranslateResult } from "./translate.js";
|
|
12
12
|
export type ClientStyle = "openai" | "anthropic";
|
|
13
13
|
export interface ClientOptions {
|
|
@@ -26,6 +26,10 @@ export interface ClientOptions {
|
|
|
26
26
|
headers?: Record<string, string>;
|
|
27
27
|
systemPrompt?: string;
|
|
28
28
|
maxTurns?: number;
|
|
29
|
+
/** Cap on concurrency-safe tool calls of one response running at once (SPEC §8 "Tool
|
|
30
|
+
* scheduling"). Absent ⇒ env `TOOLNEXUS_MAX_TOOL_CONCURRENCY` when a positive integer, else 10.
|
|
31
|
+
* Unsafe calls (anything not read-only) always run alone. */
|
|
32
|
+
maxToolConcurrency?: number;
|
|
29
33
|
hooks?: Hooks;
|
|
30
34
|
/** Retries on transient LLM errors (`429`/`500`/`502`/`503`/`504`/`529` + network). Default 2.
|
|
31
35
|
* Widen the status set with `retryableStatuses`. */
|
|
@@ -299,7 +303,19 @@ export interface ToolCallRecord {
|
|
|
299
303
|
output: string;
|
|
300
304
|
isError: boolean;
|
|
301
305
|
metadata?: Record<string, unknown>;
|
|
306
|
+
/** §8: the ToolResult's parts — present iff the result had parts. */
|
|
307
|
+
parts?: ContentPart[];
|
|
302
308
|
}
|
|
309
|
+
/** §8 RunResult.files entry: a non-text part a tool call produced this run. */
|
|
310
|
+
export type RunFile = {
|
|
311
|
+
type: "image" | "file" | "audio";
|
|
312
|
+
mimeType: string;
|
|
313
|
+
url?: string;
|
|
314
|
+
data?: string;
|
|
315
|
+
name?: string;
|
|
316
|
+
from: string;
|
|
317
|
+
callId: string;
|
|
318
|
+
};
|
|
303
319
|
/** Token usage, summed across every LLM round trip in the run. */
|
|
304
320
|
export interface Usage {
|
|
305
321
|
promptTokens: number;
|
|
@@ -319,6 +335,8 @@ export interface RunResult {
|
|
|
319
335
|
messages: any[];
|
|
320
336
|
/** Every tool call made, with its output, error flag, and metadata. */
|
|
321
337
|
toolCalls: ToolCallRecord[];
|
|
338
|
+
/** Every non-text part produced by tool calls this run, in call order (§8). Always present. */
|
|
339
|
+
files: RunFile[];
|
|
322
340
|
/** Total number of tool calls (= toolCalls.length). */
|
|
323
341
|
toolCallCount: number;
|
|
324
342
|
/** Number of LLM round trips. */
|
|
@@ -491,8 +509,15 @@ export declare class Client {
|
|
|
491
509
|
/** Run one tool through beforeTool/afterTool hooks (mutate args, short-circuit, transform result). */
|
|
492
510
|
private runTool;
|
|
493
511
|
private runToolDirect;
|
|
512
|
+
/** Runs one response's calls under the tool schedule (SPEC §8 "Tool scheduling"). */
|
|
513
|
+
private schedule;
|
|
514
|
+
/** One call's safety; looks through `call_tool` to the tool it names. Unknown ⇒ unsafe. */
|
|
515
|
+
private callSafe;
|
|
494
516
|
/** ToolTiers `call_tool {name, arguments}` -> the real call; undefined when tiers are off. */
|
|
495
517
|
private unwrapCall;
|
|
518
|
+
/** The RunResult.toolCalls entry: a `call_tool` call is recorded as the real tool with its
|
|
519
|
+
* inner arguments, the name hooks and stream events see (the transcript keeps the wrapper). */
|
|
520
|
+
private toolCallRecord;
|
|
496
521
|
/** The stream's tool_call event, naming the real tool for a `call_tool` call. */
|
|
497
522
|
private callEvent;
|
|
498
523
|
private resultEvent;
|
package/dist/client.js
CHANGED
|
@@ -3,6 +3,7 @@ import { toOpenAI, toAnthropic } from "./adapters.js";
|
|
|
3
3
|
import { applyPromptCache } from "./promptcache.js";
|
|
4
4
|
import { buildToolPlan, toolSearch, unwrapCallTool } from "./tooltiers.js";
|
|
5
5
|
import { isRetryableStatus, retryAfterMs } from "./retry.js";
|
|
6
|
+
import { isConcurrencySafe, resolveMaxToolConcurrency, scheduleCalls } from "./toolconcurrency.js";
|
|
6
7
|
import { toOpenAIWire, toAnthropicWire, checkPromptParts } from "./wire.js";
|
|
7
8
|
import { finishReasonFor, argsString, openAIMessagesToAnthropic, openAIToolsToAnthropic, openAIToolChoiceToAnthropic, hasSystemMessage, } from "./translate.js";
|
|
8
9
|
/**
|
|
@@ -106,6 +107,20 @@ function delay(ms, signal) {
|
|
|
106
107
|
signal.addEventListener("abort", () => { clearTimeout(t); reject(signal.reason ?? new Error("aborted")); }, { once: true });
|
|
107
108
|
});
|
|
108
109
|
}
|
|
110
|
+
/** Model tool-call id of each record, kept off the record itself (§8 shape has no id). */
|
|
111
|
+
const callIds = new WeakMap();
|
|
112
|
+
/** RunResult.files: every non-text part of every tool call, in call order (§8). */
|
|
113
|
+
function runFiles(toolCalls) {
|
|
114
|
+
const out = [];
|
|
115
|
+
for (const r of toolCalls) {
|
|
116
|
+
for (const p of r.parts ?? []) {
|
|
117
|
+
if (p.type === "text")
|
|
118
|
+
continue;
|
|
119
|
+
out.push({ ...p, from: r.name, callId: callIds.get(r) ?? "" });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
109
124
|
function addUsage(acc, raw, style) {
|
|
110
125
|
if (!raw)
|
|
111
126
|
return;
|
|
@@ -718,10 +733,35 @@ export class Client {
|
|
|
718
733
|
this.emitTool(name, source, result, t0);
|
|
719
734
|
return { args: a, result };
|
|
720
735
|
}
|
|
736
|
+
/** Runs one response's calls under the tool schedule (SPEC §8 "Tool scheduling"). */
|
|
737
|
+
schedule(toolkit, items, key, fn) {
|
|
738
|
+
return scheduleCalls(items, (item) => {
|
|
739
|
+
const [name, args] = key(item);
|
|
740
|
+
return this.callSafe(toolkit, name, args);
|
|
741
|
+
}, resolveMaxToolConcurrency(this.opts.maxToolConcurrency), fn);
|
|
742
|
+
}
|
|
743
|
+
/** One call's safety; looks through `call_tool` to the tool it names. Unknown ⇒ unsafe. */
|
|
744
|
+
callSafe(toolkit, name, args) {
|
|
745
|
+
if (this.opts.toolTiers && name === "tool_search")
|
|
746
|
+
return true;
|
|
747
|
+
const inner = this.unwrapCall(name, args ?? {});
|
|
748
|
+
const tool = toolkit?.get(inner ? inner.name : name);
|
|
749
|
+
return tool ? isConcurrencySafe(tool, inner ? inner.args : (args ?? {})) : false;
|
|
750
|
+
}
|
|
721
751
|
/** ToolTiers `call_tool {name, arguments}` -> the real call; undefined when tiers are off. */
|
|
722
752
|
unwrapCall(name, args) {
|
|
723
753
|
return this.opts.toolTiers ? unwrapCallTool(name, args) : undefined;
|
|
724
754
|
}
|
|
755
|
+
/** The RunResult.toolCalls entry: a `call_tool` call is recorded as the real tool with its
|
|
756
|
+
* inner arguments, the name hooks and stream events see (the transcript keeps the wrapper). */
|
|
757
|
+
toolCallRecord(name, args, result, callId) {
|
|
758
|
+
const inner = this.unwrapCall(name, args);
|
|
759
|
+
const rec = { name: inner ? inner.name : name, args: inner ? inner.args : args, output: result.output, isError: result.isError, metadata: result.metadata };
|
|
760
|
+
if (result.parts?.length)
|
|
761
|
+
rec.parts = result.parts;
|
|
762
|
+
callIds.set(rec, callId ?? "");
|
|
763
|
+
return rec;
|
|
764
|
+
}
|
|
725
765
|
/** The stream's tool_call event, naming the real tool for a `call_tool` call. */
|
|
726
766
|
callEvent(id, name, args) {
|
|
727
767
|
const inner = this.unwrapCall(name, args);
|
|
@@ -880,7 +920,7 @@ export class Client {
|
|
|
880
920
|
return this.endRun(runStart, lastModel, msg.content ?? "", messages, toolCalls, turns, usage);
|
|
881
921
|
// execute all tool calls in this turn concurrently (true parallel tool calling)
|
|
882
922
|
done.clear();
|
|
883
|
-
const settled = await untilAborted(signal,
|
|
923
|
+
const settled = await untilAborted(signal, this.schedule(toolkit, calls, (call) => [call.function.name, safeJson(call.function.arguments)], async (call) => {
|
|
884
924
|
let { args, result } = await this.runTool(toolkit, call.function.name, safeJson(call.function.arguments), call.id, turn, signal);
|
|
885
925
|
let halted;
|
|
886
926
|
const req = pendingOf(result);
|
|
@@ -892,12 +932,12 @@ export class Client {
|
|
|
892
932
|
if (!halted && call.id)
|
|
893
933
|
done.set(call.id, result);
|
|
894
934
|
return { call, args, result, halted };
|
|
895
|
-
}))
|
|
935
|
+
}));
|
|
896
936
|
// Record in tool-call order; on the FIRST durable halt, surface it and stop — deterministic,
|
|
897
937
|
// and the later suspensions' placeholder results never enter the transcript (they re-suspend
|
|
898
938
|
// on resume). Mirrors the streaming path.
|
|
899
939
|
for (const s of settled) {
|
|
900
|
-
toolCalls.push(
|
|
940
|
+
toolCalls.push(this.toolCallRecord(s.call.function.name, s.args, s.result, s.call.id));
|
|
901
941
|
messages.push(toolMessage(s.call.id, s.call.function.name, s.result));
|
|
902
942
|
if (s.halted)
|
|
903
943
|
return this.pendingRun(runStart, lastModel, s.halted, messages, toolCalls, turns, usage);
|
|
@@ -913,7 +953,7 @@ export class Client {
|
|
|
913
953
|
}
|
|
914
954
|
result(model, text, messages, toolCalls, turns, usage, exhausted = false) {
|
|
915
955
|
const incomplete = exhausted && text === "";
|
|
916
|
-
const r = { text, messages, toolCalls, toolCallCount: toolCalls.length, turns, usage, model, status: incomplete ? "incomplete" : "done" };
|
|
956
|
+
const r = { text, messages, toolCalls, files: runFiles(toolCalls), toolCallCount: toolCalls.length, turns, usage, model, status: incomplete ? "incomplete" : "done" };
|
|
917
957
|
if (incomplete)
|
|
918
958
|
r.limit = "maxTurns";
|
|
919
959
|
return r;
|
|
@@ -921,7 +961,7 @@ export class Client {
|
|
|
921
961
|
/** §10: a run halted because a tool suspended and no `waitFor` was configured. */
|
|
922
962
|
pendingRun(runStart, model, request, messages, toolCalls, turns, usage) {
|
|
923
963
|
this.emit({ event: "run", model, turns, toolCalls: toolCalls.length, totalTokens: usage.totalTokens, ms: Date.now() - runStart });
|
|
924
|
-
return { text: request.prompt, messages, toolCalls, toolCallCount: toolCalls.length, turns, usage, model, status: "pending", pending: request };
|
|
964
|
+
return { text: request.prompt, messages, toolCalls, files: runFiles(toolCalls), toolCallCount: toolCalls.length, turns, usage, model, status: "pending", pending: request };
|
|
925
965
|
}
|
|
926
966
|
/**
|
|
927
967
|
* §10: resolve a suspended tool result. Calls `waitFor(request)`, and on `ok` re-executes the
|
|
@@ -1002,7 +1042,7 @@ export class Client {
|
|
|
1002
1042
|
}
|
|
1003
1043
|
// execute all tool_use blocks in this turn concurrently (true parallel tool calling)
|
|
1004
1044
|
done.clear();
|
|
1005
|
-
const settled = await untilAborted(signal,
|
|
1045
|
+
const settled = await untilAborted(signal, this.schedule(toolkit, uses, (use) => [use.name, use.input ?? {}], async (use) => {
|
|
1006
1046
|
let { args, result } = await this.runTool(toolkit, use.name, use.input ?? {}, use.id, turn, signal);
|
|
1007
1047
|
let halted;
|
|
1008
1048
|
const req = pendingOf(result);
|
|
@@ -1014,14 +1054,14 @@ export class Client {
|
|
|
1014
1054
|
if (!halted && use.id)
|
|
1015
1055
|
done.set(use.id, result);
|
|
1016
1056
|
return { use, args, result, halted };
|
|
1017
|
-
}))
|
|
1057
|
+
}));
|
|
1018
1058
|
// Record in tool-call order; on the FIRST durable halt, include that tool_result block, stop
|
|
1019
1059
|
// building the content, and surface it — deterministic (G3), later suspensions re-suspend on
|
|
1020
1060
|
// resume. Mirrors the OpenAI path and the streaming loops.
|
|
1021
1061
|
const content = [];
|
|
1022
1062
|
let halted;
|
|
1023
1063
|
for (const s of settled) {
|
|
1024
|
-
toolCalls.push(
|
|
1064
|
+
toolCalls.push(this.toolCallRecord(s.use.name, s.args, s.result, s.use.id));
|
|
1025
1065
|
content.push(toolResultBlock(s.use.id, s.result));
|
|
1026
1066
|
if (s.halted) {
|
|
1027
1067
|
halted = s.halted;
|
|
@@ -1130,12 +1170,12 @@ export class Client {
|
|
|
1130
1170
|
for (const c of calls)
|
|
1131
1171
|
yield this.callEvent(c.id, c.name, safeJson(c.args));
|
|
1132
1172
|
done.clear();
|
|
1133
|
-
const settled = await untilAborted(signal,
|
|
1173
|
+
const settled = await untilAborted(signal, this.schedule(toolkit, calls, (c) => [c.name, safeJson(c.args)], async (c) => {
|
|
1134
1174
|
const r = await this.runTool(toolkit, c.name, safeJson(c.args), c.id, turn, signal);
|
|
1135
1175
|
if (!pendingOf(r.result) && c.id)
|
|
1136
1176
|
done.set(c.id, r.result);
|
|
1137
1177
|
return r;
|
|
1138
|
-
}))
|
|
1178
|
+
}));
|
|
1139
1179
|
for (let i = 0; i < calls.length; i++) {
|
|
1140
1180
|
const c = calls[i];
|
|
1141
1181
|
let { args, result } = settled[i];
|
|
@@ -1145,13 +1185,13 @@ export class Client {
|
|
|
1145
1185
|
const r = await this.resolvePending(toolkit, c.name, args, req, c.id, turn, signal);
|
|
1146
1186
|
result = r.result;
|
|
1147
1187
|
if (r.halted) {
|
|
1148
|
-
toolCalls.push(
|
|
1188
|
+
toolCalls.push(this.toolCallRecord(c.name, args, result, c.id));
|
|
1149
1189
|
messages.push(toolMessage(c.id, c.name, result));
|
|
1150
1190
|
yield { type: "done", result: this.pendingRun(runStart, lastModel, r.halted, messages, toolCalls, turns, usage) };
|
|
1151
1191
|
return;
|
|
1152
1192
|
}
|
|
1153
1193
|
}
|
|
1154
|
-
toolCalls.push(
|
|
1194
|
+
toolCalls.push(this.toolCallRecord(c.name, args, result, c.id));
|
|
1155
1195
|
messages.push(toolMessage(c.id, c.name, result));
|
|
1156
1196
|
yield this.resultEvent(c.id, c.name, safeJson(c.args), result);
|
|
1157
1197
|
}
|
|
@@ -1250,12 +1290,12 @@ export class Client {
|
|
|
1250
1290
|
for (const u of uses)
|
|
1251
1291
|
yield this.callEvent(u.id, u.name, u.input);
|
|
1252
1292
|
done.clear();
|
|
1253
|
-
const settled = await untilAborted(signal,
|
|
1293
|
+
const settled = await untilAborted(signal, this.schedule(toolkit, uses, (u) => [u.name, u.input ?? {}], async (u) => {
|
|
1254
1294
|
const r = await this.runTool(toolkit, u.name, u.input ?? {}, u.id, turn, signal);
|
|
1255
1295
|
if (!pendingOf(r.result) && u.id)
|
|
1256
1296
|
done.set(u.id, r.result);
|
|
1257
1297
|
return r;
|
|
1258
|
-
}))
|
|
1298
|
+
}));
|
|
1259
1299
|
const results = [];
|
|
1260
1300
|
for (let i = 0; i < uses.length; i++) {
|
|
1261
1301
|
const u = uses[i];
|
|
@@ -1266,14 +1306,14 @@ export class Client {
|
|
|
1266
1306
|
const r = await this.resolvePending(toolkit, u.name, args, req, u.id, turn, signal);
|
|
1267
1307
|
result = r.result;
|
|
1268
1308
|
if (r.halted) {
|
|
1269
|
-
toolCalls.push(
|
|
1309
|
+
toolCalls.push(this.toolCallRecord(u.name, args, result, u.id));
|
|
1270
1310
|
results.push(toolResultBlock(u.id, result));
|
|
1271
1311
|
messages.push({ role: "user", content: results });
|
|
1272
1312
|
yield { type: "done", result: this.pendingRun(runStart, lastModel, r.halted, messages, toolCalls, turns, usage) };
|
|
1273
1313
|
return;
|
|
1274
1314
|
}
|
|
1275
1315
|
}
|
|
1276
|
-
toolCalls.push(
|
|
1316
|
+
toolCalls.push(this.toolCallRecord(u.name, args, result, u.id));
|
|
1277
1317
|
results.push(toolResultBlock(u.id, result));
|
|
1278
1318
|
yield this.resultEvent(u.id, u.name, u.input, result);
|
|
1279
1319
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -22,5 +22,7 @@ export * from "./batteries.js";
|
|
|
22
22
|
export { State, context, questionMap, ask, gate, decide, applyGate, applyPolicy, answersOf, staticClassifier, Tape, DEFAULT_BANDS, type NamedQuestion, type Band, type Bands, type Answers, type Rule, type GateOutcome, type Policy, } from "./judge.js";
|
|
23
23
|
export * from "./promptcache.js";
|
|
24
24
|
export * from "./tooltiers.js";
|
|
25
|
+
export * from "./toolconcurrency.js";
|
|
25
26
|
export * from "./models.js";
|
|
26
27
|
export * from "./builtin_extras.js";
|
|
28
|
+
export * from "./media.js";
|
package/dist/index.js
CHANGED
|
@@ -25,5 +25,7 @@ export * from "./batteries.js";
|
|
|
25
25
|
export { State, context, questionMap, ask, gate, decide, applyGate, applyPolicy, answersOf, staticClassifier, Tape, DEFAULT_BANDS, } from "./judge.js";
|
|
26
26
|
export * from "./promptcache.js";
|
|
27
27
|
export * from "./tooltiers.js";
|
|
28
|
+
export * from "./toolconcurrency.js";
|
|
28
29
|
export * from "./models.js";
|
|
29
30
|
export * from "./builtin_extras.js";
|
|
31
|
+
export * from "./media.js";
|
package/dist/media.d.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Tool } from "./types.js";
|
|
2
|
+
export type MediaVerb = "image" | "video" | "speech" | "transcribe" | "cloneVoice" | "talking";
|
|
3
|
+
export type MediaKind = "image" | "video" | "audio" | "text";
|
|
4
|
+
export interface MediaVerbStyle {
|
|
5
|
+
mode: "sync" | "async";
|
|
6
|
+
kind: MediaKind;
|
|
7
|
+
start: {
|
|
8
|
+
method?: string;
|
|
9
|
+
path: string;
|
|
10
|
+
};
|
|
11
|
+
defaults?: Record<string, unknown>;
|
|
12
|
+
args: Record<string, string>;
|
|
13
|
+
idPath?: string;
|
|
14
|
+
poll?: {
|
|
15
|
+
method?: string;
|
|
16
|
+
path: string;
|
|
17
|
+
statusPath: string;
|
|
18
|
+
done?: string[];
|
|
19
|
+
failed?: string[];
|
|
20
|
+
errorPath?: string;
|
|
21
|
+
intervalMs?: number;
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
};
|
|
24
|
+
resultPath: string;
|
|
25
|
+
}
|
|
26
|
+
export interface MediaStyle {
|
|
27
|
+
baseUrl: string;
|
|
28
|
+
headers?: Record<string, string>;
|
|
29
|
+
errorEnvelope?: {
|
|
30
|
+
codePath: string;
|
|
31
|
+
okValues: unknown[];
|
|
32
|
+
messagePath?: string;
|
|
33
|
+
};
|
|
34
|
+
verbs: Partial<Record<MediaVerb, MediaVerbStyle>>;
|
|
35
|
+
}
|
|
36
|
+
export interface MediaFile {
|
|
37
|
+
type: "image" | "audio" | "file";
|
|
38
|
+
mimeType: string;
|
|
39
|
+
url: string;
|
|
40
|
+
}
|
|
41
|
+
export interface MediaResult {
|
|
42
|
+
files: MediaFile[];
|
|
43
|
+
text?: string;
|
|
44
|
+
jobId?: string;
|
|
45
|
+
status: string;
|
|
46
|
+
}
|
|
47
|
+
export interface MediaProgress {
|
|
48
|
+
verb: MediaVerb;
|
|
49
|
+
jobId: string;
|
|
50
|
+
status: string;
|
|
51
|
+
}
|
|
52
|
+
export interface MediaOptions {
|
|
53
|
+
onProgress?: (p: MediaProgress) => void;
|
|
54
|
+
/** HTTP transport (same seam as the LLM client's `fetch`). Omit ⇒ global fetch. */
|
|
55
|
+
fetch?: typeof fetch;
|
|
56
|
+
}
|
|
57
|
+
/** Per-call options of the function face. */
|
|
58
|
+
export interface MediaCallOptions {
|
|
59
|
+
/** Top-level keys overwrite the request body last. Never exposed on the tool face. */
|
|
60
|
+
providerOptions?: Record<string, unknown>;
|
|
61
|
+
signal?: AbortSignal;
|
|
62
|
+
}
|
|
63
|
+
export type ImageArgs = {
|
|
64
|
+
prompt: string;
|
|
65
|
+
width?: number;
|
|
66
|
+
height?: number;
|
|
67
|
+
aspectRatio?: string;
|
|
68
|
+
seed?: number;
|
|
69
|
+
};
|
|
70
|
+
export type VideoArgs = {
|
|
71
|
+
prompt: string;
|
|
72
|
+
image?: string;
|
|
73
|
+
seconds?: number;
|
|
74
|
+
aspectRatio?: string;
|
|
75
|
+
};
|
|
76
|
+
export type SpeechArgs = {
|
|
77
|
+
text: string;
|
|
78
|
+
voice?: string;
|
|
79
|
+
};
|
|
80
|
+
export type TranscribeArgs = {
|
|
81
|
+
audio: string;
|
|
82
|
+
};
|
|
83
|
+
export type CloneVoiceArgs = {
|
|
84
|
+
name: string;
|
|
85
|
+
samples: string[];
|
|
86
|
+
};
|
|
87
|
+
export type TalkingArgs = {
|
|
88
|
+
image: string;
|
|
89
|
+
audio: string;
|
|
90
|
+
};
|
|
91
|
+
export interface MediaVerbArg {
|
|
92
|
+
name: string;
|
|
93
|
+
type: string;
|
|
94
|
+
items?: string;
|
|
95
|
+
description: string;
|
|
96
|
+
required?: boolean;
|
|
97
|
+
}
|
|
98
|
+
export interface MediaVerbSpec {
|
|
99
|
+
verb: MediaVerb;
|
|
100
|
+
tool: string;
|
|
101
|
+
description: string;
|
|
102
|
+
args: MediaVerbArg[];
|
|
103
|
+
}
|
|
104
|
+
/** Canonical verbs — `conformance/media/verbs.json`, embedded verbatim (byte contract). */
|
|
105
|
+
export declare const MEDIA_VERBS: MediaVerbSpec[];
|
|
106
|
+
/** Read one style file (JSON). */
|
|
107
|
+
export declare function loadMediaStyle(path: string): MediaStyle;
|
|
108
|
+
export declare class Media {
|
|
109
|
+
private readonly opts;
|
|
110
|
+
private readonly styles;
|
|
111
|
+
constructor(styles: MediaStyle | MediaStyle[], opts?: MediaOptions);
|
|
112
|
+
private resolve;
|
|
113
|
+
/** Generic entry point: run `verb` with normalised `args` (§7G engine). */
|
|
114
|
+
generate(verb: MediaVerb | string, args: Record<string, unknown>, callOpts?: MediaCallOptions): Promise<MediaResult>;
|
|
115
|
+
private typed;
|
|
116
|
+
image(input: ImageArgs & MediaCallOptions): Promise<MediaResult>;
|
|
117
|
+
video(input: VideoArgs & MediaCallOptions): Promise<MediaResult>;
|
|
118
|
+
speech(input: SpeechArgs & MediaCallOptions): Promise<MediaResult>;
|
|
119
|
+
transcribe(input: TranscribeArgs & MediaCallOptions): Promise<MediaResult>;
|
|
120
|
+
cloneVoice(input: CloneVoiceArgs & MediaCallOptions): Promise<MediaResult>;
|
|
121
|
+
talking(input: TalkingArgs & MediaCallOptions): Promise<MediaResult>;
|
|
122
|
+
/** One Tool (source "media") per supported verb, in canonical order (§7G tool face). */
|
|
123
|
+
tools(): Tool[];
|
|
124
|
+
}
|
|
125
|
+
/** Build a media engine over one style or a list (first style declaring a verb wins). */
|
|
126
|
+
export declare function createMedia(styles: MediaStyle | MediaStyle[], opts?: MediaOptions): Media;
|