@sema-agent/core 2.3.0 → 2.4.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/agents/send-message-tool.d.ts +4 -0
- package/dist/agents/send-message-tool.js +37 -24
- package/dist/agents/subagent.js +275 -127
- package/dist/brain/errors.d.ts +1 -0
- package/dist/brain/errors.js +14 -0
- package/dist/brain/stream-engine.js +3 -3
- package/dist/core/context-edit.js +2 -1
- package/dist/core/runner/prepare-task.js +20 -1
- package/dist/core/runner/tool-output-projection.js +2 -1
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +37 -0
- package/dist/core/store-contracts/checkpoint-store-contract.js +195 -0
- package/dist/core/store-contracts/contract-harness.d.ts +6 -0
- package/dist/core/store-contracts/contract-harness.js +16 -0
- package/dist/core/store-contracts/contract-kit-version.d.ts +1 -0
- package/dist/core/store-contracts/contract-kit-version.js +2 -0
- package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +3 -0
- package/dist/core/store-contracts/file-snapshot-store-contract.js +126 -0
- package/dist/core/store-contracts/mailbox-store-contract.d.ts +6 -0
- package/dist/core/store-contracts/mailbox-store-contract.js +193 -0
- package/dist/core/store-contracts/session-repo-contract.d.ts +3 -0
- package/dist/core/store-contracts/session-repo-contract.js +36 -0
- package/dist/core/store-contracts/tool-result-store-contract.d.ts +3 -0
- package/dist/core/store-contracts/tool-result-store-contract.js +35 -0
- package/dist/core/task-notification.d.ts +2 -0
- package/dist/core/task-registry-agent.d.ts +4 -0
- package/dist/core/task-registry-agent.js +13 -0
- package/dist/core/task-registry-monitor.js +6 -6
- package/dist/core/task-registry-shared.d.ts +8 -2
- package/dist/core/task-registry-shared.js +1 -1
- package/dist/core/task-registry.d.ts +6 -0
- package/dist/core/task-registry.js +48 -4
- package/dist/core/tool-result-store.d.ts +3 -2
- package/dist/core/tool-result-store.js +12 -4
- package/dist/core/trace.d.ts +7 -0
- package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
- package/dist/engine/lsp/node-lsp-manager.js +16 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/orchestration/builtin-workflows.d.ts +1 -1
- package/dist/orchestration/builtin-workflows.js +11 -2
- package/dist/orchestration/workflow-governance.d.ts +6 -1
- package/dist/orchestration/workflow-governance.js +24 -4
- package/dist/orchestration/workflow-primitives.js +7 -1
- package/dist/orchestration/workflow.d.ts +1 -0
- package/dist/orchestration/workflow.js +31 -2
- package/dist/tools/fs/fs-bash.d.ts +7 -1
- package/dist/tools/fs/fs-bash.js +51 -20
- package/dist/tools/fs/fs-read.js +22 -11
- package/dist/tools/fs/fs-search-tools.js +3 -3
- package/dist/tools/fs/fs-shared.d.ts +20 -7
- package/dist/tools/fs/fs-shared.js +17 -3
- package/dist/tools/fs/fs-write.js +4 -4
- package/dist/tools/fs/index.d.ts +2 -0
- package/dist/tools/fs/index.js +7 -1
- package/dist/tools/fs/repo-map.js +2 -2
- package/dist/tools/fs/safety.d.ts +10 -0
- package/dist/tools/fs/safety.js +15 -1
- package/dist/tools/monitor.js +18 -4
- package/dist/tools/web.js +6 -2
- package/dist/tools/worktree.js +46 -25
- package/package.json +1 -1
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { beginContract } from "./contract-harness.js";
|
|
3
|
+
export const MAILBOX_CONTRACT_SCOPE = "default";
|
|
4
|
+
const msg = (content, sentAt = 1000) => ({ content, sentAt });
|
|
5
|
+
async function withStores(mk, fn) {
|
|
6
|
+
const live = [];
|
|
7
|
+
const make = () => {
|
|
8
|
+
const s = mk();
|
|
9
|
+
live.push(s);
|
|
10
|
+
return s;
|
|
11
|
+
};
|
|
12
|
+
try {
|
|
13
|
+
await fn(make);
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
for (const s of live.splice(0))
|
|
17
|
+
s.close?.();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function mailboxStoreContract(mk, runAssertion) {
|
|
21
|
+
const { run: runRaw, settle } = beginContract(runAssertion);
|
|
22
|
+
const run = (name, fn) => runRaw(name, () => withStores(mk, fn));
|
|
23
|
+
const S = MAILBOX_CONTRACT_SCOPE;
|
|
24
|
+
run("append 铸单调 seq;claimLease 取全部可见消息(不删);ack 才删;活租约=单消费者", async (make) => {
|
|
25
|
+
const s = make();
|
|
26
|
+
assert.equal(await s.append(S, "a1", msg("m1")), 1);
|
|
27
|
+
assert.equal(await s.append(S, "a1", msg("m2")), 2);
|
|
28
|
+
assert.equal(await s.peekCount(S, "a1"), 2);
|
|
29
|
+
const lease = await s.claimLease(S, "a1", "w1", 60_000, 10_000);
|
|
30
|
+
assert.deepEqual(lease.messages.map((m) => m.content), ["m1", "m2"]);
|
|
31
|
+
assert.equal(lease.maxSeq, 2);
|
|
32
|
+
assert.equal(await s.claimLease(S, "a1", "w2", 60_000, 20_000), null);
|
|
33
|
+
assert.equal(await s.peekCount(S, "a1"), 2);
|
|
34
|
+
await s.ack(S, "a1", "w1", 2);
|
|
35
|
+
assert.equal(await s.peekCount(S, "a1"), 0);
|
|
36
|
+
});
|
|
37
|
+
run("X-1 无损:lease 过期/release 后消息按原 seq 回可见;seq 在箱的一生中永不复用", async (make) => {
|
|
38
|
+
const s = make();
|
|
39
|
+
await s.append(S, "a1", msg("m1"));
|
|
40
|
+
const l1 = await s.claimLease(S, "a1", "w1", 1_000, 10_000);
|
|
41
|
+
assert.equal(l1.maxSeq, 1);
|
|
42
|
+
const l2 = await s.claimLease(S, "a1", "w2", 1_000, 20_000);
|
|
43
|
+
assert.deepEqual(l2.messages.map((m) => m.seq), [1]);
|
|
44
|
+
await s.releaseLease(S, "a1", "w2");
|
|
45
|
+
const l3 = await s.claimLease(S, "a1", "w3", 1_000, 20_500);
|
|
46
|
+
assert.deepEqual(l3.messages.map((m) => m.seq), [1]);
|
|
47
|
+
await s.ack(S, "a1", "w3", 1);
|
|
48
|
+
assert.equal(await s.append(S, "a1", msg("m2")), 2);
|
|
49
|
+
});
|
|
50
|
+
run("lease 窗内 append:后到消息在下次 claim 时可见(lease 只覆盖 claim 时刻快照)", async (make) => {
|
|
51
|
+
const s = make();
|
|
52
|
+
await s.append(S, "a1", msg("m1"));
|
|
53
|
+
const l = await s.claimLease(S, "a1", "w1", 60_000, 10_000);
|
|
54
|
+
assert.equal(l.maxSeq, 1);
|
|
55
|
+
await s.append(S, "a1", msg("m2"));
|
|
56
|
+
await s.ack(S, "a1", "w1", l.maxSeq);
|
|
57
|
+
assert.equal(await s.peekCount(S, "a1"), 1);
|
|
58
|
+
const next = await s.claimLease(S, "a1", "w2", 60_000, 20_000);
|
|
59
|
+
assert.deepEqual(next.messages.map((m) => m.content), ["m2"]);
|
|
60
|
+
});
|
|
61
|
+
run("drop 清整箱;reap 按最新消息龄整箱老化", async (make) => {
|
|
62
|
+
const s = make();
|
|
63
|
+
await s.append(S, "a1", msg("old", 1_000));
|
|
64
|
+
await s.append(S, "a2", msg("fresh", 90_000));
|
|
65
|
+
assert.equal(await s.reap(S, 100_000, { maxAgeMs: 50_000 }), 1);
|
|
66
|
+
assert.equal(await s.peekCount(S, "a1"), 0);
|
|
67
|
+
assert.equal(await s.peekCount(S, "a2"), 1);
|
|
68
|
+
await s.drop(S, "a2");
|
|
69
|
+
assert.equal(await s.peekCount(S, "a2"), 0);
|
|
70
|
+
});
|
|
71
|
+
run("RB-86 空箱 ⇒ null:没消息就不发租约(既不假装投递,也不 fence 掉后来真正的消费者)", async (make) => {
|
|
72
|
+
const s = make();
|
|
73
|
+
assert.equal(await s.claimLease(S, "never-written", "o1", 60_000, 1_000), null, "从未写过的箱");
|
|
74
|
+
await s.append(S, "a1", msg("m1"));
|
|
75
|
+
const first = await s.claimLease(S, "a1", "o1", 60_000, 1_000);
|
|
76
|
+
await s.ack(S, "a1", "o1", first.maxSeq);
|
|
77
|
+
assert.equal(await s.peekCount(S, "a1"), 0);
|
|
78
|
+
assert.equal(await s.claimLease(S, "a1", "o1", 60_000, 2_000), null, "全部 ack 后箱内零可见消息");
|
|
79
|
+
assert.equal(await s.claimLease(S, "a2", "ghost", 60_000, 3_000), null);
|
|
80
|
+
await s.append(S, "a2", msg("real", 4_000));
|
|
81
|
+
const real = await s.claimLease(S, "a2", "o2", 60_000, 4_000);
|
|
82
|
+
assert.deepEqual(real?.messages.map((m) => m.content), ["real"], "空取不该留下 fence 后来者的幽灵租约");
|
|
83
|
+
});
|
|
84
|
+
run("RB-261 守恒律:一条消息在被 ack 消费之前必须至少投递过一次(maxSeq 不得覆盖未投递项)", async (make) => {
|
|
85
|
+
const s = make();
|
|
86
|
+
const appended = ["m1", "m2", "m3"];
|
|
87
|
+
for (const [i, c] of appended.entries())
|
|
88
|
+
await s.append(S, "a1", msg(c, 1_000 + i));
|
|
89
|
+
const delivered = new Set();
|
|
90
|
+
let round = 0;
|
|
91
|
+
while ((await s.peekCount(S, "a1")) > 0) {
|
|
92
|
+
round += 1;
|
|
93
|
+
assert.ok(round < 10, "要么有限轮内收敛,要么就是投递不动了");
|
|
94
|
+
const owner = `w${round}`;
|
|
95
|
+
const lease = await s.claimLease(S, "a1", owner, 60_000, 10_000 + round);
|
|
96
|
+
assert.notEqual(lease, null, "还有可见消息时 claimLease 不得返回 null");
|
|
97
|
+
for (const m of lease.messages)
|
|
98
|
+
delivered.add(m.content);
|
|
99
|
+
await s.ack(S, "a1", owner, lease.maxSeq);
|
|
100
|
+
}
|
|
101
|
+
assert.deepEqual([...delivered].sort(), [...appended].sort(), "每条被消费掉的消息都必须先被投递过");
|
|
102
|
+
});
|
|
103
|
+
run("releaseLease owner-fenced:非持有者的 release 是 no-op(活租约的单消费者窗口不被外人撬开)", async (make) => {
|
|
104
|
+
const s = make();
|
|
105
|
+
await s.append(S, "a1", msg("m1"));
|
|
106
|
+
const held = await s.claimLease(S, "a1", "owner-A", 60_000, 10_000);
|
|
107
|
+
assert.equal(held.messages.length, 1);
|
|
108
|
+
await s.releaseLease(S, "a1", "not-the-owner");
|
|
109
|
+
assert.equal(await s.claimLease(S, "a1", "rival", 60_000, 11_000), null, "A 的活租约必须仍然 fence 住 rival");
|
|
110
|
+
await s.releaseLease(S, "a1", "owner-A");
|
|
111
|
+
const after = await s.claimLease(S, "a1", "rival", 60_000, 12_000);
|
|
112
|
+
assert.deepEqual(after?.messages.map((m) => m.seq), [1], "归还后同一条按原 seq 回可见");
|
|
113
|
+
});
|
|
114
|
+
await settle();
|
|
115
|
+
}
|
|
116
|
+
export async function mailboxAckOwnershipContract(mk, runAssertion) {
|
|
117
|
+
const { run: runRaw, settle } = beginContract(runAssertion);
|
|
118
|
+
const run = (name, fn) => runRaw(name, () => withStores(mk, fn));
|
|
119
|
+
const S = MAILBOX_CONTRACT_SCOPE;
|
|
120
|
+
run("a non-holder's ack does not delete messages held under another owner's LIVE lease", async (make) => {
|
|
121
|
+
const s = make();
|
|
122
|
+
await s.append(S, "a1", { content: "m1", sentAt: 1 });
|
|
123
|
+
await s.append(S, "a1", { content: "m2", sentAt: 2 });
|
|
124
|
+
const lease = await s.claimLease(S, "a1", "consumer-A", 60_000, 10_000);
|
|
125
|
+
assert.equal(lease?.messages.length, 2);
|
|
126
|
+
await s.ack(S, "a1", "stale-actor", lease.maxSeq);
|
|
127
|
+
assert.equal(await s.peekCount(S, "a1"), 2, "the messages must survive — this caller never held the lease");
|
|
128
|
+
});
|
|
129
|
+
run("angle 4 — internal consistency: releaseLease and ack now apply the SAME owner fence (was the asymmetry that proved this was an oversight)", async (make) => {
|
|
130
|
+
const s = make();
|
|
131
|
+
await s.append(S, "a1", { content: "m1", sentAt: 1 });
|
|
132
|
+
await s.claimLease(S, "a1", "real-owner", 60_000, 10_000);
|
|
133
|
+
await s.releaseLease(S, "a1", "wrong-owner");
|
|
134
|
+
await s.ack(S, "a1", "wrong-owner", 1);
|
|
135
|
+
assert.equal(await s.peekCount(S, "a1"), 1, "neither operation should have touched anything");
|
|
136
|
+
});
|
|
137
|
+
run("angle 5 — ack with no lease EVER claimed is a no-op, not a silent wipe", async (make) => {
|
|
138
|
+
const s = make();
|
|
139
|
+
await s.append(S, "a1", { content: "m1", sentAt: 1 });
|
|
140
|
+
await s.ack(S, "a1", "nobody", Number.MAX_SAFE_INTEGER);
|
|
141
|
+
assert.equal(await s.peekCount(S, "a1"), 1);
|
|
142
|
+
});
|
|
143
|
+
run("the legitimate holder's own ack still works (regression check — the fix must not break the happy path)", async (make) => {
|
|
144
|
+
const s = make();
|
|
145
|
+
await s.append(S, "a1", { content: "m1", sentAt: 1 });
|
|
146
|
+
await s.append(S, "a1", { content: "m2", sentAt: 2 });
|
|
147
|
+
const lease = await s.claimLease(S, "a1", "consumer-A", 60_000, 10_000);
|
|
148
|
+
await s.ack(S, "a1", "consumer-A", lease.maxSeq);
|
|
149
|
+
assert.equal(await s.peekCount(S, "a1"), 0);
|
|
150
|
+
});
|
|
151
|
+
run("angle 3 in detail — the real loss sequence (stale owner outlives its own expired lease, a NEW owner has since taken over): the new owner's custody survives", async (make) => {
|
|
152
|
+
const s = make();
|
|
153
|
+
await s.append(S, "a1", { content: "ship-the-release", sentAt: 1 });
|
|
154
|
+
await s.append(S, "a1", { content: "notify-oncall", sentAt: 2 });
|
|
155
|
+
const leaseA = await s.claimLease(S, "a1", "consumer-A", 1_000, 1_000);
|
|
156
|
+
assert.equal(leaseA?.messages.length, 2);
|
|
157
|
+
const leaseB = await s.claimLease(S, "a1", "consumer-B", 60_000, 5_000);
|
|
158
|
+
assert.equal(leaseB?.messages.length, 2);
|
|
159
|
+
await s.ack(S, "a1", "consumer-A", leaseA.maxSeq);
|
|
160
|
+
assert.equal(await s.peekCount(S, "a1"), 2, "B's live custody must survive A's stale ack");
|
|
161
|
+
await s.ack(S, "a1", "consumer-B", leaseB.maxSeq);
|
|
162
|
+
assert.equal(await s.peekCount(S, "a1"), 0);
|
|
163
|
+
});
|
|
164
|
+
await settle();
|
|
165
|
+
}
|
|
166
|
+
export async function mailboxBundledOnlyContract(mk, runAssertion) {
|
|
167
|
+
const { run: runRaw, settle } = beginContract(runAssertion);
|
|
168
|
+
const run = (name, fn) => runRaw(name, () => withStores(mk, fn));
|
|
169
|
+
run("RB-251(黑板 [1937]):handle 含 NUL 被两个后端一致拒绝,且不得跨 (scope,handle) 串箱", async (make) => {
|
|
170
|
+
const s = make();
|
|
171
|
+
const NUL = String.fromCharCode(0);
|
|
172
|
+
await assert.rejects(s.append("s", `a${NUL}b`, msg("secret")));
|
|
173
|
+
assert.equal(await s.peekCount(`s${NUL}a`, "b"), 0, "歧义键下不得出现串箱可读");
|
|
174
|
+
await s.append("s", "plain", msg("ok"));
|
|
175
|
+
assert.equal(await s.peekCount("s", "plain"), 1);
|
|
176
|
+
});
|
|
177
|
+
run("多租户:同名 handle 在不同 scope 下是两个互不可见的箱(各自独立铸 seq)", async (make) => {
|
|
178
|
+
const s = make();
|
|
179
|
+
assert.equal(await s.append("tenant-a", "shared-handle", msg("for-a")), 1);
|
|
180
|
+
assert.equal(await s.append("tenant-b", "shared-handle", msg("for-b")), 1, "另一个 scope 的箱独立铸号");
|
|
181
|
+
assert.equal(await s.peekCount("tenant-a", "shared-handle"), 1);
|
|
182
|
+
const a = await s.claimLease("tenant-a", "shared-handle", "w", 60_000, 10_000);
|
|
183
|
+
assert.deepEqual(a.messages.map((m) => m.content), ["for-a"]);
|
|
184
|
+
const b = await s.claimLease("tenant-b", "shared-handle", "w", 60_000, 10_000);
|
|
185
|
+
assert.deepEqual(b.messages.map((m) => m.content), ["for-b"]);
|
|
186
|
+
await s.ack("tenant-a", "shared-handle", "w", a.maxSeq);
|
|
187
|
+
assert.equal(await s.peekCount("tenant-a", "shared-handle"), 0);
|
|
188
|
+
assert.equal(await s.peekCount("tenant-b", "shared-handle"), 1, "跨 scope 的 ack 不得越界");
|
|
189
|
+
await s.drop("tenant-b", "shared-handle");
|
|
190
|
+
assert.equal(await s.peekCount("tenant-b", "shared-handle"), 0);
|
|
191
|
+
});
|
|
192
|
+
await settle();
|
|
193
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { beginContract } from "./contract-harness.js";
|
|
3
|
+
const SESSION_ENTRY = (id, parentId) => ({
|
|
4
|
+
id,
|
|
5
|
+
parentId,
|
|
6
|
+
type: "message",
|
|
7
|
+
timestamp: "2026-07-30T00:00:00.000Z",
|
|
8
|
+
message: { role: "user", content: [{ type: "text", text: `t-${id}` }], timestamp: 1785340800000 },
|
|
9
|
+
});
|
|
10
|
+
export async function sessionRepoContract(make, runAssertion) {
|
|
11
|
+
const { run, settle } = beginContract(runAssertion);
|
|
12
|
+
const A = "0198fa00-0000-7000-8000-00000000000a";
|
|
13
|
+
run("RB-360 create({id}) on an id that ALREADY has history is an idempotent REOPEN — never a wipe", async () => {
|
|
14
|
+
const repo = make();
|
|
15
|
+
const sess = await repo.create({ id: A });
|
|
16
|
+
await sess.getStorage().appendEntry(SESSION_ENTRY("e1", null));
|
|
17
|
+
await sess.getStorage().appendEntry(SESSION_ENTRY("e2", "e1"));
|
|
18
|
+
const again = await repo.create({ id: A });
|
|
19
|
+
const entries = await again.getStorage().getEntries();
|
|
20
|
+
assert.deepEqual(entries.map((e) => e.id), ["e1", "e2"]);
|
|
21
|
+
const exported = (await repo.exportEntries?.(A)) ?? [];
|
|
22
|
+
assert.deepEqual(exported.map((e) => e.id), ["e1", "e2"]);
|
|
23
|
+
});
|
|
24
|
+
run("create({id}) fresh → open() sees appended entries (round-trip floor)", async () => {
|
|
25
|
+
const repo = make();
|
|
26
|
+
const sess = await repo.create({ id: A });
|
|
27
|
+
await sess.getStorage().appendEntry(SESSION_ENTRY("e1", null));
|
|
28
|
+
const opened = await repo.open({ id: A, createdAt: "" });
|
|
29
|
+
assert.deepEqual((await opened.getStorage().getEntries()).map((e) => e.id), ["e1"]);
|
|
30
|
+
});
|
|
31
|
+
run("open(missing) throws not_found — never a silent create", async () => {
|
|
32
|
+
const repo = make();
|
|
33
|
+
await assert.rejects(repo.open({ id: "0198fa00-0000-7000-8000-0000000000ff", createdAt: "" }), /not.?found/i);
|
|
34
|
+
});
|
|
35
|
+
await settle();
|
|
36
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { beginContract } from "./contract-harness.js";
|
|
3
|
+
export async function toolResultStoreContract(make, runAssertion) {
|
|
4
|
+
const { run, settle } = beginContract(runAssertion);
|
|
5
|
+
run("put/get full + sliced; write-once no-op; unknown → undefined", async () => {
|
|
6
|
+
const store = make();
|
|
7
|
+
const ref = "tr_s_c";
|
|
8
|
+
await store.put(ref, "0123456789");
|
|
9
|
+
assert.deepEqual(await store.get(ref), { content: "0123456789", offset: 0, totalChars: 10 });
|
|
10
|
+
assert.deepEqual(await store.get(ref, { offset: 2, limit: 3 }), { content: "234", offset: 2, totalChars: 10 });
|
|
11
|
+
await store.put(ref, "IGNORED");
|
|
12
|
+
assert.equal((await store.get(ref)).content, "0123456789");
|
|
13
|
+
assert.equal(await store.get("tr_unknown"), undefined);
|
|
14
|
+
});
|
|
15
|
+
run("RB-266 unsafe refs are rejected at the WRITE by every backend; the read face degrades instead", async () => {
|
|
16
|
+
const store = make();
|
|
17
|
+
const NUL = String.fromCharCode(0);
|
|
18
|
+
const unsafe = ["", ".", "..", "a/b", "../escape", "dir\\ref", `tr_x${NUL}y`];
|
|
19
|
+
for (const ref of unsafe) {
|
|
20
|
+
await assert.rejects((async () => store.put(ref, "payload"))(), `put(${JSON.stringify(ref)}) must reject — a durable backend cannot key it`);
|
|
21
|
+
assert.equal(await store.get(ref), undefined, "get of an unsafe ref degrades to undefined");
|
|
22
|
+
}
|
|
23
|
+
await store.put("tr_sess-1_call.7", "ok");
|
|
24
|
+
assert.equal((await store.get("tr_sess-1_call.7")).content, "ok");
|
|
25
|
+
});
|
|
26
|
+
run("RB-273 a ref the contract ACCEPTS round-trips on every backend (no backend is stricter than the rule)", async () => {
|
|
27
|
+
const store = make();
|
|
28
|
+
for (const ref of ["tr_sess_call:1", "tr_sess_call%2F1", "tr_sess_call 1", "tr_sess_日本語"]) {
|
|
29
|
+
await store.put(ref, `payload-${ref}`);
|
|
30
|
+
assert.equal((await store.get(ref))?.content, `payload-${ref}`, `put(${JSON.stringify(ref)}) must round-trip here`);
|
|
31
|
+
}
|
|
32
|
+
assert.equal((await store.get("tr_sess_call:1")).content, "payload-tr_sess_call:1");
|
|
33
|
+
});
|
|
34
|
+
await settle();
|
|
35
|
+
}
|
|
@@ -66,6 +66,8 @@ export declare function settleBackgroundAgentLane(core: DurableAgentCore, id: st
|
|
|
66
66
|
result?: string;
|
|
67
67
|
resultFull?: string;
|
|
68
68
|
error?: string;
|
|
69
|
+
errorCode?: string;
|
|
70
|
+
retryable?: boolean;
|
|
69
71
|
stoppedBy?: StopSource;
|
|
70
72
|
seq?: number;
|
|
71
73
|
}): "completed" | "failed" | "killed" | undefined;
|
|
@@ -98,6 +100,8 @@ export declare function settleRevivedAgentLane(core: DurableAgentCore, id: strin
|
|
|
98
100
|
result?: string;
|
|
99
101
|
resultFull?: string;
|
|
100
102
|
error?: string;
|
|
103
|
+
errorCode?: string;
|
|
104
|
+
retryable?: boolean;
|
|
101
105
|
}): "completed" | "failed" | "killed" | undefined;
|
|
102
106
|
export declare function unmarkRetainedContinuationLane(core: DurableAgentCore, id: string): void;
|
|
103
107
|
export declare function attachAgentNotifyLane(core: DurableAgentCore, id: string, notify: NonNullable<BackgroundAgentTaskHandle["notify"]>, cycle?: number): void;
|
|
@@ -3,6 +3,7 @@ import { uuidv7 } from "../internal/harness.js";
|
|
|
3
3
|
import { canAccessAgentRecord, BackgroundAgentStoreError, } from "./background-agent-store.js";
|
|
4
4
|
import { shutdownDebug } from "./shutdown-debug.js";
|
|
5
5
|
import { delimitUntrusted } from "./untrusted-text.js";
|
|
6
|
+
import { boundedRedactedSummary } from "./untrusted-egress.js";
|
|
6
7
|
import { mintCompletionId, commitCompletionIdIfEmpty, clipTaskOutput, assertOwnership, sleepPollStep, alreadyTerminalStopNote, canAccess, normalizeAgentName, closestName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR, } from "./task-registry-shared.js";
|
|
7
8
|
import { buildToolResultRef, OFFLOAD_TOOL_NAME } from "./tool-result-store.js";
|
|
8
9
|
export function ensureDurableHeartbeatLane(core) {
|
|
@@ -636,6 +637,7 @@ export async function consumeParkedFlipLane(core, id, stores, ticket) {
|
|
|
636
637
|
handle.resolveParkedStop = undefined;
|
|
637
638
|
handle.updatedAt = Date.now();
|
|
638
639
|
handle.completionId = undefined;
|
|
640
|
+
handle.terminalNotified = undefined;
|
|
639
641
|
if (flipped.seq !== undefined)
|
|
640
642
|
handle.cycleSeq = flipped.seq;
|
|
641
643
|
return true;
|
|
@@ -681,6 +683,7 @@ export function settleBackgroundAgentLane(core, id, outcome) {
|
|
|
681
683
|
handle.status = outcome.status;
|
|
682
684
|
mintCompletionId(handle);
|
|
683
685
|
handle.notify = undefined;
|
|
686
|
+
handle.onReapTerminal = undefined;
|
|
684
687
|
for (const [, , qResolve] of handle.preAttachQueue ?? [])
|
|
685
688
|
qResolve?.({ ok: false, reason: "not_running" });
|
|
686
689
|
handle.preAttachQueue = undefined;
|
|
@@ -699,6 +702,10 @@ export function settleBackgroundAgentLane(core, id, outcome) {
|
|
|
699
702
|
}
|
|
700
703
|
if (outcome.error !== undefined && !(outcome.status === "killed" && outcome.error === BG_AGENT_REAP_STOP_ERROR)) {
|
|
701
704
|
handle.error = outcome.error;
|
|
705
|
+
if (outcome.errorCode !== undefined)
|
|
706
|
+
handle.errorCode = outcome.errorCode;
|
|
707
|
+
if (outcome.retryable !== undefined)
|
|
708
|
+
handle.errorRetryable = outcome.retryable;
|
|
702
709
|
}
|
|
703
710
|
handle.updatedAt = Date.now();
|
|
704
711
|
if (outcome.seq !== undefined)
|
|
@@ -840,6 +847,7 @@ export function reviveBackgroundAgentLane(core, id, access, abort) {
|
|
|
840
847
|
handle.resultIsPartial = undefined;
|
|
841
848
|
handle.stopSource = undefined;
|
|
842
849
|
handle.completionId = undefined;
|
|
850
|
+
handle.terminalNotified = undefined;
|
|
843
851
|
handle.reviveCycle = (handle.reviveCycle ?? 0) + 1;
|
|
844
852
|
handle.cycleSeq = (handle.cycleSeq ?? 1) + 1;
|
|
845
853
|
handle.updatedAt = Date.now();
|
|
@@ -1076,6 +1084,11 @@ ${resultText}` : "(no result text)"}`;
|
|
|
1076
1084
|
retrieval_status: retrieval,
|
|
1077
1085
|
...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
|
|
1078
1086
|
...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
|
|
1087
|
+
...(handle.status === "failed" && handle.error !== undefined
|
|
1088
|
+
? { error: delimitUntrusted("agent error", boundedRedactedSummary(handle.error, 300)) }
|
|
1089
|
+
: {}),
|
|
1090
|
+
...(handle.status === "failed" && handle.errorCode !== undefined ? { errorCode: handle.errorCode } : {}),
|
|
1091
|
+
...(handle.status === "failed" && handle.errorRetryable !== undefined ? { retryable: handle.errorRetryable } : {}),
|
|
1079
1092
|
...(handle.resultIsPartial ? { partial_result: true } : {}),
|
|
1080
1093
|
...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
|
|
1081
1094
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { delimitUntrusted } from "./untrusted-text.js";
|
|
2
|
-
import { assertOwnership, defaultMonitorTimers, MONITOR_MAX_TIMEOUT_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_BATCH_WINDOW_MS, MONITOR_MAX_BATCHES_PER_MINUTE, MONITOR_STORM_BURST, MONITOR_STORM_KILL_AFTER_MS, MONITOR_LINE_BUF_CAP,
|
|
2
|
+
import { assertOwnership, defaultMonitorTimers, MONITOR_MAX_TIMEOUT_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_BATCH_WINDOW_MS, MONITOR_MAX_BATCHES_PER_MINUTE, MONITOR_STORM_BURST, MONITOR_STORM_KILL_AFTER_MS, MONITOR_LINE_BUF_CAP, MONITOR_SPILL_CAP_CHARS, TASK_OUTPUT_MAX_CHARS, mintCompletionId, clipMonitorEvent, clipMonitorLine, terminalTaskSummary, accountDroppedBytes, rollSpoolText, statusFromBackground, droppedGapNote, firstDropNote, alreadyTerminalStopNote, clipTaskOutput, sleepPollStep, } from "./task-registry-shared.js";
|
|
3
3
|
import { buildToolResultRef, OFFLOAD_TOOL_NAME } from "./tool-result-store.js";
|
|
4
4
|
export function registerMonitorLane(core, input) {
|
|
5
5
|
assertOwnership(input, "registerMonitor");
|
|
@@ -49,14 +49,14 @@ function spillRolledMonitorChunk(handle, stream, dropped) {
|
|
|
49
49
|
const store = handle.toolResultStore;
|
|
50
50
|
if (store === undefined)
|
|
51
51
|
return;
|
|
52
|
-
const used = handle.
|
|
53
|
-
if (used >=
|
|
52
|
+
const used = handle.spillCharsUsed ?? 0;
|
|
53
|
+
if (used >= MONITOR_SPILL_CAP_CHARS) {
|
|
54
54
|
handle.spillCapped = true;
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
const n = stream === "out" ? (handle.spillSegCount ?? 0) : (handle.spillErrSegCount ?? 0);
|
|
58
58
|
const ref = buildToolResultRef(handle.spillSessionId ?? "no-session", `${handle.id}_${stream}_seg${n}`);
|
|
59
|
-
handle.
|
|
59
|
+
handle.spillCharsUsed = used + dropped.length;
|
|
60
60
|
if (stream === "out")
|
|
61
61
|
handle.spillSegCount = n + 1;
|
|
62
62
|
else
|
|
@@ -88,7 +88,7 @@ function monitorSpillNote(handle) {
|
|
|
88
88
|
const coverage = handle.spillFailed === true
|
|
89
89
|
? " — a write failed partway through; the ref chain may be INCOMPLETE, read what is there"
|
|
90
90
|
: handle.spillCapped === true
|
|
91
|
-
? ` — spill cap (${
|
|
91
|
+
? ` — spill cap (${MONITOR_SPILL_CAP_CHARS} chars) reached; earlier segments retained, later rolls were not spilled`
|
|
92
92
|
: "";
|
|
93
93
|
return `; spilled to ${clauses.join(", ")} (read back via ${OFFLOAD_TOOL_NAME})${coverage}`;
|
|
94
94
|
}
|
|
@@ -359,7 +359,7 @@ function explicitStopTerminalNote(handle, alreadyGone) {
|
|
|
359
359
|
task_type: "monitor",
|
|
360
360
|
...(handle.toolUseId !== undefined ? { toolUseId: handle.toolUseId } : {}),
|
|
361
361
|
status: "killed",
|
|
362
|
-
stoppedBy: handle.stoppedBy,
|
|
362
|
+
...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
|
|
363
363
|
summary: `${terminalTaskSummary("monitor", label, "killed")} — ${detail}${droppedGapNote(handle.spool)}`,
|
|
364
364
|
...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
|
|
365
365
|
});
|
|
@@ -16,6 +16,8 @@ export interface UnifiedTaskOutput {
|
|
|
16
16
|
retrieval_status: TaskRetrievalStatus;
|
|
17
17
|
content?: string;
|
|
18
18
|
error?: string;
|
|
19
|
+
errorCode?: string;
|
|
20
|
+
retryable?: boolean;
|
|
19
21
|
stoppedBy?: StopSource;
|
|
20
22
|
seq?: number;
|
|
21
23
|
partial_result?: boolean;
|
|
@@ -135,9 +137,13 @@ export interface BackgroundAgentTaskHandle extends SemaTaskHandle {
|
|
|
135
137
|
resultFull?: string;
|
|
136
138
|
spillRef?: string;
|
|
137
139
|
error?: string;
|
|
140
|
+
errorCode?: string;
|
|
141
|
+
errorRetryable?: boolean;
|
|
138
142
|
resultIsPartial?: boolean;
|
|
139
143
|
stopSource?: StopSource;
|
|
140
144
|
stoppedBy?: StopSource;
|
|
145
|
+
onReapTerminal?: () => void;
|
|
146
|
+
terminalNotified?: true;
|
|
141
147
|
}
|
|
142
148
|
export interface MonitorTimers {
|
|
143
149
|
setInterval: (fn: () => unknown, ms: number) => unknown;
|
|
@@ -188,7 +194,7 @@ export interface MonitorTaskHandle extends SemaTaskHandle {
|
|
|
188
194
|
spillSessionId?: string;
|
|
189
195
|
spillSegCount?: number;
|
|
190
196
|
spillErrSegCount?: number;
|
|
191
|
-
|
|
197
|
+
spillCharsUsed?: number;
|
|
192
198
|
spillCapped?: true;
|
|
193
199
|
spillFailed?: true;
|
|
194
200
|
}
|
|
@@ -237,7 +243,7 @@ export declare function statusFromBackground(status: string, exitCode?: number):
|
|
|
237
243
|
export declare function rollSpoolText(spool: {
|
|
238
244
|
rolledChars: number;
|
|
239
245
|
}, s: string, cap: number, onDrop?: (dropped: string) => void): string;
|
|
240
|
-
export declare const
|
|
246
|
+
export declare const MONITOR_SPILL_CAP_CHARS: number;
|
|
241
247
|
export declare function accountDroppedBytes(spool: {
|
|
242
248
|
droppedBytes?: number;
|
|
243
249
|
dropUnknown?: true;
|
|
@@ -94,7 +94,7 @@ export function rollSpoolText(spool, s, cap, onDrop) {
|
|
|
94
94
|
onDrop?.(dropped);
|
|
95
95
|
return s.slice(0, half) + s.slice(s.length - half);
|
|
96
96
|
}
|
|
97
|
-
export const
|
|
97
|
+
export const MONITOR_SPILL_CAP_CHARS = 64 * 1024 * 1024;
|
|
98
98
|
export function accountDroppedBytes(spool, poll) {
|
|
99
99
|
const d = poll.bytesDroppedBeforeCursor ?? 0;
|
|
100
100
|
if (d > 0)
|
|
@@ -133,6 +133,8 @@ export declare class TaskRegistry {
|
|
|
133
133
|
result?: string;
|
|
134
134
|
resultFull?: string;
|
|
135
135
|
error?: string;
|
|
136
|
+
errorCode?: string;
|
|
137
|
+
retryable?: boolean;
|
|
136
138
|
stoppedBy?: StopSource;
|
|
137
139
|
seq?: number;
|
|
138
140
|
}): "completed" | "failed" | "killed" | undefined;
|
|
@@ -166,9 +168,13 @@ export declare class TaskRegistry {
|
|
|
166
168
|
result?: string;
|
|
167
169
|
resultFull?: string;
|
|
168
170
|
error?: string;
|
|
171
|
+
errorCode?: string;
|
|
172
|
+
retryable?: boolean;
|
|
169
173
|
}): "completed" | "failed" | "killed" | undefined;
|
|
170
174
|
unmarkRetainedContinuation(id: string): void;
|
|
171
175
|
attachAgentNotify(id: string, notify: NonNullable<BackgroundAgentTaskHandle["notify"]>, cycle?: number): void;
|
|
176
|
+
attachAgentTerminalNotifier(id: string, fn: () => void): void;
|
|
177
|
+
claimAgentTerminalNotify(id: string): boolean;
|
|
172
178
|
deliverToRunningAgent(id: string, access: TaskAccess, notification: import("./task-notification.js").TaskNotificationPayload, opts?: {
|
|
173
179
|
priority?: import("./task-notification.js").SystemInjectionPriority;
|
|
174
180
|
}): Promise<{
|
|
@@ -52,6 +52,23 @@ function bashMirrorGapNote(handle) {
|
|
|
52
52
|
? " [!] The output file is INCOMPLETE (one or more writes to it failed) — trust the result text / TaskOutput over the file."
|
|
53
53
|
: "";
|
|
54
54
|
}
|
|
55
|
+
function explicitStopBashTerminalNote(handle, alreadyGone) {
|
|
56
|
+
const resultText = handle.spool !== undefined ? bashTerminalResult(handle.spool) : "";
|
|
57
|
+
const detail = alreadyGone
|
|
58
|
+
? "killed before completion (stopped via TaskStop; the process was already gone)"
|
|
59
|
+
: "killed before completion (stopped via TaskStop)";
|
|
60
|
+
handle.onTerminal?.({
|
|
61
|
+
task_id: handle.id,
|
|
62
|
+
task_type: "background_bash",
|
|
63
|
+
...(handle.toolUseId !== undefined ? { toolUseId: handle.toolUseId } : {}),
|
|
64
|
+
...(handle.outputFile !== undefined ? { output_file: handle.outputFile } : {}),
|
|
65
|
+
status: "killed",
|
|
66
|
+
...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
|
|
67
|
+
summary: `${terminalTaskSummary("bash", (handle.description ?? "background command").slice(0, 200), "killed")} — ${detail}${bashMirrorGapNote(handle)}${handle.spool !== undefined ? droppedGapNote(handle.spool) : ""}`,
|
|
68
|
+
...(resultText.length > 0 ? { result: resultText, partial: true } : {}),
|
|
69
|
+
...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
55
72
|
function monitorInTimeoutWindow(handle) {
|
|
56
73
|
return handle.status === "running" && handle.deadlineAt !== undefined && handle.timers.now() < handle.deadlineAt;
|
|
57
74
|
}
|
|
@@ -145,6 +162,23 @@ export class TaskRegistry {
|
|
|
145
162
|
attachAgentNotify(id, notify, cycle) {
|
|
146
163
|
return attachAgentNotifyLane(this.core, id, notify, cycle);
|
|
147
164
|
}
|
|
165
|
+
attachAgentTerminalNotifier(id, fn) {
|
|
166
|
+
const handle = this.handles.get(id);
|
|
167
|
+
if (!handle || handle.type !== "background_agent")
|
|
168
|
+
return;
|
|
169
|
+
if (handle.status !== "running" && handle.status !== "parked" && handle.status !== "pending")
|
|
170
|
+
return;
|
|
171
|
+
handle.onReapTerminal = fn;
|
|
172
|
+
}
|
|
173
|
+
claimAgentTerminalNotify(id) {
|
|
174
|
+
const handle = this.handles.get(id);
|
|
175
|
+
if (!handle || handle.type !== "background_agent")
|
|
176
|
+
return true;
|
|
177
|
+
if (handle.terminalNotified === true)
|
|
178
|
+
return false;
|
|
179
|
+
handle.terminalNotified = true;
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
148
182
|
async deliverToRunningAgent(id, access, notification, opts) {
|
|
149
183
|
return deliverToRunningAgentLane(this.core, id, access, notification, opts);
|
|
150
184
|
}
|
|
@@ -471,8 +505,17 @@ export class TaskRegistry {
|
|
|
471
505
|
if (!canAccess(handle, access))
|
|
472
506
|
continue;
|
|
473
507
|
this.markStopSource(handle.id, "system");
|
|
508
|
+
const reapTerminalNote = handle.onReapTerminal;
|
|
474
509
|
handle.abort.abort();
|
|
475
510
|
this.settleBackgroundAgent(handle.id, { status: "killed", error: "session released" });
|
|
511
|
+
if (reapTerminalNote !== undefined && handle.terminalNotified !== true) {
|
|
512
|
+
handle.terminalNotified = true;
|
|
513
|
+
try {
|
|
514
|
+
reapTerminalNote();
|
|
515
|
+
}
|
|
516
|
+
catch {
|
|
517
|
+
}
|
|
518
|
+
}
|
|
476
519
|
reaped++;
|
|
477
520
|
}
|
|
478
521
|
this.pokeBgQuiescence(sessionId);
|
|
@@ -1122,7 +1165,7 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
|
|
|
1122
1165
|
handle.status = "killed";
|
|
1123
1166
|
handle.updatedAt = Date.now();
|
|
1124
1167
|
mintCompletionId(handle);
|
|
1125
|
-
this.
|
|
1168
|
+
this.notifyTerminalOnce(handle, () => explicitStopBashTerminalNote(handle, true));
|
|
1126
1169
|
return {
|
|
1127
1170
|
content: `Terminated ${handle.id} (the process was already gone).`,
|
|
1128
1171
|
details: {
|
|
@@ -1151,7 +1194,7 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
|
|
|
1151
1194
|
handle.status = "killed";
|
|
1152
1195
|
handle.updatedAt = Date.now();
|
|
1153
1196
|
mintCompletionId(handle);
|
|
1154
|
-
this.
|
|
1197
|
+
this.notifyTerminalOnce(handle, () => explicitStopBashTerminalNote(handle, false));
|
|
1155
1198
|
return {
|
|
1156
1199
|
content: `Terminated ${handle.id}.`,
|
|
1157
1200
|
details: {
|
|
@@ -1193,9 +1236,10 @@ export function createTaskOutputTool(opts) {
|
|
|
1193
1236
|
let effectiveTimeoutMs = requestedMs;
|
|
1194
1237
|
let waitClampNote = "";
|
|
1195
1238
|
const wall = opts.deadlineMs?.();
|
|
1239
|
+
const requestedWaitMs = Math.max(1, Math.floor(requestedMs ?? BLOCK_DEFAULT_TIMEOUT_MS));
|
|
1196
1240
|
if (args.block !== false && wall !== undefined) {
|
|
1197
1241
|
const remainMs = wall - Date.now() - BLOCK_WAIT_WRITEOUT_RESERVE_MS;
|
|
1198
|
-
const wantMs = Math.min(BLOCK_MAX_TIMEOUT_MS,
|
|
1242
|
+
const wantMs = Math.min(BLOCK_MAX_TIMEOUT_MS, requestedWaitMs);
|
|
1199
1243
|
if (remainMs <= 0) {
|
|
1200
1244
|
effectiveTimeoutMs = 1;
|
|
1201
1245
|
waitClampNote =
|
|
@@ -1203,7 +1247,7 @@ export function createTaskOutputTool(opts) {
|
|
|
1203
1247
|
}
|
|
1204
1248
|
else if (remainMs < wantMs) {
|
|
1205
1249
|
effectiveTimeoutMs = remainMs;
|
|
1206
|
-
waitClampNote = `\n\nNOTE: wait clamped to ${Math.round(remainMs / 1000)}s (requested ${Math.round(
|
|
1250
|
+
waitClampNote = `\n\nNOTE: wait clamped to ${Math.round(remainMs / 1000)}s (requested ${Math.round(requestedWaitMs / 1000)}s) — the task's wall-clock deadline is near. If the task is still running after this wait, do NOT re-wait: proceed with other work or write out your results now.`;
|
|
1207
1251
|
}
|
|
1208
1252
|
}
|
|
1209
1253
|
const r = await opts.registry.pollTask(id, { owner: ctx.taskId ?? opts.owner, scope: ctx.principal ?? opts.scope, ...((ctx.sessionId ?? opts.sessionId) !== undefined ? { sessionId: ctx.sessionId ?? opts.sessionId } : {}) }, {
|
|
@@ -45,6 +45,7 @@ export declare class ScopedToolResultStore implements ToolResultStore {
|
|
|
45
45
|
}
|
|
46
46
|
export declare function isVolatileOffloadStore(store: ToolResultStore): boolean;
|
|
47
47
|
export declare const OFFLOAD_TOOL_NAME = "ReadToolResult";
|
|
48
|
+
export declare function offloadPagebackHint(ref: string, form: "preview" | "cleared", reachableTools?: ReadonlySet<string>): string;
|
|
48
49
|
export declare const PERSISTED_OUTPUT_PREFIX = "<persisted-output ref=";
|
|
49
50
|
export declare const DEFAULT_TOOL_RESULT_THRESHOLD_CHARS = 20000;
|
|
50
51
|
export declare function firstPartyOffloadPolicy(toolName: string): {
|
|
@@ -54,6 +55,6 @@ export declare function firstPartyOffloadPolicy(toolName: string): {
|
|
|
54
55
|
export declare function buildPreview(full: string, ref: string, sizes?: {
|
|
55
56
|
head: number;
|
|
56
57
|
tail: number;
|
|
57
|
-
}): string;
|
|
58
|
-
export declare function withToolResultOffload(tool: AgentTool, store: ToolResultStore, thresholdChars: number, sessionId: string): AgentTool;
|
|
58
|
+
}, reachableTools?: ReadonlySet<string>): string;
|
|
59
|
+
export declare function withToolResultOffload(tool: AgentTool, store: ToolResultStore, thresholdChars: number, sessionId: string, reachableTools?: () => ReadonlySet<string> | undefined): AgentTool;
|
|
59
60
|
export declare function createReadToolResultTool(store: ToolResultStore): AgentTool;
|