@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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -0
  3. package/dist/agent-BaD2kax9.mjs +68 -0
  4. package/dist/agent-BaD2kax9.mjs.map +1 -0
  5. package/dist/agent-GDLdDc2w.mjs +902 -0
  6. package/dist/agent-GDLdDc2w.mjs.map +1 -0
  7. package/dist/bash-QNMTQ1PD.mjs +142 -0
  8. package/dist/bash-QNMTQ1PD.mjs.map +1 -0
  9. package/dist/compactions-3AjPqjeZ.mjs +190 -0
  10. package/dist/compactions-3AjPqjeZ.mjs.map +1 -0
  11. package/dist/edit-Qxl4w1C2.mjs +170 -0
  12. package/dist/edit-Qxl4w1C2.mjs.map +1 -0
  13. package/dist/env-UUFv5Mye.mjs +30 -0
  14. package/dist/env-UUFv5Mye.mjs.map +1 -0
  15. package/dist/fetch-D8cmmA3E.mjs +111 -0
  16. package/dist/fetch-D8cmmA3E.mjs.map +1 -0
  17. package/dist/find-Bah9SKWX.mjs +87 -0
  18. package/dist/find-Bah9SKWX.mjs.map +1 -0
  19. package/dist/grep-B2Ax7ewy.mjs +244 -0
  20. package/dist/grep-B2Ax7ewy.mjs.map +1 -0
  21. package/dist/index-node-esm-D6YQRKEz.mjs +1537 -0
  22. package/dist/index-node-esm-D6YQRKEz.mjs.map +1 -0
  23. package/dist/index.d.mts +1605 -0
  24. package/dist/index.d.mts.map +1 -0
  25. package/dist/index.mjs +948 -0
  26. package/dist/index.mjs.map +1 -0
  27. package/dist/jsonl-Dw_VBp46.mjs +134 -0
  28. package/dist/jsonl-Dw_VBp46.mjs.map +1 -0
  29. package/dist/manager-D-lWBTHG.mjs +271 -0
  30. package/dist/manager-D-lWBTHG.mjs.map +1 -0
  31. package/dist/markdown-Bv4J8cg3.mjs +53 -0
  32. package/dist/markdown-Bv4J8cg3.mjs.map +1 -0
  33. package/dist/masker-Bv9KjkDF.mjs +396 -0
  34. package/dist/masker-Bv9KjkDF.mjs.map +1 -0
  35. package/dist/model-_3ZA2xqm.mjs +27 -0
  36. package/dist/model-_3ZA2xqm.mjs.map +1 -0
  37. package/dist/notify-DpG_m9iP.mjs +96 -0
  38. package/dist/notify-DpG_m9iP.mjs.map +1 -0
  39. package/dist/read-Djsx2NZl.mjs +205 -0
  40. package/dist/read-Djsx2NZl.mjs.map +1 -0
  41. package/dist/registry-CFmR5G3J.mjs +564 -0
  42. package/dist/registry-CFmR5G3J.mjs.map +1 -0
  43. package/dist/search-CRw247ES.mjs +121 -0
  44. package/dist/search-CRw247ES.mjs.map +1 -0
  45. package/dist/session/claude.d.mts +59 -0
  46. package/dist/session/claude.d.mts.map +1 -0
  47. package/dist/session/claude.mjs +455 -0
  48. package/dist/session/claude.mjs.map +1 -0
  49. package/dist/session-CUUdWFC5.mjs +450 -0
  50. package/dist/session-CUUdWFC5.mjs.map +1 -0
  51. package/dist/session-Ds5nJz5A.d.mts +346 -0
  52. package/dist/session-Ds5nJz5A.d.mts.map +1 -0
  53. package/dist/session.d.mts +37 -0
  54. package/dist/session.d.mts.map +1 -0
  55. package/dist/session.mjs +106 -0
  56. package/dist/session.mjs.map +1 -0
  57. package/dist/subagent-baqzjRMc.mjs +136 -0
  58. package/dist/subagent-baqzjRMc.mjs.map +1 -0
  59. package/dist/swarm-BJ3gQA1Z.mjs +194 -0
  60. package/dist/swarm-BJ3gQA1Z.mjs.map +1 -0
  61. package/dist/swarm-BuFx_xeY.mjs +109 -0
  62. package/dist/swarm-BuFx_xeY.mjs.map +1 -0
  63. package/dist/tasks-D8rARHZ1.mjs +65 -0
  64. package/dist/tasks-D8rARHZ1.mjs.map +1 -0
  65. package/dist/tasks-WmC270X2.mjs +615 -0
  66. package/dist/tasks-WmC270X2.mjs.map +1 -0
  67. package/dist/tools-DW1dbcCz.mjs +695 -0
  68. package/dist/tools-DW1dbcCz.mjs.map +1 -0
  69. package/dist/truncate-BSMXgiXj.mjs +102 -0
  70. package/dist/truncate-BSMXgiXj.mjs.map +1 -0
  71. package/dist/uuid-ByXP5SO5.mjs +59 -0
  72. package/dist/uuid-ByXP5SO5.mjs.map +1 -0
  73. package/dist/wakeup-BYqTnfSp.mjs +29 -0
  74. package/dist/wakeup-BYqTnfSp.mjs.map +1 -0
  75. package/dist/write-D3L-92vj.mjs +89 -0
  76. package/dist/write-D3L-92vj.mjs.map +1 -0
  77. 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