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 +6 -2
- package/dist/acp.d.ts +6 -0
- package/dist/acp.js +38 -16
- package/dist/agents/home.d.ts +85 -4
- package/dist/agents/home.js +347 -52
- package/dist/classifier.d.ts +11 -3
- package/dist/classifier.js +120 -3
- package/dist/client.d.ts +109 -0
- package/dist/client.js +380 -75
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp.js +13 -1
- package/dist/nodefetch.d.ts +1 -0
- package/dist/nodefetch.js +156 -0
- package/dist/transcript_search.d.ts +27 -0
- package/dist/transcript_search.js +165 -0
- package/package.json +2 -2
- package/src/acp.ts +42 -15
- package/src/agents/home.ts +381 -53
- package/src/classifier.ts +117 -6
- package/src/client.ts +368 -57
- package/src/index.ts +1 -0
- package/src/mcp.ts +18 -1
- package/src/nodefetch.ts +143 -0
- package/src/transcript_search.ts +172 -0
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` · `
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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/
|
|
286
|
+
await call("session/set_model", { sessionId, modelId: model });
|
|
285
287
|
}
|
|
286
|
-
catch (
|
|
287
|
-
|
|
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:
|
|
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
|
-
|
|
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 });
|
package/dist/agents/home.d.ts
CHANGED
|
@@ -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). */
|