@zaly/agent 0.0.3
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/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/agent-BaD2kax9.mjs +68 -0
- package/dist/agent-BaD2kax9.mjs.map +1 -0
- package/dist/agent-GDLdDc2w.mjs +902 -0
- package/dist/agent-GDLdDc2w.mjs.map +1 -0
- package/dist/bash-QNMTQ1PD.mjs +142 -0
- package/dist/bash-QNMTQ1PD.mjs.map +1 -0
- package/dist/compactions-3AjPqjeZ.mjs +190 -0
- package/dist/compactions-3AjPqjeZ.mjs.map +1 -0
- package/dist/edit-Qxl4w1C2.mjs +170 -0
- package/dist/edit-Qxl4w1C2.mjs.map +1 -0
- package/dist/env-UUFv5Mye.mjs +30 -0
- package/dist/env-UUFv5Mye.mjs.map +1 -0
- package/dist/fetch-D8cmmA3E.mjs +111 -0
- package/dist/fetch-D8cmmA3E.mjs.map +1 -0
- package/dist/find-Bah9SKWX.mjs +87 -0
- package/dist/find-Bah9SKWX.mjs.map +1 -0
- package/dist/grep-B2Ax7ewy.mjs +244 -0
- package/dist/grep-B2Ax7ewy.mjs.map +1 -0
- package/dist/index-node-esm-D6YQRKEz.mjs +1537 -0
- package/dist/index-node-esm-D6YQRKEz.mjs.map +1 -0
- package/dist/index.d.mts +1605 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +948 -0
- package/dist/index.mjs.map +1 -0
- package/dist/jsonl-Dw_VBp46.mjs +134 -0
- package/dist/jsonl-Dw_VBp46.mjs.map +1 -0
- package/dist/manager-D-lWBTHG.mjs +271 -0
- package/dist/manager-D-lWBTHG.mjs.map +1 -0
- package/dist/markdown-Bv4J8cg3.mjs +53 -0
- package/dist/markdown-Bv4J8cg3.mjs.map +1 -0
- package/dist/masker-Bv9KjkDF.mjs +396 -0
- package/dist/masker-Bv9KjkDF.mjs.map +1 -0
- package/dist/model-_3ZA2xqm.mjs +27 -0
- package/dist/model-_3ZA2xqm.mjs.map +1 -0
- package/dist/notify-DpG_m9iP.mjs +96 -0
- package/dist/notify-DpG_m9iP.mjs.map +1 -0
- package/dist/read-Djsx2NZl.mjs +205 -0
- package/dist/read-Djsx2NZl.mjs.map +1 -0
- package/dist/registry-CFmR5G3J.mjs +564 -0
- package/dist/registry-CFmR5G3J.mjs.map +1 -0
- package/dist/search-CRw247ES.mjs +121 -0
- package/dist/search-CRw247ES.mjs.map +1 -0
- package/dist/session/claude.d.mts +59 -0
- package/dist/session/claude.d.mts.map +1 -0
- package/dist/session/claude.mjs +455 -0
- package/dist/session/claude.mjs.map +1 -0
- package/dist/session-CUUdWFC5.mjs +450 -0
- package/dist/session-CUUdWFC5.mjs.map +1 -0
- package/dist/session-Ds5nJz5A.d.mts +346 -0
- package/dist/session-Ds5nJz5A.d.mts.map +1 -0
- package/dist/session.d.mts +37 -0
- package/dist/session.d.mts.map +1 -0
- package/dist/session.mjs +106 -0
- package/dist/session.mjs.map +1 -0
- package/dist/subagent-baqzjRMc.mjs +136 -0
- package/dist/subagent-baqzjRMc.mjs.map +1 -0
- package/dist/swarm-BJ3gQA1Z.mjs +194 -0
- package/dist/swarm-BJ3gQA1Z.mjs.map +1 -0
- package/dist/swarm-BuFx_xeY.mjs +109 -0
- package/dist/swarm-BuFx_xeY.mjs.map +1 -0
- package/dist/tasks-D8rARHZ1.mjs +65 -0
- package/dist/tasks-D8rARHZ1.mjs.map +1 -0
- package/dist/tasks-WmC270X2.mjs +615 -0
- package/dist/tasks-WmC270X2.mjs.map +1 -0
- package/dist/tools-DW1dbcCz.mjs +695 -0
- package/dist/tools-DW1dbcCz.mjs.map +1 -0
- package/dist/truncate-BSMXgiXj.mjs +102 -0
- package/dist/truncate-BSMXgiXj.mjs.map +1 -0
- package/dist/uuid-ByXP5SO5.mjs +59 -0
- package/dist/uuid-ByXP5SO5.mjs.map +1 -0
- package/dist/wakeup-BYqTnfSp.mjs +29 -0
- package/dist/wakeup-BYqTnfSp.mjs.map +1 -0
- package/dist/write-D3L-92vj.mjs +89 -0
- package/dist/write-D3L-92vj.mjs.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,902 @@
|
|
|
1
|
+
import { r as uuidv7 } from "./uuid-ByXP5SO5.mjs";
|
|
2
|
+
import { n as taskCompletionMessage, r as taskInfoPart, t as Tasks } from "./tasks-WmC270X2.mjs";
|
|
3
|
+
import { Emitter, safeStringify, toError, toValue } from "@zaly/shared";
|
|
4
|
+
import { AiError, isContextOverflow, runTool } from "@zaly/ai";
|
|
5
|
+
//#region src/utils/usage.ts
|
|
6
|
+
/** Sum two Usages. Optional fields are only present in the result
|
|
7
|
+
* when at least one input had them set, so callers can tell "no
|
|
8
|
+
* reasoning happened this turn" apart from "0 reasoning tokens." */
|
|
9
|
+
function addUsage(a, b) {
|
|
10
|
+
const out = {
|
|
11
|
+
input: a.input + b.input,
|
|
12
|
+
output: a.output + b.output
|
|
13
|
+
};
|
|
14
|
+
if (a.cacheRead !== void 0 || b.cacheRead !== void 0) out.cacheRead = (a.cacheRead ?? 0) + (b.cacheRead ?? 0);
|
|
15
|
+
if (a.cacheWrite !== void 0 || b.cacheWrite !== void 0) out.cacheWrite = (a.cacheWrite ?? 0) + (b.cacheWrite ?? 0);
|
|
16
|
+
if (a.reasoning !== void 0 || b.reasoning !== void 0) out.reasoning = (a.reasoning ?? 0) + (b.reasoning ?? 0);
|
|
17
|
+
if (a.cost || b.cost) out.cost = addUsage(a.cost ?? empty(), b.cost ?? empty());
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
var TokenUsage = class {
|
|
21
|
+
#last = empty();
|
|
22
|
+
#total = empty();
|
|
23
|
+
constructor(messages) {
|
|
24
|
+
for (const m of messages ?? []) if (m.role === "assistant" && m.meta?.usage) this.add(m.meta.usage);
|
|
25
|
+
}
|
|
26
|
+
get cost() {
|
|
27
|
+
return this.total.cost ?? empty();
|
|
28
|
+
}
|
|
29
|
+
get input() {
|
|
30
|
+
return this.last.input;
|
|
31
|
+
}
|
|
32
|
+
get output() {
|
|
33
|
+
return this.last.output;
|
|
34
|
+
}
|
|
35
|
+
get cacheRead() {
|
|
36
|
+
return this.last.cacheRead ?? 0;
|
|
37
|
+
}
|
|
38
|
+
get cacheWrite() {
|
|
39
|
+
return this.last.cacheWrite ?? 0;
|
|
40
|
+
}
|
|
41
|
+
get reasoning() {
|
|
42
|
+
return this.last.reasoning ?? 0;
|
|
43
|
+
}
|
|
44
|
+
add(count) {
|
|
45
|
+
this.#last = count;
|
|
46
|
+
this.#total = addUsage(this.#total, count);
|
|
47
|
+
}
|
|
48
|
+
resetLast() {
|
|
49
|
+
this.#last = empty();
|
|
50
|
+
}
|
|
51
|
+
get last() {
|
|
52
|
+
return this.#last;
|
|
53
|
+
}
|
|
54
|
+
get total() {
|
|
55
|
+
return this.#total;
|
|
56
|
+
}
|
|
57
|
+
get contextSize() {
|
|
58
|
+
return this.last.input + this.last.output + (this.last.cacheRead ?? 0) + (this.last.cacheWrite ?? 0);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
function empty() {
|
|
62
|
+
return {
|
|
63
|
+
input: 0,
|
|
64
|
+
output: 0
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/stop.ts
|
|
69
|
+
/**
|
|
70
|
+
* Subscribes to an `AgentEvent` stream, accumulates the bookkeeping
|
|
71
|
+
* the loop needs to make stop-or-continue decisions, and exposes a
|
|
72
|
+
* single `detect()` to consult after each step.
|
|
73
|
+
*
|
|
74
|
+
* Lives outside `AgentSession` so concerns separate cleanly:
|
|
75
|
+
* - the session owns conversation, queues, status, and loop control;
|
|
76
|
+
* - the policy owns counters and the rules that act on them.
|
|
77
|
+
*
|
|
78
|
+
* Wire it up via `attach(session)` (returns an unsubscribe), or feed
|
|
79
|
+
* events manually with `handle(event)` for custom drivers.
|
|
80
|
+
*/
|
|
81
|
+
var StopPolicy = class {
|
|
82
|
+
#opts;
|
|
83
|
+
#steps = 0;
|
|
84
|
+
#consecutiveErrors = 0;
|
|
85
|
+
#calls = [];
|
|
86
|
+
#usage = {
|
|
87
|
+
input: 0,
|
|
88
|
+
output: 0
|
|
89
|
+
};
|
|
90
|
+
#totalUsage = {
|
|
91
|
+
input: 0,
|
|
92
|
+
output: 0
|
|
93
|
+
};
|
|
94
|
+
constructor(opts = {}) {
|
|
95
|
+
this.#opts = opts;
|
|
96
|
+
}
|
|
97
|
+
/** Token usage from the most recent step's response. */
|
|
98
|
+
get usage() {
|
|
99
|
+
return this.#usage;
|
|
100
|
+
}
|
|
101
|
+
/** Cumulative token usage across every step in the current run. */
|
|
102
|
+
get totalUsage() {
|
|
103
|
+
return this.#totalUsage;
|
|
104
|
+
}
|
|
105
|
+
get steps() {
|
|
106
|
+
return this.#steps;
|
|
107
|
+
}
|
|
108
|
+
get consecutiveErrors() {
|
|
109
|
+
return this.#consecutiveErrors;
|
|
110
|
+
}
|
|
111
|
+
/** Tool-call history fed to the loop detector. Read-only. */
|
|
112
|
+
get calls() {
|
|
113
|
+
return this.#calls;
|
|
114
|
+
}
|
|
115
|
+
/** Subscribe to an emitter. To detach later, register the listener
|
|
116
|
+
* yourself and pass the same fn to `emitter.off(fn)`:
|
|
117
|
+
*
|
|
118
|
+
* const handler = (e) => policy.handle(e)
|
|
119
|
+
* emitter.all(handler)
|
|
120
|
+
* // later:
|
|
121
|
+
* emitter.off(handler) */
|
|
122
|
+
attach(emitter) {
|
|
123
|
+
emitter.onAny((event) => this.handle(event));
|
|
124
|
+
}
|
|
125
|
+
/** Feed a single event into the policy. Public so custom drivers
|
|
126
|
+
* can drive it without going through `attach`. */
|
|
127
|
+
handle(event) {
|
|
128
|
+
switch (event.type) {
|
|
129
|
+
case "step-end":
|
|
130
|
+
this.#steps++;
|
|
131
|
+
break;
|
|
132
|
+
case "tool-call":
|
|
133
|
+
this.#calls.push(event.call);
|
|
134
|
+
break;
|
|
135
|
+
case "tool-result":
|
|
136
|
+
this.#consecutiveErrors = event.result.isError ? this.#consecutiveErrors + 1 : 0;
|
|
137
|
+
break;
|
|
138
|
+
case "stream-event":
|
|
139
|
+
if (event.event.type === "finish") {
|
|
140
|
+
this.#usage = event.event.usage;
|
|
141
|
+
this.#totalUsage = addUsage(this.#totalUsage, event.event.usage);
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** Reset per-run counters. Token totals (`usage` / `totalUsage`)
|
|
147
|
+
* persist unless `keepUsage: false` is passed — billing-style
|
|
148
|
+
* displays usually want them sticky across resets. */
|
|
149
|
+
reset(opts = {}) {
|
|
150
|
+
this.#steps = 0;
|
|
151
|
+
this.#consecutiveErrors = 0;
|
|
152
|
+
this.#calls = [];
|
|
153
|
+
if (opts.keepUsage === false) {
|
|
154
|
+
this.#usage = {
|
|
155
|
+
input: 0,
|
|
156
|
+
output: 0
|
|
157
|
+
};
|
|
158
|
+
this.#totalUsage = {
|
|
159
|
+
input: 0,
|
|
160
|
+
output: 0
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/** Decide whether the loop should stop. Returns the stop reason or
|
|
165
|
+
* `undefined` to continue. Order matters — `loop-detected` wins
|
|
166
|
+
* over `max-tool-errors` if both fire, since a model in a tight
|
|
167
|
+
* loop is the more pressing thing to surface. */
|
|
168
|
+
detect() {
|
|
169
|
+
if (this.#opts.maxSteps !== void 0 && this.#steps >= this.#opts.maxSteps) return "max-steps";
|
|
170
|
+
if (this.#detectLoop()) return "loop-detected";
|
|
171
|
+
if (this.#opts.maxToolErrors !== void 0 && this.#consecutiveErrors >= this.#opts.maxToolErrors) return "max-tool-errors";
|
|
172
|
+
if (this.#opts.tokenBudget !== void 0 && this.#totalUsage.input + this.#totalUsage.output > this.#opts.tokenBudget) return "token-budget";
|
|
173
|
+
}
|
|
174
|
+
#detectLoop() {
|
|
175
|
+
const calls = this.#calls;
|
|
176
|
+
if (calls.length === 0) return false;
|
|
177
|
+
const consecutive = this.#opts.loopConsecutive ?? 3;
|
|
178
|
+
if (Number.isFinite(consecutive) && calls.length >= consecutive) {
|
|
179
|
+
const last = hashCall(calls[calls.length - 1]);
|
|
180
|
+
let run = 1;
|
|
181
|
+
for (let i = calls.length - 2; i >= 0 && run < consecutive; i--) if (hashCall(calls[i]) === last) run++;
|
|
182
|
+
else break;
|
|
183
|
+
if (run >= consecutive) return true;
|
|
184
|
+
}
|
|
185
|
+
const window = this.#opts.loopWindow ?? 10;
|
|
186
|
+
const windowRepeats = this.#opts.loopWindowRepeats ?? 4;
|
|
187
|
+
if (Number.isFinite(windowRepeats) && calls.length >= windowRepeats) {
|
|
188
|
+
const slice = calls.slice(Math.max(0, calls.length - window));
|
|
189
|
+
const counts = /* @__PURE__ */ new Map();
|
|
190
|
+
for (const call of slice) {
|
|
191
|
+
const h = hashCall(call);
|
|
192
|
+
const next = (counts.get(h) ?? 0) + 1;
|
|
193
|
+
if (next >= windowRepeats) return true;
|
|
194
|
+
counts.set(h, next);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
function hashCall(call) {
|
|
201
|
+
return `${call.name}\0${safeStringify(call.params)}`;
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/agent.ts
|
|
205
|
+
const PRESSURE_LEVELS = [
|
|
206
|
+
.75,
|
|
207
|
+
.85,
|
|
208
|
+
.95
|
|
209
|
+
];
|
|
210
|
+
/**
|
|
211
|
+
* Long-lived agent — drives the multi-turn loop, owns the run-time
|
|
212
|
+
* status / queues, and delegates conversation state to a `Session`.
|
|
213
|
+
*
|
|
214
|
+
* Typical interactive use:
|
|
215
|
+
*
|
|
216
|
+
* ```ts
|
|
217
|
+
* const agent = await Agent.load({ model, tools })
|
|
218
|
+
* agent.session.on("node", (e) => render(e))
|
|
219
|
+
* agent.send({ role: "user", content: "hi" }) // auto-runs
|
|
220
|
+
* // …user types again later…
|
|
221
|
+
* agent.send({ role: "user", content: "follow-up" }) // queues if running
|
|
222
|
+
* ```
|
|
223
|
+
*
|
|
224
|
+
* Headless / one-shot use is just a thin wrapper on top — see
|
|
225
|
+
* `runAgent` in the test helpers.
|
|
226
|
+
*/
|
|
227
|
+
var Agent = class extends Emitter {
|
|
228
|
+
#opts;
|
|
229
|
+
#ctx;
|
|
230
|
+
#stopPolicy;
|
|
231
|
+
#tasks;
|
|
232
|
+
/** Nesting depth — see `AgentOptions.depth`. Read-only; subagents pass
|
|
233
|
+
* `parent.depth + 1` when constructing their child. */
|
|
234
|
+
depth;
|
|
235
|
+
/** Cap on `depth` — see `AgentOptions.maxDepth`. */
|
|
236
|
+
maxDepth;
|
|
237
|
+
#started = false;
|
|
238
|
+
#parent;
|
|
239
|
+
#injectQueue = [];
|
|
240
|
+
#appendQueue = [];
|
|
241
|
+
#notifyQueue = [];
|
|
242
|
+
#status = "idle";
|
|
243
|
+
#abortController;
|
|
244
|
+
#pauseRequested;
|
|
245
|
+
#running;
|
|
246
|
+
#usage;
|
|
247
|
+
/** Pending future-injects scheduled via `scheduleWakeup`. Cleared
|
|
248
|
+
* whenever the loop becomes active for any reason — the scheduled
|
|
249
|
+
* inject was a fallback "wake me up at time T if nothing else does,"
|
|
250
|
+
* and once something else has, the timer is moot. Hints from cancelled
|
|
251
|
+
* wakeups carry over as a system message so the intent doesn't
|
|
252
|
+
* evaporate. */
|
|
253
|
+
#wakeups = /* @__PURE__ */ new Map();
|
|
254
|
+
#lastStop;
|
|
255
|
+
/** Synchronous, low-level constructor. Prefer `Agent.load(opts)` —
|
|
256
|
+
* it runs the same construction *plus* any async setup (resolving
|
|
257
|
+
* `session: SessionOptions` to a built `Session`, skills discovery,
|
|
258
|
+
* future warm-ups). The constructor is `protected` so test doubles /
|
|
259
|
+
* subclasses can still call `super(init)` directly; they're
|
|
260
|
+
* responsible for providing a pre-built `Session` on `init.session`. */
|
|
261
|
+
constructor(ctx) {
|
|
262
|
+
super();
|
|
263
|
+
const opts = ctx.opts;
|
|
264
|
+
this.#ctx = ctx;
|
|
265
|
+
this.#opts = ctx.opts;
|
|
266
|
+
this.depth = opts.depth ?? 0;
|
|
267
|
+
this.maxDepth = opts.maxDepth ?? 2;
|
|
268
|
+
this.#tasks = new Tasks();
|
|
269
|
+
this.#tasks.$tools = async () => this.tools;
|
|
270
|
+
this.#tasks.heartbeatMs = opts.heartbeatMs;
|
|
271
|
+
this.#tasks.onEmitError = (error) => opts.logger?.child("tasks").error(error);
|
|
272
|
+
this.#tasks.on("task-done", ({ task }) => {
|
|
273
|
+
this.send(taskCompletionMessage(task));
|
|
274
|
+
});
|
|
275
|
+
this.#tasks.on("heartbeat", ({ running }) => {
|
|
276
|
+
this.send({
|
|
277
|
+
content: [{
|
|
278
|
+
content: [taskInfoPart(running)],
|
|
279
|
+
tag: "heartbeat",
|
|
280
|
+
type: "meta"
|
|
281
|
+
}],
|
|
282
|
+
role: "system"
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
this.#stopPolicy = new StopPolicy(opts.stop);
|
|
286
|
+
this.#stopPolicy.attach(this);
|
|
287
|
+
this.onEmitError = (error) => opts.logger?.child("events").error(error);
|
|
288
|
+
this.#ctx.attach(this);
|
|
289
|
+
this.#usage = new TokenUsage(this.messages);
|
|
290
|
+
this.#ctx.on("session", () => this.reset());
|
|
291
|
+
}
|
|
292
|
+
async reset() {
|
|
293
|
+
this.stop({
|
|
294
|
+
abort: true,
|
|
295
|
+
reason: "reset"
|
|
296
|
+
});
|
|
297
|
+
await this.waitIdle();
|
|
298
|
+
await this.#tasks.killAll();
|
|
299
|
+
this.#cancelAllWakeups();
|
|
300
|
+
this.#usage = new TokenUsage(this.messages);
|
|
301
|
+
this.#stopPolicy.reset({ keepUsage: false });
|
|
302
|
+
this.#injectQueue = [];
|
|
303
|
+
this.#appendQueue = [];
|
|
304
|
+
this.#notifyQueue = [];
|
|
305
|
+
this.#wakeups.clear();
|
|
306
|
+
this.#stop("natural");
|
|
307
|
+
}
|
|
308
|
+
/** Spawn a child agent that inherits this agent's runtime defaults —
|
|
309
|
+
* cwd, model, permissions, full effective tool list (user tools +
|
|
310
|
+
* the loaded skill tool), `depth + 1`, and `maxDepth`. Pass
|
|
311
|
+
* `overrides` to specialize: typical ones are `prompt` (the child
|
|
312
|
+
* has its own role), `session` (the child writes to its own JSONL),
|
|
313
|
+
* and `tools` (curated subset).
|
|
314
|
+
*
|
|
315
|
+
* Used by the `subagent` tool for delegation; equally useful for
|
|
316
|
+
* ad-hoc parallel-agent patterns (multiple children working on
|
|
317
|
+
* pieces of a task in parallel).
|
|
318
|
+
*
|
|
319
|
+
* Depth cap: when the child would be at `maxDepth`, the `subagent`
|
|
320
|
+
* tool is filtered out of its inherited tool list so recursion
|
|
321
|
+
* bottoms out cleanly. The model just doesn't see the tool — no
|
|
322
|
+
* error path. */
|
|
323
|
+
async child(overrides = {}) {
|
|
324
|
+
const childDepth = overrides.depth ?? this.depth + 1;
|
|
325
|
+
let tools = [...this.tools].filter((t) => t.name !== "skill");
|
|
326
|
+
if (childDepth >= this.maxDepth) tools = tools.filter((t) => t.name !== "subagent");
|
|
327
|
+
const { createAgent } = await import("./index.mjs").then((n) => n.t);
|
|
328
|
+
const ret = await createAgent({
|
|
329
|
+
cwd: this.cwd,
|
|
330
|
+
depth: childDepth,
|
|
331
|
+
mask: this.#opts.mask,
|
|
332
|
+
maxDepth: this.maxDepth,
|
|
333
|
+
model: this.model,
|
|
334
|
+
notify: this.#opts.notify,
|
|
335
|
+
permissions: await this.ctx.permissions(),
|
|
336
|
+
skills: this.#ctx.skills,
|
|
337
|
+
swarm: await this.#ctx.swarm(),
|
|
338
|
+
tools,
|
|
339
|
+
...overrides
|
|
340
|
+
});
|
|
341
|
+
ret.#parent = this;
|
|
342
|
+
await ret.start();
|
|
343
|
+
return ret;
|
|
344
|
+
}
|
|
345
|
+
/** Active conversation — delegates to the underlying `Session`. */
|
|
346
|
+
get messages() {
|
|
347
|
+
return this.session.messages;
|
|
348
|
+
}
|
|
349
|
+
get status() {
|
|
350
|
+
return this.#status;
|
|
351
|
+
}
|
|
352
|
+
get cwd() {
|
|
353
|
+
return this.#ctx.cwd;
|
|
354
|
+
}
|
|
355
|
+
get model() {
|
|
356
|
+
return this.#ctx.model;
|
|
357
|
+
}
|
|
358
|
+
get session() {
|
|
359
|
+
return this.#ctx.session;
|
|
360
|
+
}
|
|
361
|
+
get signal() {
|
|
362
|
+
return this.#abortController?.signal;
|
|
363
|
+
}
|
|
364
|
+
/** Token usage from the most recent step's response. Drives
|
|
365
|
+
* `contextSize` and any "this turn used N tokens" UI. */
|
|
366
|
+
get usage() {
|
|
367
|
+
return this.#usage;
|
|
368
|
+
}
|
|
369
|
+
/** Cumulative token usage across every step in the current run.
|
|
370
|
+
* Useful for billing-style displays. */
|
|
371
|
+
get totalUsage() {
|
|
372
|
+
return this.#usage.total;
|
|
373
|
+
}
|
|
374
|
+
/** Logical size of the current conversation in tokens — what the
|
|
375
|
+
* next request will send as input. Equals the last step's
|
|
376
|
+
* `input + output` (since the model's reply becomes part of the
|
|
377
|
+
* next prompt). Returns 0 before any step has run. */
|
|
378
|
+
get contextSize() {
|
|
379
|
+
return this.#usage.contextSize;
|
|
380
|
+
}
|
|
381
|
+
get pressure() {
|
|
382
|
+
const used = this.contextSize;
|
|
383
|
+
const limit = this.model?.spec.contextSize ?? 0;
|
|
384
|
+
const ratio = limit > 0 ? used / limit : 0;
|
|
385
|
+
return {
|
|
386
|
+
level: PRESSURE_LEVELS.findLast((t) => ratio >= t) ?? 0,
|
|
387
|
+
limit,
|
|
388
|
+
ratio,
|
|
389
|
+
used
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
get lastStop() {
|
|
393
|
+
return this.#lastStop;
|
|
394
|
+
}
|
|
395
|
+
get steps() {
|
|
396
|
+
return this.#stopPolicy.steps;
|
|
397
|
+
}
|
|
398
|
+
get parent() {
|
|
399
|
+
return this.#parent;
|
|
400
|
+
}
|
|
401
|
+
get tools() {
|
|
402
|
+
return this.#ctx.tools;
|
|
403
|
+
}
|
|
404
|
+
get prompt() {
|
|
405
|
+
return this.#ctx.prompt;
|
|
406
|
+
}
|
|
407
|
+
get ctx() {
|
|
408
|
+
return this.#ctx;
|
|
409
|
+
}
|
|
410
|
+
/** Long-running task registry — exposed for the TUI / introspection
|
|
411
|
+
* tools (`task_list`, etc.). Mutating directly is a foot-gun;
|
|
412
|
+
* prefer the agent's higher-level surface. */
|
|
413
|
+
get tasks() {
|
|
414
|
+
return this.#tasks;
|
|
415
|
+
}
|
|
416
|
+
#enqueue(message, opts = {}) {
|
|
417
|
+
const messages = Array.isArray(message) ? message : [message];
|
|
418
|
+
if (!messages.length) return;
|
|
419
|
+
for (const m of messages) m.id ??= uuidv7();
|
|
420
|
+
(opts.mode === "append" ? this.#appendQueue : this.#injectQueue).push(...messages);
|
|
421
|
+
this.emit("pending", { messages });
|
|
422
|
+
}
|
|
423
|
+
/** Append a message and (re)start the loop. The user spoke; the agent
|
|
424
|
+
* responds. If a loop is currently running, the message lands on the
|
|
425
|
+
* follow-up queue and is processed after the current turn naturally
|
|
426
|
+
* stops. Otherwise the loop starts immediately. */
|
|
427
|
+
send(message, opts = {}) {
|
|
428
|
+
this.#enqueue(message ?? [], opts);
|
|
429
|
+
if (opts.run === false) return;
|
|
430
|
+
if (this.#status === "idle" || this.#status === "paused") this.run();
|
|
431
|
+
}
|
|
432
|
+
async useTool(name, params, msg, opts = {}) {
|
|
433
|
+
const tool = this.tools.find((t) => t.name === name);
|
|
434
|
+
if (!tool) throw new Error(`cannot inject tool use: no tool named "${name}"`);
|
|
435
|
+
const id = uuidv7();
|
|
436
|
+
const call = {
|
|
437
|
+
id,
|
|
438
|
+
name,
|
|
439
|
+
params,
|
|
440
|
+
type: "tool-call"
|
|
441
|
+
};
|
|
442
|
+
const result = await runTool(tool, params, await this.#toolContext(), { preflight: false });
|
|
443
|
+
const messages = [
|
|
444
|
+
{
|
|
445
|
+
content: [{
|
|
446
|
+
content: msg,
|
|
447
|
+
data: { tool: name },
|
|
448
|
+
tag: "user-tool-use",
|
|
449
|
+
type: "meta"
|
|
450
|
+
}],
|
|
451
|
+
role: "system"
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
content: [call],
|
|
455
|
+
role: "assistant"
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
content: [{
|
|
459
|
+
content: result.content,
|
|
460
|
+
error: result.error,
|
|
461
|
+
id,
|
|
462
|
+
isError: result.isError,
|
|
463
|
+
meta: result.meta,
|
|
464
|
+
name,
|
|
465
|
+
type: "tool-result"
|
|
466
|
+
}],
|
|
467
|
+
role: "tool"
|
|
468
|
+
}
|
|
469
|
+
];
|
|
470
|
+
if (opts.hidden) for (const m of messages) m.hidden = true;
|
|
471
|
+
return {
|
|
472
|
+
call,
|
|
473
|
+
messages,
|
|
474
|
+
result
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
notify(type, data) {
|
|
478
|
+
const meta = typeof data === "string" || Array.isArray(data) ? {
|
|
479
|
+
content: data,
|
|
480
|
+
tag: type,
|
|
481
|
+
type: "meta"
|
|
482
|
+
} : {
|
|
483
|
+
data,
|
|
484
|
+
tag: type,
|
|
485
|
+
type: "meta"
|
|
486
|
+
};
|
|
487
|
+
this.#notifyQueue.push(meta);
|
|
488
|
+
}
|
|
489
|
+
/** Schedule a one-shot wake-up at `delayMs` from now. The agent will
|
|
490
|
+
* receive a system message at that time IF nothing else has woken
|
|
491
|
+
* the loop in the meantime — task-done injects, heartbeats, and user
|
|
492
|
+
* messages all auto-cancel pending wakeups (their job was to ensure
|
|
493
|
+
* the agent gets a turn; if it's already getting one, the timer is
|
|
494
|
+
* redundant).
|
|
495
|
+
*
|
|
496
|
+
* Cancelled wakeups don't silently disappear — their `hint`s, if any,
|
|
497
|
+
* are folded into a system message that lands in the next step
|
|
498
|
+
* alongside whatever woke the loop. The model sees the hints with a
|
|
499
|
+
* `status="cancelled"` marker so it knows the timer didn't fire.
|
|
500
|
+
*
|
|
501
|
+
* Returns the wakeup id (mostly for telemetry / TUI display — the
|
|
502
|
+
* model can't usefully cancel it later, since by the time it has a
|
|
503
|
+
* turn to call cancel, the wakeup has either fired or been cancelled
|
|
504
|
+
* for it). */
|
|
505
|
+
scheduleWakeup(opts) {
|
|
506
|
+
const id = uuidv7();
|
|
507
|
+
const timer = setTimeout(() => {
|
|
508
|
+
this.#wakeups.delete(id);
|
|
509
|
+
this.send({
|
|
510
|
+
content: [{
|
|
511
|
+
data: {
|
|
512
|
+
hint: opts.hint,
|
|
513
|
+
id
|
|
514
|
+
},
|
|
515
|
+
tag: "wakeup",
|
|
516
|
+
type: "meta"
|
|
517
|
+
}],
|
|
518
|
+
role: "system"
|
|
519
|
+
});
|
|
520
|
+
}, opts.delayMs);
|
|
521
|
+
timer.unref();
|
|
522
|
+
this.#wakeups.set(id, {
|
|
523
|
+
hint: opts.hint,
|
|
524
|
+
timer
|
|
525
|
+
});
|
|
526
|
+
return id;
|
|
527
|
+
}
|
|
528
|
+
/** Cancel all pending wakeups, surfacing their hints as a single
|
|
529
|
+
* system message so the model can see what was queued. Called from
|
|
530
|
+
* `#setStatus` whenever the agent transitions to `streaming` — that's
|
|
531
|
+
* the unambiguous "loop is active" signal. */
|
|
532
|
+
#cancelAllWakeups() {
|
|
533
|
+
if (this.#wakeups.size === 0) return;
|
|
534
|
+
const carried = [];
|
|
535
|
+
for (const [id, { hint, timer }] of this.#wakeups) {
|
|
536
|
+
clearTimeout(timer);
|
|
537
|
+
if (hint) carried.push({
|
|
538
|
+
hint,
|
|
539
|
+
id
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
this.#wakeups.clear();
|
|
543
|
+
if (carried.length > 0) {
|
|
544
|
+
const parts = carried.map((c) => ({
|
|
545
|
+
data: {
|
|
546
|
+
hint: c.hint,
|
|
547
|
+
id: c.id,
|
|
548
|
+
status: "cancelled"
|
|
549
|
+
},
|
|
550
|
+
tag: "wakeup",
|
|
551
|
+
type: "meta"
|
|
552
|
+
}));
|
|
553
|
+
this.#injectQueue.push({
|
|
554
|
+
content: parts,
|
|
555
|
+
role: "system"
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
/** Drive steps until the agent goes idle (no queued messages and
|
|
560
|
+
* the model stopped naturally) or hits a non-recoverable stop.
|
|
561
|
+
* Doubles as a resume from `paused` — drains queued messages and
|
|
562
|
+
* picks the loop back up. If a run is already in flight, returns
|
|
563
|
+
* the existing promise. */
|
|
564
|
+
run() {
|
|
565
|
+
if (this.#running) return this.#running;
|
|
566
|
+
this.#pauseRequested = void 0;
|
|
567
|
+
this.#abortController = void 0;
|
|
568
|
+
this.#running = this.#loop().catch((error) => {
|
|
569
|
+
this.#opts.logger?.child("run").error(error);
|
|
570
|
+
return this.#stop(error instanceof Error && error.name === "AbortError" ? "aborted" : "error", error);
|
|
571
|
+
}).finally(() => {
|
|
572
|
+
this.#abortController = void 0;
|
|
573
|
+
this.#running = void 0;
|
|
574
|
+
});
|
|
575
|
+
return this.#running;
|
|
576
|
+
}
|
|
577
|
+
async #collect() {
|
|
578
|
+
this.#abortController = new AbortController();
|
|
579
|
+
const context = {
|
|
580
|
+
messages: [...this.ctx.messages],
|
|
581
|
+
prompt: this.prompt,
|
|
582
|
+
tools: this.tools
|
|
583
|
+
};
|
|
584
|
+
await this.emitSerial("context", context);
|
|
585
|
+
const model = this.model;
|
|
586
|
+
if (!model) throw new Error("model must be loaded to collect");
|
|
587
|
+
return model.stream(context, this.#streamOpts());
|
|
588
|
+
}
|
|
589
|
+
#shouldAutoCompact() {
|
|
590
|
+
const opts = toValue(this.#opts.compaction);
|
|
591
|
+
if (!(opts?.enabled ?? true)) return false;
|
|
592
|
+
const threshold = opts?.threshold ?? .95;
|
|
593
|
+
return this.pressure.ratio >= threshold;
|
|
594
|
+
}
|
|
595
|
+
#streamOpts() {
|
|
596
|
+
const base = this.#opts.request ?? {};
|
|
597
|
+
return {
|
|
598
|
+
...base,
|
|
599
|
+
onEvent: (event) => {
|
|
600
|
+
this.emit("stream-event", { event });
|
|
601
|
+
try {
|
|
602
|
+
this.#opts.onEvent?.(event);
|
|
603
|
+
} catch (error) {
|
|
604
|
+
this.#opts.logger?.child("stream").error(error);
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
onUpdate: this.#opts.onUpdate,
|
|
608
|
+
reasoning: {
|
|
609
|
+
...base.reasoning,
|
|
610
|
+
effort: this.#ctx.reasoning
|
|
611
|
+
},
|
|
612
|
+
signal: this.#abortController?.signal
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
/** Run exactly one step. Useful for tests and custom drivers
|
|
616
|
+
* that want to interleave logic between steps. */
|
|
617
|
+
async step() {
|
|
618
|
+
if (this.#shouldAutoCompact()) await this.compact();
|
|
619
|
+
if (this.#notifyQueue.length > 0) this.#injectQueue.push({
|
|
620
|
+
content: this.#notifyQueue.splice(0),
|
|
621
|
+
role: "system"
|
|
622
|
+
});
|
|
623
|
+
for (const m of this.#injectQueue.splice(0)) await this.session.add(m);
|
|
624
|
+
for (const m of this.#appendQueue.splice(0)) await this.session.add(m);
|
|
625
|
+
this.#setStatus("streaming");
|
|
626
|
+
let collected;
|
|
627
|
+
try {
|
|
628
|
+
collected = await this.#collect();
|
|
629
|
+
} catch (error) {
|
|
630
|
+
const err = toError(error);
|
|
631
|
+
const overflow = isContextOverflow({ message: err.message });
|
|
632
|
+
if (overflow) this.#opts.logger?.warn("Context overflow detected:", err);
|
|
633
|
+
return {
|
|
634
|
+
error: err,
|
|
635
|
+
finishReason: overflow ? "length" : "error",
|
|
636
|
+
kind: overflow ? "context-overflow" : "error",
|
|
637
|
+
usage: {
|
|
638
|
+
input: 0,
|
|
639
|
+
output: 0
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
this.#usage.add(collected.meta.usage);
|
|
644
|
+
const result = {
|
|
645
|
+
finishReason: collected.meta.finishReason,
|
|
646
|
+
message: collected,
|
|
647
|
+
usage: collected.meta.usage
|
|
648
|
+
};
|
|
649
|
+
if (this.#opts.contextLimit !== void 0 && isContextOverflow({
|
|
650
|
+
contextLimit: this.#opts.contextLimit,
|
|
651
|
+
usageInput: result.usage.input + (result.usage.cacheRead ?? 0) + (result.usage.cacheWrite ?? 0)
|
|
652
|
+
})) return {
|
|
653
|
+
kind: "context-overflow",
|
|
654
|
+
...result
|
|
655
|
+
};
|
|
656
|
+
const calls = await this.#parseToolCalls(collected);
|
|
657
|
+
await this.session.add(collected);
|
|
658
|
+
if (calls.length === 0) return {
|
|
659
|
+
kind: "natural",
|
|
660
|
+
...result
|
|
661
|
+
};
|
|
662
|
+
return {
|
|
663
|
+
kind: "tool-calls",
|
|
664
|
+
toolMessage: await this.#runTools(calls),
|
|
665
|
+
...result
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
async #parseToolCalls(message) {
|
|
669
|
+
if (typeof message.content === "string") return [];
|
|
670
|
+
const calls = message.content.filter((p) => p.type === "tool-call");
|
|
671
|
+
for (const call of calls) {
|
|
672
|
+
const tool = this.tools.find((t) => t.name === call.name);
|
|
673
|
+
if (!tool) continue;
|
|
674
|
+
try {
|
|
675
|
+
call.params = await tool.validator.cleanParams(call.params) ?? call.params;
|
|
676
|
+
} catch {}
|
|
677
|
+
}
|
|
678
|
+
return calls;
|
|
679
|
+
}
|
|
680
|
+
async compact() {
|
|
681
|
+
const prev = this.#status;
|
|
682
|
+
const pressure = this.pressure;
|
|
683
|
+
this.#setStatus("compacting");
|
|
684
|
+
this.#usage.resetLast();
|
|
685
|
+
try {
|
|
686
|
+
const { Compaction } = await import("./compactions-3AjPqjeZ.mjs");
|
|
687
|
+
const opts = {
|
|
688
|
+
...toValue(this.#opts.compaction),
|
|
689
|
+
signal: this.#abortController?.signal
|
|
690
|
+
};
|
|
691
|
+
await new Compaction(this, opts).compact(pressure);
|
|
692
|
+
} finally {
|
|
693
|
+
this.#setStatus(prev);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
async #runTools(calls) {
|
|
697
|
+
this.#setStatus("running-tools");
|
|
698
|
+
this.emit("tool-calls", { calls });
|
|
699
|
+
for (const call of calls) this.emit("tool-call", { call });
|
|
700
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
701
|
+
const resultParts = await this.#tasks.run(calls, await this.#toolContext(), { onResult: (call, result) => {
|
|
702
|
+
emitted.add(call.id);
|
|
703
|
+
this.emit("tool-result", {
|
|
704
|
+
call,
|
|
705
|
+
result
|
|
706
|
+
});
|
|
707
|
+
} });
|
|
708
|
+
for (let i = 0; i < calls.length; i++) {
|
|
709
|
+
const call = calls[i];
|
|
710
|
+
const part = resultParts[i];
|
|
711
|
+
if (emitted.has(call.id)) continue;
|
|
712
|
+
this.emit("tool-result", {
|
|
713
|
+
call,
|
|
714
|
+
result: {
|
|
715
|
+
content: part.content,
|
|
716
|
+
error: part.error,
|
|
717
|
+
isError: part.isError ?? false,
|
|
718
|
+
meta: part.meta
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
const message = {
|
|
723
|
+
content: resultParts,
|
|
724
|
+
role: "tool"
|
|
725
|
+
};
|
|
726
|
+
await this.session.add(message);
|
|
727
|
+
return message;
|
|
728
|
+
}
|
|
729
|
+
/** Resolve when the agent is in a quiescent state — `idle` or
|
|
730
|
+
* `paused`. If a run is currently in flight (or was just kicked off
|
|
731
|
+
* synchronously by `send` / `inject` / a wakeup), waits for it to
|
|
732
|
+
* settle before returning the final status. If no run is in flight,
|
|
733
|
+
* resolves immediately with the current status.
|
|
734
|
+
*
|
|
735
|
+
* Use this in REPL-style drivers to await the loop without racing
|
|
736
|
+
* with internally-triggered runs (the wakeup timer, heartbeat, or
|
|
737
|
+
* task-completion injects all spawn `void this.run()` independently
|
|
738
|
+
* of caller-driven `await agent.run()`). The `#running` promise is
|
|
739
|
+
* set synchronously inside `run()` before any await point, so this
|
|
740
|
+
* catches every kicked-off cycle including the one `send` just fired.
|
|
741
|
+
*
|
|
742
|
+
* Errors from the loop are swallowed — callers read `agent.lastError`
|
|
743
|
+
* / `agent.lastStopReason` for diagnostics. The point of `waitIdle`
|
|
744
|
+
* is just to know "the loop is no longer driving." */
|
|
745
|
+
async waitIdle(timeout) {
|
|
746
|
+
if (!this.#running) return this.#status;
|
|
747
|
+
const wait = [this.#running.catch(() => void 0)];
|
|
748
|
+
if (timeout !== void 0) wait.push(new Promise((resolve) => setTimeout(resolve, timeout)));
|
|
749
|
+
await Promise.race(wait);
|
|
750
|
+
return this.#status;
|
|
751
|
+
}
|
|
752
|
+
/** Stop the loop with a custom reason.
|
|
753
|
+
* When `abort: true` (the default), also trigger the abort signal so any
|
|
754
|
+
* in-flight stream or tool calls can react to the stop.
|
|
755
|
+
* When `abort: false`, the agent still stops immediately, but any active
|
|
756
|
+
* stream or tool calls are left to run to completion */
|
|
757
|
+
stop(opts = {}) {
|
|
758
|
+
this.#pauseRequested ??= opts.reason ?? "stop";
|
|
759
|
+
if (opts.abort ?? true) this.#abortController?.abort(opts.reason);
|
|
760
|
+
}
|
|
761
|
+
async start() {
|
|
762
|
+
if (this.#started) return;
|
|
763
|
+
this.#started = true;
|
|
764
|
+
await this.emit("start");
|
|
765
|
+
}
|
|
766
|
+
get started() {
|
|
767
|
+
return this.#started;
|
|
768
|
+
}
|
|
769
|
+
async #turn() {
|
|
770
|
+
for (let step = 1;; step++) {
|
|
771
|
+
if (this.#abortController?.signal.aborted) return {
|
|
772
|
+
kind: "aborted",
|
|
773
|
+
reason: this.#abortController.signal.reason
|
|
774
|
+
};
|
|
775
|
+
if (this.#pauseRequested !== void 0) return {
|
|
776
|
+
kind: "paused",
|
|
777
|
+
reason: this.#pauseRequested
|
|
778
|
+
};
|
|
779
|
+
this.emit("step-start", { step });
|
|
780
|
+
const outcome = await this.step();
|
|
781
|
+
this.emit("step-end", {
|
|
782
|
+
outcome: outcome.kind,
|
|
783
|
+
step
|
|
784
|
+
});
|
|
785
|
+
if (outcome.kind === "error") return {
|
|
786
|
+
kind: outcome.error?.name === "AbortError" ? "aborted" : "error",
|
|
787
|
+
reason: outcome.error
|
|
788
|
+
};
|
|
789
|
+
if (outcome.kind === "natural") return { kind: "natural" };
|
|
790
|
+
if (outcome.kind === "context-overflow") {
|
|
791
|
+
if (toValue(this.#opts.compaction)?.enabled === false) return { kind: "context-overflow" };
|
|
792
|
+
this.#opts.logger?.child("compaction").warn(`context overflow detected (size ${this.contextSize}, limit ${this.pressure.limit}); running compaction`);
|
|
793
|
+
await this.compact();
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
const stop = this.#stopPolicy.detect();
|
|
797
|
+
if (stop) return { kind: stop };
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
async #loop() {
|
|
801
|
+
if (!this.#started) await this.start();
|
|
802
|
+
this.#stopPolicy.reset();
|
|
803
|
+
for (let turn = 1;; turn++) {
|
|
804
|
+
this.emit("turn-start", { turn });
|
|
805
|
+
const result = await this.#turn();
|
|
806
|
+
const reason = result.reason ? toError(result.reason).message : void 0;
|
|
807
|
+
this.emit("turn-end", {
|
|
808
|
+
outcome: result.kind,
|
|
809
|
+
reason,
|
|
810
|
+
turn
|
|
811
|
+
});
|
|
812
|
+
if (result.kind === "natural") {
|
|
813
|
+
if (this.#appendQueue.length > 0) {
|
|
814
|
+
for (const m of this.#appendQueue.splice(0)) await this.session.add(m);
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
if (this.#injectQueue.length > 0) continue;
|
|
818
|
+
}
|
|
819
|
+
return this.#stop(result.kind, result.reason);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
#stop(kind, reason) {
|
|
823
|
+
this.#lastStop = {
|
|
824
|
+
error: reason instanceof Error ? reason : void 0,
|
|
825
|
+
kind,
|
|
826
|
+
reason: reason === void 0 ? void 0 : toError(reason).message
|
|
827
|
+
};
|
|
828
|
+
this.#setStatus(kind === "natural" ? "idle" : "paused");
|
|
829
|
+
this.emit("stop", {
|
|
830
|
+
status: this.#status,
|
|
831
|
+
usage: this.usage,
|
|
832
|
+
...this.#lastStop
|
|
833
|
+
});
|
|
834
|
+
return kind;
|
|
835
|
+
}
|
|
836
|
+
/** Build the per-step `ToolContext` handed to each tool. The session
|
|
837
|
+
* cwd, an abort signal scoped to the in-flight stream, and the
|
|
838
|
+
* long-running spawn registry are all surfaced here. */
|
|
839
|
+
async #toolContext() {
|
|
840
|
+
const masker = await this.#ctx.masker();
|
|
841
|
+
return {
|
|
842
|
+
agent: this,
|
|
843
|
+
bash: this.#opts.bash,
|
|
844
|
+
cwd: this.cwd,
|
|
845
|
+
isMasked: (msgId, partIdx) => masker?.isMasked(msgId, partIdx) ?? false,
|
|
846
|
+
messages: this.session.messages,
|
|
847
|
+
need: (scope, input) => this.#need(scope, input),
|
|
848
|
+
perms: await this.ctx.permissions(),
|
|
849
|
+
sessionDir: this.session.dir,
|
|
850
|
+
signal: this.#abortController?.signal,
|
|
851
|
+
swarm: () => this.#ctx.swarm(),
|
|
852
|
+
tasks: this.#tasks
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
/** Implementation of `ctx.need(scope, input)`. Resolves on `allow`,
|
|
856
|
+
* throws a `PERMISSION_DENIED` `AiError` on `deny`, and escalates
|
|
857
|
+
* `ask` to `AgentOptions.allow` (treating it as `deny` when no
|
|
858
|
+
* callback is configured). */
|
|
859
|
+
async #need(scope, input) {
|
|
860
|
+
const r = (await this.ctx.permissions()).validate(scope, input);
|
|
861
|
+
if (r.verdict === "allow") return;
|
|
862
|
+
if (r.verdict === "ask" && this.#opts.allow) {
|
|
863
|
+
if (await this.#opts.allow({
|
|
864
|
+
ask: r.ask,
|
|
865
|
+
input,
|
|
866
|
+
reason: r.reason,
|
|
867
|
+
scope,
|
|
868
|
+
suggestions: r.suggestions
|
|
869
|
+
})) return;
|
|
870
|
+
}
|
|
871
|
+
throw new AiError({
|
|
872
|
+
code: "PERMISSION_DENIED",
|
|
873
|
+
data: {
|
|
874
|
+
input,
|
|
875
|
+
scope,
|
|
876
|
+
suggestions: r.suggestions,
|
|
877
|
+
verdict: r.verdict
|
|
878
|
+
},
|
|
879
|
+
message: r.reason,
|
|
880
|
+
retryable: r.verdict === "ask"
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
/** Tear down session-scoped resources. Called on agent disposal — the
|
|
884
|
+
* agent doesn't auto-dispose today, so callers (TUI on quit, headless
|
|
885
|
+
* runner on completion) should invoke this explicitly. */
|
|
886
|
+
async dispose() {
|
|
887
|
+
this.clearListeners();
|
|
888
|
+
this.#cancelAllWakeups();
|
|
889
|
+
await this.#tasks.killAll();
|
|
890
|
+
}
|
|
891
|
+
#setStatus(status) {
|
|
892
|
+
const prev = this.#status;
|
|
893
|
+
if (prev === status) return;
|
|
894
|
+
if (status === "streaming" && (prev === "idle" || prev === "paused")) this.#cancelAllWakeups();
|
|
895
|
+
this.#status = status;
|
|
896
|
+
this.emit("status", { status });
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
//#endregion
|
|
900
|
+
export { Agent };
|
|
901
|
+
|
|
902
|
+
//# sourceMappingURL=agent-GDLdDc2w.mjs.map
|