toolnexus 0.18.1 → 0.20.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/dist/acp.d.ts +42 -0
- package/dist/acp.js +246 -0
- package/dist/agents/agent.d.ts +12 -1
- package/dist/agents/agent.js +3 -2
- package/dist/agents/home.js +4 -2
- package/dist/agents/loop.d.ts +21 -0
- package/dist/agents/loop.js +38 -2
- package/dist/agents/runtime.d.ts +76 -6
- package/dist/agents/runtime.js +92 -22
- package/dist/builtin.d.ts +32 -2
- package/dist/builtin.js +342 -58
- package/dist/classifier.d.ts +23 -0
- package/dist/classifier.js +33 -25
- package/dist/client.d.ts +116 -16
- package/dist/client.js +197 -43
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/order.d.ts +46 -0
- package/dist/order.js +71 -0
- package/dist/skill.d.ts +13 -0
- package/dist/skill.js +110 -27
- package/dist/types.d.ts +11 -0
- package/dist/types.js +22 -0
- package/package.json +2 -2
- package/src/acp.ts +308 -0
- package/src/agents/agent.ts +15 -3
- package/src/agents/home.ts +4 -2
- package/src/agents/loop.ts +40 -2
- package/src/agents/runtime.ts +154 -29
- package/src/builtin.ts +379 -55
- package/src/classifier.ts +45 -27
- package/src/client.ts +220 -51
- package/src/index.ts +1 -0
- package/src/order.ts +70 -0
- package/src/skill.ts +121 -27
- package/src/types.ts +22 -0
package/dist/agents/runtime.js
CHANGED
|
@@ -1,16 +1,60 @@
|
|
|
1
|
-
import { createClient, InMemoryConversationStore } from "../client.js";
|
|
1
|
+
import { createClient, createInProcessFetch, InMemoryConversationStore, } from "../client.js";
|
|
2
2
|
import { runGated } from "./loop.js";
|
|
3
3
|
import { createToolkit } from "../toolkit.js";
|
|
4
4
|
import { defineTool } from "../native.js";
|
|
5
|
-
/** The real clock (default).
|
|
5
|
+
/** The real clock (default). See `Clock.setTimeout` for why `keepAlive` defaults to true. */
|
|
6
6
|
export const systemClock = {
|
|
7
7
|
now: () => Date.now(),
|
|
8
|
-
setTimeout(fn, ms) {
|
|
8
|
+
setTimeout(fn, ms, opts) {
|
|
9
9
|
const t = setTimeout(fn, ms);
|
|
10
|
-
|
|
10
|
+
if (opts?.keepAlive === false)
|
|
11
|
+
t.unref?.();
|
|
11
12
|
return () => clearTimeout(t);
|
|
12
13
|
},
|
|
13
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* The §7D AGENT status vocabulary, as a value. It is a DIFFERENT, larger closed set than the
|
|
17
|
+
* §8 CLIENT vocabulary (`RUN_STATUSES` = done/pending/incomplete): only this one contains
|
|
18
|
+
* `"timeout"`, and `"timeout"` here means a `wait()` DEADLINE expired while the child keeps
|
|
19
|
+
* running — never that an LLM call timed out. Two fields are called `status`; they are not
|
|
20
|
+
* interchangeable (ADR 0027 D2.1).
|
|
21
|
+
*/
|
|
22
|
+
export const TASK_STATUSES = [
|
|
23
|
+
"done", "pending", "incomplete", "interrupted", "closed", "timeout", "error",
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* The CLOSED `limit` vocabulary (addendum A14). A value NAMES THE BUDGET FIELD that stopped the
|
|
27
|
+
* run, spelled exactly as `Budget` spells it, plus the two non-budget stops `completion` and
|
|
28
|
+
* `timeout`. Identical in all seven ports, like the `loopUnsupported` strings — "which limit
|
|
29
|
+
* stopped me" is only answerable if the answer is the same word everywhere.
|
|
30
|
+
*
|
|
31
|
+
* A port's INTERNAL pool/dimension name is an implementation detail and must never leak into
|
|
32
|
+
* the field; `canonicalLimit` is the boundary that enforces that here.
|
|
33
|
+
*/
|
|
34
|
+
export const TASK_LIMITS = [
|
|
35
|
+
"maxTurns", "maxTokens", "maxToolCalls", "maxWallMs", "maxChildren", "maxConcurrent", "maxDepth",
|
|
36
|
+
"completion", "timeout",
|
|
37
|
+
];
|
|
38
|
+
/** Map anything the internals produce onto the closed vocabulary; unknown ⇒ undefined, never a
|
|
39
|
+
* leaked internal name. */
|
|
40
|
+
export function canonicalLimit(limit) {
|
|
41
|
+
return limit && TASK_LIMITS.includes(limit) ? limit : undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* NAMED CONSTANTS for the two closed vocabularies. Every `TaskResult` construction site in this
|
|
45
|
+
* file spells its status and limit through these, never as a string literal, so a rename cannot
|
|
46
|
+
* silently desync the two fields — which is exactly how `status:"timeout"` came to ship with an
|
|
47
|
+
* empty `limit` in three of the seven ports (addendum A18).
|
|
48
|
+
*/
|
|
49
|
+
export const S = Object.freeze({
|
|
50
|
+
done: "done", pending: "pending", incomplete: "incomplete", interrupted: "interrupted",
|
|
51
|
+
closed: "closed", timeout: "timeout", error: "error",
|
|
52
|
+
});
|
|
53
|
+
export const L = Object.freeze({
|
|
54
|
+
maxTurns: "maxTurns", maxTokens: "maxTokens", maxToolCalls: "maxToolCalls", maxWallMs: "maxWallMs",
|
|
55
|
+
maxChildren: "maxChildren", maxConcurrent: "maxConcurrent", maxDepth: "maxDepth",
|
|
56
|
+
completion: "completion", timeout: "timeout",
|
|
57
|
+
});
|
|
14
58
|
/** Type guard for the `spawn` return union. */
|
|
15
59
|
export function isVerbError(x) {
|
|
16
60
|
return typeof x === "object" && x !== null && "error" in x && typeof x.error === "string";
|
|
@@ -36,6 +80,8 @@ export class Handle {
|
|
|
36
80
|
deadline;
|
|
37
81
|
/** Total tokens attributed to this handle (own turns + descendant roll-up). */
|
|
38
82
|
usageTotal = 0;
|
|
83
|
+
/** Tokens spent by THIS handle's own turns, excluding descendants (ADR 0025 D1). */
|
|
84
|
+
ownTokens = 0;
|
|
39
85
|
/** Total tool calls attributed to this handle (own + descendant roll-up). */
|
|
40
86
|
toolCallsTotal = 0;
|
|
41
87
|
/** LLM round trips run by this handle itself (never reset — resume grows it). */
|
|
@@ -112,6 +158,18 @@ export class AgentRuntime {
|
|
|
112
158
|
turnSlots;
|
|
113
159
|
turnQueue = [];
|
|
114
160
|
constructor(opts) {
|
|
161
|
+
if (opts.fetch && opts.inProcess) {
|
|
162
|
+
throw new Error("toolnexus/agents: RuntimeOptions.fetch and RuntimeOptions.inProcess are mutually exclusive — set one, not both");
|
|
163
|
+
}
|
|
164
|
+
if (opts.llm && opts.inProcess) {
|
|
165
|
+
throw new Error("toolnexus/agents: RuntimeOptions.llm and RuntimeOptions.inProcess are mutually exclusive — an in-process model has no wire endpoint to point llm at");
|
|
166
|
+
}
|
|
167
|
+
if (opts.inProcess) {
|
|
168
|
+
// Reuse the SAME adapter createInProcessClient builds on (ADR 0030) — the
|
|
169
|
+
// runtime's in-process wiring is a CALLER of that export, not a second copy
|
|
170
|
+
// of the request/response assembly.
|
|
171
|
+
opts = { ...opts, fetch: createInProcessFetch(opts.inProcess) };
|
|
172
|
+
}
|
|
115
173
|
this.opts = opts;
|
|
116
174
|
this.registry = opts.registry;
|
|
117
175
|
this.clock = opts.clock ?? systemClock;
|
|
@@ -207,11 +265,11 @@ export class AgentRuntime {
|
|
|
207
265
|
if (opts?.by && h.parent !== opts.by) {
|
|
208
266
|
return Promise.resolve({
|
|
209
267
|
text: `wait refused: only the spawner may wait on ${h.id}`,
|
|
210
|
-
isError: true, status:
|
|
268
|
+
isError: true, status: S.error, turns: h.turnsTotal, totalTokens: h.usageTotal, ownTokens: h.ownTokens,
|
|
211
269
|
});
|
|
212
270
|
}
|
|
213
271
|
if (h.state === "closed") {
|
|
214
|
-
return Promise.resolve(h.lastResult ?? { text: "closed", isError: true, status:
|
|
272
|
+
return Promise.resolve(h.lastResult ?? { text: "closed", isError: true, status: S.closed, turns: h.turnsTotal, totalTokens: h.usageTotal, ownTokens: h.ownTokens });
|
|
215
273
|
}
|
|
216
274
|
if (h.state === "suspended")
|
|
217
275
|
return Promise.resolve(this.pendingResult(h));
|
|
@@ -233,7 +291,7 @@ export class AgentRuntime {
|
|
|
233
291
|
const i = h.waiters.indexOf(finish);
|
|
234
292
|
if (i >= 0)
|
|
235
293
|
h.waiters.splice(i, 1);
|
|
236
|
-
finish({ text: `wait timeout after ${opts.timeoutMs}ms (child still ${h.state})`, isError: true, status:
|
|
294
|
+
finish({ text: `wait timeout after ${opts.timeoutMs}ms (child still ${h.state})`, isError: true, status: S.timeout, limit: L.timeout, turns: h.turnsTotal, totalTokens: h.usageTotal, ownTokens: h.ownTokens });
|
|
237
295
|
}, opts.timeoutMs);
|
|
238
296
|
}
|
|
239
297
|
});
|
|
@@ -290,7 +348,7 @@ export class AgentRuntime {
|
|
|
290
348
|
await h.def.onClose?.(h, reason);
|
|
291
349
|
const prev = h.state;
|
|
292
350
|
h.state = "closed";
|
|
293
|
-
const final = { text: "closed", isError: true, status:
|
|
351
|
+
const final = { text: "closed", isError: true, status: S.closed, turns: h.turnsTotal, totalTokens: h.usageTotal, ownTokens: h.ownTokens };
|
|
294
352
|
for (const w of h.waiters.splice(0))
|
|
295
353
|
w(final);
|
|
296
354
|
this.t(`${h.id}: ${prev}→closed (${reason})`);
|
|
@@ -324,13 +382,16 @@ export class AgentRuntime {
|
|
|
324
382
|
if (!leaf)
|
|
325
383
|
throw new Error("no suspended handle to resume");
|
|
326
384
|
this.t(`${leaf.id}: resume with Answer(ok=${answer.ok}) at checkpoint (turns so far: ${leaf.turnsTotal})`);
|
|
327
|
-
|
|
385
|
+
// The value is the settled result of the TOPMOST handle the cascade re-ran — what the host
|
|
386
|
+
// would have got from `Agent.run` had the suspension never happened (ADR 0025 D3).
|
|
387
|
+
let topmost = await this.resumeSuspended(leaf, answer, { inline: false });
|
|
328
388
|
for (let p = leaf.parent; p && p !== this.root; p = p.parent) {
|
|
329
389
|
if (p.state !== "suspended")
|
|
330
390
|
break;
|
|
331
391
|
this.t(`${p.id}: cascade resume (reattaching delegated work)`);
|
|
332
|
-
await this.resumeSuspended(p, undefined, { inline: false });
|
|
392
|
+
topmost = await this.resumeSuspended(p, undefined, { inline: false });
|
|
333
393
|
}
|
|
394
|
+
return topmost;
|
|
334
395
|
}
|
|
335
396
|
deepestSuspended(h) {
|
|
336
397
|
for (const c of h.children) {
|
|
@@ -377,22 +438,25 @@ export class AgentRuntime {
|
|
|
377
438
|
const now = this.clock.now();
|
|
378
439
|
for (let a = h; a; a = a.parent) {
|
|
379
440
|
if (a.pool.tokens <= 0)
|
|
380
|
-
return
|
|
441
|
+
return L.maxTokens;
|
|
381
442
|
if (a.pool.toolCalls <= 0)
|
|
382
|
-
return
|
|
443
|
+
return L.maxToolCalls;
|
|
383
444
|
if (a.deadline !== undefined && now >= a.deadline)
|
|
384
|
-
return
|
|
445
|
+
return L.maxWallMs;
|
|
385
446
|
}
|
|
386
447
|
return undefined;
|
|
387
448
|
}
|
|
388
449
|
/** Lifetime turn cap on the handle itself (turns never reset across resumes). */
|
|
389
450
|
turnCap(h) {
|
|
390
|
-
return h.turnsTotal >= h.eff.maxTurns ?
|
|
451
|
+
return h.turnsTotal >= h.eff.maxTurns ? L.maxTurns : undefined;
|
|
391
452
|
}
|
|
392
453
|
/** A limit stop is LOUD: settle an incomplete result (never silent, never a throw). */
|
|
393
454
|
settleRefused(h, limit) {
|
|
455
|
+
// `limit` here is already a Budget field name (`poolLimit`/`turnCap` return them verbatim);
|
|
456
|
+
// `canonicalLimit` below is the guard that keeps it that way if either ever changes.
|
|
394
457
|
const text = `budget exhausted (${limit}); partial work preserved`;
|
|
395
|
-
|
|
458
|
+
// The exhausted pool is COMPUTED here and used to be discarded into prose. `limit` carries it.
|
|
459
|
+
const r = { text, isError: true, status: S.incomplete, turns: h.turnsTotal, totalTokens: h.usageTotal, ownTokens: h.ownTokens, limit };
|
|
396
460
|
h.lastResult = r;
|
|
397
461
|
for (const w of h.waiters.splice(0))
|
|
398
462
|
w(r);
|
|
@@ -400,6 +464,7 @@ export class AgentRuntime {
|
|
|
400
464
|
}
|
|
401
465
|
/** Usage roll-up IS the budget ledger: every ancestor's pool drains live. */
|
|
402
466
|
rollUp(h, tokens, toolCalls) {
|
|
467
|
+
h.ownTokens += tokens; // own figure: outside the ancestor walk, by construction
|
|
403
468
|
for (let a = h; a; a = a.parent) {
|
|
404
469
|
a.usageTotal += tokens;
|
|
405
470
|
a.toolCallsTotal += toolCalls;
|
|
@@ -524,7 +589,7 @@ export class AgentRuntime {
|
|
|
524
589
|
}
|
|
525
590
|
pendingResult(h) {
|
|
526
591
|
const req = h.pendingRequest;
|
|
527
|
-
return { text: req?.prompt ?? "", isError: false, status:
|
|
592
|
+
return { text: req?.prompt ?? "", isError: false, status: S.pending, pending: req, turns: h.turnsTotal, totalTokens: h.usageTotal, ownTokens: h.ownTokens };
|
|
528
593
|
}
|
|
529
594
|
/** The escalation seam: this handle's §10 interpreter authority, with the
|
|
530
595
|
* escalation traced (nearest interpreter wins; the Request's `data.path` names
|
|
@@ -592,7 +657,7 @@ export class AgentRuntime {
|
|
|
592
657
|
h.checkpoint = { input };
|
|
593
658
|
h.drained = []; // drained items ride the checkpointed input, not the inbox
|
|
594
659
|
this.t(`${h.id}: running→suspended (pending "${stamped.kind}")`);
|
|
595
|
-
result = { text: r.text, isError: false, status:
|
|
660
|
+
result = { text: r.text, isError: false, status: S.pending, pending: stamped, turns: h.turnsTotal, totalTokens: h.usageTotal, ownTokens: h.ownTokens };
|
|
596
661
|
}
|
|
597
662
|
else if (r.status === "incomplete") {
|
|
598
663
|
h.state = "idle";
|
|
@@ -602,14 +667,18 @@ export class AgentRuntime {
|
|
|
602
667
|
// generic sentence. Reading `limit` is how the runtime tells them apart.
|
|
603
668
|
result = {
|
|
604
669
|
text: r.limit === "completion" ? r.text : "hit maxTurns without a final answer",
|
|
605
|
-
isError: true, status:
|
|
670
|
+
isError: true, status: S.incomplete, turns: h.turnsTotal, totalTokens: h.usageTotal, ownTokens: h.ownTokens,
|
|
671
|
+
// The LATENT instance golang found: forwarding the client's `limit` straight through
|
|
672
|
+
// would hand a host `incomplete` with an empty limit whenever the client omits it.
|
|
673
|
+
// The `?? L.maxTurns` is what makes the invariant hold by construction, not by luck.
|
|
674
|
+
limit: canonicalLimit(r.limit) ?? L.maxTurns,
|
|
606
675
|
};
|
|
607
676
|
}
|
|
608
677
|
else {
|
|
609
678
|
h.state = "idle";
|
|
610
679
|
h.drained = [];
|
|
611
680
|
this.t(`${h.id}: running→idle (done, turns=${r.turns}, tokens=${r.usage.totalTokens})`);
|
|
612
|
-
result = { text: r.text, isError: false, status:
|
|
681
|
+
result = { text: r.text, isError: false, status: S.done, turns: h.turnsTotal, totalTokens: h.usageTotal, ownTokens: h.ownTokens };
|
|
613
682
|
}
|
|
614
683
|
}
|
|
615
684
|
catch (e) {
|
|
@@ -624,8 +693,9 @@ export class AgentRuntime {
|
|
|
624
693
|
this.t(`${h.id}: running→idle (${aborted ? "interrupted; inbox intact" : `error: ${msg}`})`);
|
|
625
694
|
result = {
|
|
626
695
|
text: msg, isError: true,
|
|
627
|
-
|
|
628
|
-
|
|
696
|
+
// None of these three is a limit stop, so none carries a `limit` (A18).
|
|
697
|
+
status: closing ? S.closed : aborted ? S.interrupted : S.error,
|
|
698
|
+
turns: h.turnsTotal, totalTokens: h.usageTotal, ownTokens: h.ownTokens,
|
|
629
699
|
};
|
|
630
700
|
}
|
|
631
701
|
finally {
|
|
@@ -703,7 +773,7 @@ export class AgentRuntime {
|
|
|
703
773
|
r = await rt.wait(existing, { by: parent });
|
|
704
774
|
}
|
|
705
775
|
else {
|
|
706
|
-
r = existing.lastResult ?? { text: `no recorded result for ${existing.id}`, isError: true, status:
|
|
776
|
+
r = existing.lastResult ?? { text: `no recorded result for ${existing.id}`, isError: true, status: S.error, turns: existing.turnsTotal, totalTokens: existing.usageTotal, ownTokens: existing.ownTokens };
|
|
707
777
|
}
|
|
708
778
|
}
|
|
709
779
|
else {
|
package/dist/builtin.d.ts
CHANGED
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
import type { Tool } from "./types.js";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Config for the single global builtin toggle (mirrors MCP isEnabled precedence),
|
|
4
|
+
* plus the host boundary: which interpreter `bash` runs, what a relative path
|
|
5
|
+
* resolves against, and whether paths leaving it are refused (SPEC §4A, ADR 0034).
|
|
6
|
+
*/
|
|
3
7
|
export type BuiltinsConfig = boolean | {
|
|
4
8
|
enabled?: boolean;
|
|
5
9
|
disabled?: boolean;
|
|
6
10
|
tools?: Record<string, boolean>;
|
|
11
|
+
/**
|
|
12
|
+
* The argv prefix `bash` runs a command with — `["sh","-c"]`,
|
|
13
|
+
* `["cmd","/d","/s","/c"]`, `["powershell","-NoProfile","-Command"]`. Set,
|
|
14
|
+
* it is used verbatim. Absent, an interpreter is DETECTED at toolkit
|
|
15
|
+
* construction (POSIX `sh -c`; Windows %COMSPEC% → pwsh → powershell → bash).
|
|
16
|
+
*/
|
|
17
|
+
shell?: string[];
|
|
18
|
+
/**
|
|
19
|
+
* What a RELATIVE path means, for every builtin that touches the
|
|
20
|
+
* filesystem — including the paths inside apply_patch's patch text, and as
|
|
21
|
+
* bash's default workdir. Empty ⇒ the process cwd, i.e. today's behaviour.
|
|
22
|
+
*/
|
|
23
|
+
baseDir?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Refuse any path whose CANONICAL form leaves baseDir (symlinks, Windows
|
|
26
|
+
* junctions and short names resolved on both sides), and Windows reserved
|
|
27
|
+
* device names. Default off. A guarantee about path resolution in the file
|
|
28
|
+
* builtins, NOT a sandbox — `bash` still reaches the whole filesystem.
|
|
29
|
+
*/
|
|
30
|
+
confineToBaseDir?: boolean;
|
|
7
31
|
};
|
|
8
32
|
/**
|
|
9
33
|
* Whether the builtin source is on. Default ON. Same precedence as MCP:
|
|
@@ -22,4 +46,10 @@ export declare function selectBuiltins(cfg: BuiltinsConfig | undefined): Tool[];
|
|
|
22
46
|
* for parity: bash, read, write, edit, grep, glob, webfetch, question,
|
|
23
47
|
* apply_patch, todowrite.
|
|
24
48
|
*/
|
|
25
|
-
export declare function createBuiltinTools(): Tool[];
|
|
49
|
+
export declare function createBuiltinTools(cfg?: BuiltinsConfig): Tool[];
|
|
50
|
+
/**
|
|
51
|
+
* The interpreter the builtins resolved for `bash` — what a host prints, and
|
|
52
|
+
* what `metadata.shell` carries on every bash result. Throws the same error
|
|
53
|
+
* `selectBuiltins` fails construction with when nothing resolves.
|
|
54
|
+
*/
|
|
55
|
+
export declare function builtinShell(cfg?: BuiltinsConfig): string[];
|