toolnexus 0.27.0 → 0.29.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
@@ -92,6 +92,9 @@ tool. Runnable: `npm run example:prompt-cache` ([`examples/prompt_cache.ts`](exa
92
92
  `after`; `read` / `grep` report truncation in metadata.
93
93
  - `agents.compactor({ onCompact })`, `agent.asToolWith(ctx => ({ hooks, waitFor }))`, runtime
94
94
  `waitFor`, `ctx.callId` on tools, `listModels(opts)`.
95
+ - `compactOnOverflow: agents.compactor({ maxTokens: tighter })` — when the provider rejects a prompt as too
96
+ long (`isContextOverflow(status, body)`), compact and re-send the same turn; three rapid
97
+ compactions in a row fail with `ContextOverflowError` (SPEC §7F.1).
95
98
 
96
99
  An in-process model streams for real when it is given as `generateStream(request, emit)`; a plain
97
100
  `generate` still refuses `stream()`.
@@ -152,7 +155,8 @@ const rs = await c.states("a", "b").evaluate(judge.noul("q", "…")) // [{
152
155
 
153
156
  - `backend`: `typesafe` (default) · `openrouter` (`typesafe/jev-1.14`) · `cloudflare` (`/ai/run`
154
157
  envelope; `${CLOUDFLARE_ACCOUNT_ID}` in the URL expands at call time, errors name it unexpanded)
155
- · `vercel` · `custom` (requires `baseUrl` and `supports`). The table is data:
158
+ · `vercel` · `openai` (OpenAI Decisions API, `POST {baseUrl}/decisions`, `gpt-6-luna`,
159
+ `OPENAI_API_KEY`; raw questions are unsupported, `calibrated` is false) · `custom` (requires `baseUrl` and `supports`). The table is data:
156
160
  `CLASSIFIER_BACKENDS`; `supports: { images }` overrides it per classifier.
157
161
  - Images (style `systemone` only): image parts in the state are lifted into `assets` (cloudflare:
158
162
  data URLs). PNG/JPEG, 280–1568 px a side, ≤ 16 MiB each and 32 MiB per request — never resized.
@@ -173,27 +177,67 @@ const rs = await c.states("a", "b").evaluate(judge.noul("q", "…")) // [{
173
177
 
174
178
  ## Media generation
175
179
 
176
- Generate images, video and speech (plus transcription, voice cloning and talking-head video)
177
- from a **JSON style** instead of provider code. Async jobs are started and polled for you. The same
178
- verbs become agent tools (`generate_image`, `generate_video`, `generate_speech`, …), and whatever
179
- they produce lands in `RunResult.files`. `examples/media-styles/a2e.json` ships first and reads
180
- `A2E_API_KEY` at call time. Its result URLs expire in 3–30 days, so save what you keep. SPEC §7G;
181
- 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/>.
182
188
 
183
189
  ```ts
184
- import { loadMediaStyle, createMedia, createToolkit } from "toolnexus"
190
+ import { generateMedia, createMedia, loadMediaStyle, createToolkit, MediaStyle } from "toolnexus"
185
191
 
186
- const media = createMedia(loadMediaStyle("examples/media-styles/a2e.json"), {
187
- onProgress: ({ verb, status }) => console.log(verb, status),
188
- })
189
- const img = await media.image({ prompt: "a red fox in fresh snow", width: 1024, height: 768 })
190
- 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)
191
195
 
192
- const tk = await createToolkit({ media }) // the agent gets generate_image, …
193
- const res = await agent.run("Draw a fox, then animate it.", { toolkit: tk })
194
- 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")
195
200
  ```
196
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
+
197
241
  ## MCP 2026-07-28 + OAuth
198
242
 
199
243
  `loadMcp` / `Toolkit.create({ mcpConfig })` now speak MCP **2026-07-28** first, with no MCP SDK on
@@ -317,7 +361,7 @@ Everything else — the full surface, with runnable examples — lives on the do
317
361
  | **Start here** | [Quickstart](https://muthuishere.github.io/toolnexus/quickstart/) · [Concepts](https://muthuishere.github.io/toolnexus/concepts/) · [Install](https://muthuishere.github.io/toolnexus/install/) |
318
362
  | **Tool sources** | [MCP](https://muthuishere.github.io/toolnexus/mcp/) · [Skills](https://muthuishere.github.io/toolnexus/skills/) · [Native](https://muthuishere.github.io/toolnexus/native/) · [HTTP](https://muthuishere.github.io/toolnexus/http/) · [Built-ins](https://muthuishere.github.io/toolnexus/builtins/) · [A2A](https://muthuishere.github.io/toolnexus/a2a/) |
319
363
  | **The loop** | [Streaming](https://muthuishere.github.io/toolnexus/streaming/) · [Memory](https://muthuishere.github.io/toolnexus/memory/) · [Suspension](https://muthuishere.github.io/toolnexus/suspension/) · [Resilience](https://muthuishere.github.io/toolnexus/resilience/) · [Observability](https://muthuishere.github.io/toolnexus/observability/) |
320
- | **Agents** | [Sub-agents & teams](https://muthuishere.github.io/toolnexus/subagents/) · [Personas](https://muthuishere.github.io/toolnexus/persona-agents/) · [Typed decisions](https://muthuishere.github.io/toolnexus/judge/) |
364
+ | **Agents** | [Sub-agents & teams](https://muthuishere.github.io/toolnexus/subagents/) · [Personas](https://muthuishere.github.io/toolnexus/persona-agents/) · [Dreaming](https://muthuishere.github.io/toolnexus/dreaming/) · [Typed decisions](https://muthuishere.github.io/toolnexus/judge/) |
321
365
  | **API reference** | [JavaScript](https://muthuishere.github.io/toolnexus/api/javascript/) |
322
366
  | **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/) |
323
367
 
package/dist/acp.d.ts CHANGED
@@ -65,6 +65,12 @@ export interface ACPClient {
65
65
  * Shaped for {@link InProcessOptions.generateStream}. The returned content is the
66
66
  * message text only. */
67
67
  generateStream: (request: InProcessRequest, emit: (event: StreamEvent) => void, opts?: ACPGenerateOptions) => Promise<InProcessResponse>;
68
+ /** Sends `session/set_mode {sessionId, modeId}` on the running session (no restart). The
69
+ * agent's error rejects this call only; the client stays usable. */
70
+ setMode: (mode: string) => Promise<void>;
71
+ /** Changes the model on the running session exactly as the `model` option does:
72
+ * `session/set_config_option {configId:"model"}`, then `session/set_model`; both failing rejects. */
73
+ setModel: (model: string) => Promise<void>;
68
74
  /** Terminates the agent's process group. Idempotent — a second call is a no-op. */
69
75
  close: () => Promise<void>;
70
76
  /** The agent's raw `initialize` result. */
package/dist/acp.js CHANGED
@@ -276,22 +276,23 @@ export async function loadACP(opts) {
276
276
  if (opts.mode) {
277
277
  await step("session/set_mode", () => call("session/set_mode", { sessionId, modeId: opts.mode }));
278
278
  }
279
- if (opts.model) {
280
- // Passed through unchanged. Stable ACP spelling first, then the older session/set_model.
281
- const model = opts.model;
282
- await step(`set model ${JSON.stringify(model)}`, async () => {
279
+ // Passed through unchanged. Stable ACP spelling first, then the older session/set_model.
280
+ const sendModel = async (model) => {
281
+ try {
282
+ await call("session/set_config_option", { sessionId, configId: "model", value: model });
283
+ }
284
+ catch (e1) {
283
285
  try {
284
- await call("session/set_config_option", { sessionId, configId: "model", value: model });
286
+ await call("session/set_model", { sessionId, modelId: model });
285
287
  }
286
- catch (e1) {
287
- try {
288
- await call("session/set_model", { sessionId, modelId: model });
289
- }
290
- catch (e2) {
291
- throw new Error(`${e1.message}; ${e2.message}`);
292
- }
288
+ catch (e2) {
289
+ throw new Error(`${e1.message}; ${e2.message}`);
293
290
  }
294
- });
291
+ }
292
+ };
293
+ if (opts.model) {
294
+ const model = opts.model;
295
+ await step(`set model ${JSON.stringify(model)}`, () => sendModel(model));
295
296
  }
296
297
  // Serialises turns: one ACP session is one conversation, so concurrent
297
298
  // prompts must not interleave into it. Idiomatic promise-chaining queue —
@@ -340,12 +341,20 @@ export async function loadACP(opts) {
340
341
  break;
341
342
  }
342
343
  case "tool_call_update": {
344
+ const callId = u.toolCallId ?? "";
345
+ // A non-empty title is remembered as the call's name for every later event (#188).
346
+ if (u.title)
347
+ toolNames.set(callId, u.title);
348
+ const name = toolNames.get(callId) || "";
349
+ const raw = u.rawInput;
350
+ if (raw && typeof raw === "object" && !Array.isArray(raw) && Object.keys(raw).length > 0) {
351
+ send({ type: "tool_call_args", id: callId, name, args: raw, via: "acp" });
352
+ }
343
353
  if (u.status !== "completed" && u.status !== "failed")
344
354
  break;
345
- const name = u.title || toolNames.get(u.toolCallId ?? "") || "";
346
355
  send({
347
356
  type: "tool_result",
348
- id: u.toolCallId ?? "",
357
+ id: callId,
349
358
  name,
350
359
  output: acpToolContentText(u.content),
351
360
  isError: u.status === "failed",
@@ -427,7 +436,20 @@ export async function loadACP(opts) {
427
436
  await Promise.race([childGone, new Promise((r) => setTimeout(r, 1000).unref())]);
428
437
  }
429
438
  const generate = (request, callOpts) => generateStream(request, undefined, callOpts);
430
- const client = { generate, generateStream, close, initializeResult };
439
+ // SPEC §8C (#197): sent at once on the running session — no restart, not queued behind a turn.
440
+ const liveCall = async (label, fn) => {
441
+ if (closed)
442
+ throw new Error("toolnexus: ACP client is closed");
443
+ try {
444
+ await fn();
445
+ }
446
+ catch (e) {
447
+ throw new Error(`toolnexus: acp: ${label}: ${e instanceof Error ? e.message : String(e)}`);
448
+ }
449
+ };
450
+ const setMode = (mode) => liveCall("session/set_mode", () => call("session/set_mode", { sessionId, modeId: mode }));
451
+ const setModel = (model) => liveCall(`set model ${JSON.stringify(model)}`, () => sendModel(model));
452
+ const client = { generate, generateStream, setMode, setModel, close, initializeResult };
431
453
  // Non-enumerable, undocumented — internal test hook only, so the public
432
454
  // ACPClient surface stays exactly {generate, generateStream, close}.
433
455
  Object.defineProperty(client, "_pid", { value: child.pid, enumerable: false });
@@ -1,6 +1,7 @@
1
1
  import type { Tool } from "../types.js";
2
2
  import { Agent, type AgentRunOptions } from "./agent.js";
3
3
  import { AgentRuntime } from "./runtime.js";
4
+ import type { ConversationStore } from "../client.js";
4
5
  /**
5
6
  * Bootstrap discovery order (§7E): identity first, durable memory last. Each present
6
7
  * file is injected into the soul as a `## <filename>` section, in exactly this order.
@@ -16,29 +17,72 @@ export declare const HEARTBEAT_OK = "HEARTBEAT_OK";
16
17
  * model knows the silent-reply contract, and the word "Heartbeat" so a HEARTBEAT.md
17
18
  * that keys off it can recognise the trigger. */
18
19
  export declare const HEARTBEAT_PROMPT: string;
20
+ /** The preset index cap (§7E "Memory hierarchy"): Claude Code's 200 lines / 25 KB. */
21
+ export declare const MEMORY_INDEX_CAP_PRESET: {
22
+ readonly maxLines: 200;
23
+ readonly maxBytes: 25600;
24
+ };
25
+ /** `memoryIndexCap` option: `true` ⇒ the preset; an object's missing field ⇒ the preset's. */
26
+ export type MemoryIndexCap = boolean | {
27
+ maxLines?: number;
28
+ maxBytes?: number;
29
+ };
30
+ /** Options for {@link composeSoul}. Absent ⇒ byte-identical to the pre-hierarchy soul. */
31
+ export interface ComposeSoulOptions {
32
+ /** Second, smaller injection cap for `MEMORY.md` only (whole lines). Off by default. */
33
+ memoryIndexCap?: MemoryIndexCap;
34
+ /** Notes tier on — selects the notice wording that points at `note:<slug>`. */
35
+ notes?: boolean;
36
+ }
37
+ /** The exact notice appended to a capped `MEMORY.md` (§7E "Memory hierarchy"). */
38
+ export declare function memoryIndexNotice(maxLines: number, maxBytes: number, notes: boolean): string;
39
+ /**
40
+ * Apply the index cap to `MEMORY.md`'s (already 2 MB-capped) content. Lines are split on
41
+ * "\n"; one trailing "\n" does not start a line. Within both bounds ⇒ returned unchanged.
42
+ * Otherwise keep the first `maxLines` lines, drop trailing whole lines until the "\n"-joined
43
+ * UTF-8 length is <= `maxBytes` (a lone over-long first line is cut at a character
44
+ * boundary), and append {@link memoryIndexNotice}.
45
+ */
46
+ export declare function applyIndexCap(content: string, cap: {
47
+ maxLines: number;
48
+ maxBytes: number;
49
+ }, notes?: boolean): string;
19
50
  /**
20
51
  * Compose the bootstrap files present in `dir` into one soul string — the frozen
21
52
  * snapshot injected as the system prompt for the whole run. Only present files
22
53
  * appear, always in {@link BOOTSTRAP_ORDER}, each as a `## <filename>` section.
23
54
  */
24
- export declare function composeSoul(dir: string): {
55
+ export declare function composeSoul(dir: string, opts?: ComposeSoulOptions): {
25
56
  soul: string;
26
57
  found: string[];
27
58
  };
59
+ /** A valid note slug (§7E "Memory hierarchy"): lowercase, no path separators, no `..`. */
60
+ export declare const NOTE_SLUG_RE: RegExp;
61
+ /** Options for {@link memoryTool}. */
62
+ export interface MemoryToolOptions {
63
+ /** Turn on the notes tier: `note:<slug>` targets plus `read`/`list`. Off ⇒ the tool is
64
+ * byte-identical to the pre-hierarchy tool (schema, description, behavior). */
65
+ notes?: boolean;
66
+ }
67
+ /** The notes-on description (§7E): says what memory is FOR, not rules about what to record. */
68
+ export declare const MEMORY_NOTES_DESCRIPTION: string;
28
69
  /**
29
70
  * The `memory` builtin (§7E, file-backed, opt-in). One tool, three actions over
30
71
  * `MEMORY.md` (the agent's own notes) and `USER.md` (its model of the user):
31
72
  *
32
73
  * - `add` — append an entry
33
- * - `replace` — swap an existing substring for `with`
34
- * - `remove` — delete an existing substring
74
+ * - `replace` — swap the first occurrence of an existing substring for `with`
75
+ * - `remove` — delete the first occurrence of an existing substring
76
+ *
77
+ * With `{notes: true}` it also takes `target: "note:<slug>"` (`<dir>/memory/<slug>.md`,
78
+ * never injected into the soul) and two read actions, `read` and `list`.
35
79
  *
36
80
  * Every action writes to **disk**. It does **not** touch the live session's system
37
81
  * prompt: the frozen-snapshot rule means the edit loads only at the START of the
38
82
  * next session (keeping a long-lived persona cache-stable). A `replace`/`remove`
39
83
  * whose substring is absent is a loud `isError` — never a silent no-op.
40
84
  */
41
- export declare function memoryTool(dir: string): Tool;
85
+ export declare function memoryTool(dir: string, opts?: MemoryToolOptions): Tool;
42
86
  /** Options for {@link fromDir}. */
43
87
  export interface FromDirOptions {
44
88
  /** Routing description a delegating model sees; default derived from the dir. */
@@ -51,6 +95,17 @@ export interface FromDirOptions {
51
95
  tools?: Tool[];
52
96
  /** Set `false` to omit the memory tool (a read-only persona). */
53
97
  memory?: boolean;
98
+ /** Cap the injected `MEMORY.md` (whole lines; `true` ⇒ 200 lines / 25600 bytes). Off by default. */
99
+ memoryIndexCap?: MemoryIndexCap;
100
+ /** Turn on the notes tier of the memory tool (`note:<slug>`, `read`, `list`). Off by default. */
101
+ notes?: boolean;
102
+ /** Dream on a schedule: when this persona is started with {@link startAgent}, a
103
+ * {@link dreamer} over the same dir runs on the same runtime every `everyMs`. */
104
+ dream?: {
105
+ everyMs: number;
106
+ store: ConversationStore;
107
+ instructions?: string;
108
+ };
54
109
  }
55
110
  /**
56
111
  * The directory IS the agent (§7E). Discovers the bootstrap files in `dir`, composes
@@ -59,6 +114,32 @@ export interface FromDirOptions {
59
114
  * all apply.
60
115
  */
61
116
  export declare function fromDir(dir: string, opts?: FromDirOptions): Agent;
117
+ /**
118
+ * What a dream pass is FOR and the order it works in (§7E "Dreaming"). Byte-identical in
119
+ * every port. It becomes the dreamer's `## HEARTBEAT.md` section, so the ordinary
120
+ * {@link HEARTBEAT_PROMPT} wake drives a pass and `HEARTBEAT_OK` keeps an idle pass silent.
121
+ */
122
+ export declare const DREAM_PROMPT: string;
123
+ /** Options for {@link dreamer}. */
124
+ export interface DreamerOptions {
125
+ /** The transcripts to verify memory against — must implement `list()`. Required. */
126
+ store: ConversationStore;
127
+ /** Host prose appended after {@link DREAM_PROMPT} (and after `DREAM.md`, if present). */
128
+ instructions?: string;
129
+ /** Agent name; default `<basename(dir)>-dreamer`. */
130
+ name?: string;
131
+ /** Model id; default `"inherit"` — the persona's client (use a cheap model there). */
132
+ model?: string;
133
+ }
134
+ /**
135
+ * The built-in dreamer preset (§7E "Dreaming"): a plain {@link Agent} whose tools are
136
+ * EXACTLY `memory` (notes on, over `dir`) and `transcript_search` (over `store`) — never
137
+ * the persona's shell/file tools. Its soul is one `## HEARTBEAT.md` section holding
138
+ * {@link DREAM_PROMPT}, then `DREAM.md` from `dir` (trimmed, if non-empty), then
139
+ * `instructions` (trimmed, if non-empty), joined by blank lines. Schedule it with
140
+ * {@link startAgent} or run one pass with `run(HEARTBEAT_PROMPT, …)`.
141
+ */
142
+ export declare function dreamer(dir: string, opts: DreamerOptions): Agent;
62
143
  /** Heartbeat configuration for {@link startAgent}. */
63
144
  export interface HeartbeatOptions {
64
145
  /** Interval between beats, in ms (measured on the runtime's clock). */