toolnexus 0.27.0 → 0.28.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.
@@ -317,7 +321,7 @@ Everything else — the full surface, with runnable examples — lives on the do
317
321
  | **Start here** | [Quickstart](https://muthuishere.github.io/toolnexus/quickstart/) · [Concepts](https://muthuishere.github.io/toolnexus/concepts/) · [Install](https://muthuishere.github.io/toolnexus/install/) |
318
322
  | **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
323
  | **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/) |
324
+ | **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
325
  | **API reference** | [JavaScript](https://muthuishere.github.io/toolnexus/api/javascript/) |
322
326
  | **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
327
 
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). */