toolnexus 0.28.0 → 0.29.1

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
@@ -177,27 +177,67 @@ const rs = await c.states("a", "b").evaluate(judge.noul("q", "…")) // [{
177
177
 
178
178
  ## Media generation
179
179
 
180
- Generate images, video and speech (plus transcription, voice cloning and talking-head video)
181
- from a **JSON style** instead of provider code. Async jobs are started and polled for you. The same
182
- verbs become agent tools (`generate_image`, `generate_video`, `generate_speech`, …), and whatever
183
- they produce lands in `RunResult.files`. `examples/media-styles/a2e.json` ships first and reads
184
- `A2E_API_KEY` at call time. Its result URLs expire in 3–30 days, so save what you keep. SPEC §7G;
185
- guide: <https://muthuishere.github.io/toolnexus/media/>.
180
+ Images, video, speech, music, transcription, voice cloning and talking-head video, with **one
181
+ client per media object**. The quickest start is a **CLI preset**: `MediaStyle.XaiGrokCli`, `MediaStyle.OpenAICodexCli` and `MediaStyle.GoogleAgyCli` drive the vendor's
182
+ official CLI (`grok`, `codex`, `agy`) in its documented headless mode (`grok -p`, `codex exec`,
183
+ `agy -p`), signed in by you. Grok does image and video; Codex and Antigravity do image. Each preset
184
+ has a mock twin (`MediaStyle.XaiGrokCliMock`, …) for tests. Your provider's terms of service apply to how you use its CLI.
185
+ Every Grok image carries a small visible **"Grok" mark in the bottom-right corner**; toolnexus does
186
+ not remove it. Install, sign-in, Grok video, limits and models:
187
+ <https://muthuishere.github.io/toolnexus/cookbook/cli-media-setup/>. SPEC §7G; guide: <https://muthuishere.github.io/toolnexus/media/>.
186
188
 
187
189
  ```ts
188
- import { loadMediaStyle, createMedia, createToolkit } from "toolnexus"
190
+ import { generateMedia, createMedia, loadMediaStyle, createToolkit, MediaStyle } from "toolnexus"
189
191
 
190
- const media = createMedia(loadMediaStyle("examples/media-styles/a2e.json"), {
191
- onProgress: ({ verb, status }) => console.log(verb, status),
192
- })
193
- const img = await media.image({ prompt: "a red fox in fresh snow", width: 1024, height: 768 })
194
- const vid = await media.video({ prompt: "the fox blinks", image: img.files[0].url, seconds: 5 })
192
+ // one line
193
+ const r = await generateMedia(MediaStyle.XaiGrokCli, "image", { prompt: "a red apple", aspectRatio: "16:9" })
194
+ console.log(r.files[0].path)
195
195
 
196
- const tk = await createToolkit({ media }) // the agent gets generate_image, …
197
- const res = await agent.run("Draw a fox, then animate it.", { toolkit: tk })
198
- for (const f of res.files) console.log(f.from, f.mimeType, f.url)
196
+ // or keep a media object, and give it to your agent
197
+ const media = createMedia(loadMediaStyle(MediaStyle.XaiGrokCli))
198
+ const clip = await media.video({ prompt: "the apple rolls off the table" })
199
+ const tk = await createToolkit({ media }) // generate_image, generate_video (source "media")
199
200
  ```
200
201
 
202
+ For an HTTP API, give `createMedia(client)` one named client — `OpenAIMediaClient`, `OpenRouterMediaClient`, `FalMediaClient`, `ElevenLabsMediaClient` or `A2EMediaClient`. Each reads its key from its env
203
+ var (`OPENAI_API_KEY`, `FAL_API_KEY`, `ELEVENLABS_API_KEY`, …) at call time and never logs it.
204
+
205
+ ```ts
206
+ import { createMedia, ElevenLabsMediaClient } from "toolnexus"
207
+
208
+ const voice = createMedia(new ElevenLabsMediaClient({ gather: { mode: "url" } })) // speech, music, transcribe, cloneVoice
209
+ const say = await voice.speech({ text: "Hello from the fox." })
210
+ ```
211
+
212
+ - **Typed verbs** — `image`, `video`, `speech`, `music`, `transcribe`, `cloneVoice`, `talk`, each
213
+ `(args & { signal?, deadline?, progress?, providerOptions? }) => Promise<MediaResult>`;
214
+ `generate(verb, args, ctx)` is the raw call. `MediaResult { files, text?, jobId?, status, usage? }`
215
+ — `usage` (`inputTokens`, `outputTokens`, `costUsd`, `raw`) only as the provider reported it
216
+ (OpenAI images, OpenRouter, Grok's `raw.costUsdTicks` unconverted), also on the tool's `metadata.usage`.
217
+ Args are checked per client: a client that does not accept an arg (e.g. `seconds` on
218
+ the Grok CLI preset, which takes `prompt` and size hints only) or lacks a required one fails with
219
+ `unavailable`; a key that is not a canonical arg of the verb fails at once.
220
+ - **Config** (every built-in client, one shape): `model`, `models: { [verb]: model }`, `key`, `url`,
221
+ `command` (CLI), `loop: { intervalMs, timeoutMs, onProgress }` (timeout default 10 min),
222
+ `gather: { mode: "url" | "download" | "inline", dir, maxBytes, keep }` (default `download` into
223
+ `<tmp>/toolnexus-media`, 100 MiB cap), `fetch` (injected transport).
224
+ - **Agents** — `createToolkit({ media })` (or `media.tools()`); produced files land in
225
+ `RunResult.files` (`{ type, mimeType, url?, data?, path?, from, callId }` — `path` is the saved
226
+ local file; it is never sent to the model). Only **image** parts are sent
227
+ back to the model; audio/video stay on `RunResult.files`.
228
+ - **Your own provider** — implement `MediaClient` (`name`, `supports(verb)`, optional `args(verb)`,
229
+ any of the seven verb methods); throw `MediaUnavailableError` when it cannot serve a call (no key, no credits).
230
+ Advanced: `styleMediaClient(loadMediaStyle("my-style.json"))` turns a JSON style into a client.
231
+ - **Errors** — a typed verb throws `MediaError` (`status`: `failed` | `timeout` | `cancelled` |
232
+ `unavailable`); a media tool never throws (`isError: true`).
233
+
234
+ Run it: `npm run build && npm run example:media`. It uses whatever is present, in order: the `grok`
235
+ and `codex` CLIs, `ELEVENLABS_API_KEY`, then `OPENAI_API_KEY`, `FAL_API_KEY`,
236
+ `A2E_API_KEY`; makes an image, ElevenLabs speech (`MEDIA_VOICE` picks the voice) and calls the
237
+ `generate_image` tool; `MEDIA_VIDEO=1` adds a Grok video, `MEDIA_PAID=1` paid video/talk. It prints
238
+ client, verb, path or URL, mime type, size and usage per file, exits non-zero if a step fails, and
239
+ exits 0 with a hint when nothing is available.
240
+
201
241
  ## MCP 2026-07-28 + OAuth
202
242
 
203
243
  `loadMcp` / `Toolkit.create({ mcpConfig })` now speak MCP **2026-07-28** first, with no MCP SDK on
package/dist/client.d.ts CHANGED
@@ -1,8 +1,3 @@
1
- /**
2
- * Unified LLM client — the host loop. Give it a plain base URL + a style
3
- * ("openai" | "anthropic"), and it runs the tool-calling agent loop against a
4
- * Toolkit. See ../../SPEC.md §8.
5
- */
6
1
  import type { Toolkit } from "./toolkit.js";
7
2
  import type { ToolHints, ToolResult, Request, Answer } from "./types.js";
8
3
  import { type PromptCacheOptions } from "./promptcache.js";
@@ -368,9 +363,12 @@ export type RunFile = {
368
363
  url?: string;
369
364
  data?: string;
370
365
  name?: string;
366
+ path?: string;
371
367
  from: string;
372
368
  callId: string;
373
369
  };
370
+ /** RunResult.files: every non-text part of every tool call, in call order (§8). */
371
+ export declare function runFiles(toolCalls: ToolCallRecord[]): RunFile[];
374
372
  /** Token usage, summed across every LLM round trip in the run. */
375
373
  export interface Usage {
376
374
  promptTokens: number;
package/dist/client.js CHANGED
@@ -1,10 +1,16 @@
1
+ /**
2
+ * Unified LLM client — the host loop. Give it a plain base URL + a style
3
+ * ("openai" | "anthropic"), and it runs the tool-calling agent loop against a
4
+ * Toolkit. See ../../SPEC.md §8.
5
+ */
6
+ import { mediaPartPath } from "./media.js";
1
7
  import { pendingOf } from "./types.js";
2
8
  import { toOpenAI, toAnthropic } from "./adapters.js";
3
9
  import { applyPromptCache } from "./promptcache.js";
4
10
  import { buildToolPlan, toolSearch, unwrapCallTool } from "./tooltiers.js";
5
11
  import { isRetryableStatus, retryAfterMs } from "./retry.js";
6
12
  import { isConcurrencySafe, resolveMaxToolConcurrency, scheduleCalls } from "./toolconcurrency.js";
7
- import { toOpenAIWire, toAnthropicWire, checkPromptParts } from "./wire.js";
13
+ import { toOpenAIWire, toAnthropicWire, checkPromptParts, modelParts } from "./wire.js";
8
14
  import { finishReasonFor, argsString, openAIMessagesToAnthropic, openAIToolsToAnthropic, openAIToolChoiceToAnthropic, hasSystemMessage, } from "./translate.js";
9
15
  /**
10
16
  * The §8 CLIENT status vocabulary, as a value. It is a DIFFERENT, smaller closed set than the
@@ -139,13 +145,15 @@ function delay(ms, signal) {
139
145
  /** Model tool-call id of each record, kept off the record itself (§8 shape has no id). */
140
146
  const callIds = new WeakMap();
141
147
  /** RunResult.files: every non-text part of every tool call, in call order (§8). */
142
- function runFiles(toolCalls) {
148
+ export function runFiles(toolCalls) {
143
149
  const out = [];
144
150
  for (const r of toolCalls) {
145
151
  for (const p of r.parts ?? []) {
146
152
  if (p.type === "text")
147
153
  continue;
148
- out.push({ ...p, from: r.name, callId: callIds.get(r) ?? "" });
154
+ // D20: a media file's local path rides on the run file, never on the part.
155
+ const local = mediaPartPath(p);
156
+ out.push({ ...p, ...(local !== undefined ? { path: local } : {}), from: r.name, callId: callIds.get(r) ?? "" });
149
157
  }
150
158
  }
151
159
  return out;
@@ -1858,17 +1866,19 @@ function objArgs(v) {
1858
1866
  */
1859
1867
  function toolMessage(id, name, result) {
1860
1868
  const m = { role: "tool", tool_call_id: id, content: result.output };
1861
- if (result.parts?.length) {
1869
+ const parts = modelParts(result);
1870
+ if (parts.length) {
1862
1871
  m.name = name;
1863
- m.parts = result.parts;
1872
+ m.parts = parts;
1864
1873
  }
1865
1874
  return m;
1866
1875
  }
1867
1876
  /** A canonical Anthropic `tool_result` block; parts ride along and are emitted natively. */
1868
1877
  function toolResultBlock(id, result) {
1869
1878
  const b = { type: "tool_result", tool_use_id: id, content: result.output, is_error: result.isError };
1870
- if (result.parts?.length)
1871
- b.parts = result.parts;
1879
+ const parts = modelParts(result);
1880
+ if (parts.length)
1881
+ b.parts = parts;
1872
1882
  return b;
1873
1883
  }
1874
1884
  const INTERRUPTED = { output: "interrupted: the turn ended before this tool returned. Whether its side effects happened is unknown. Do not re-run it automatically; check first.", isError: true };