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 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 Clojure**. Built on
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() // 10 built-in tools, on by default
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 seven ports: [`SPEC.md`](https://github.com/muthuishere/toolnexus/blob/main/SPEC.md).
174
+ Contract across all eight ports: [`SPEC.md`](https://github.com/muthuishere/toolnexus/blob/main/SPEC.md).
@@ -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
- const judged = { ...r, toolCalls: accumulated };
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
@@ -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
- return defineTool({
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, Promise.all(calls.map(async (call) => {
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({ name: s.call.function.name, args: s.args, output: s.result.output, isError: s.result.isError, metadata: s.result.metadata });
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, Promise.all(uses.map(async (use) => {
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({ name: s.use.name, args: s.args, output: s.result.output, isError: s.result.isError, metadata: s.result.metadata });
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, Promise.all(calls.map(async (c) => {
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({ name: c.name, args, output: result.output, isError: result.isError, metadata: result.metadata });
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({ name: c.name, args, output: result.output, isError: result.isError, metadata: result.metadata });
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, Promise.all(uses.map(async (u) => {
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({ name: u.name, args, output: result.output, isError: result.isError, metadata: result.metadata });
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({ name: u.name, args, output: result.output, isError: result.isError, metadata: result.metadata });
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";
@@ -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;