@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,615 @@
1
+ import { r as uuidv7 } from "./uuid-ByXP5SO5.mjs";
2
+ import { Emitter, safeStringify } from "@zaly/shared";
3
+ import { AiError, isStreamable, runTool, stringifyContent, toErrorResult } from "@zaly/ai";
4
+ //#region src/tasks.ts
5
+ const DEFAULT_GRACE_MS = 1e4;
6
+ /**
7
+ * Tool registry + execution machinery + long-running task bookkeeping.
8
+ * One Tasks instance per Agent. Owns the tool list (the agent delegates
9
+ * `agent.tools` to it) and runs every assistant tool batch through
10
+ * `run()`, which:
11
+ *
12
+ * - validates each call's params,
13
+ * - dispatches to the named tool (sync results land immediately),
14
+ * - races `Streamable` returns against a grace window,
15
+ * - chains `parallel: false` calls behind their predecessors,
16
+ * - returns `ToolResultPart[]` 1:1 with the assistant's calls,
17
+ * surfacing partial results / placeholders for anything still running
18
+ * when the grace expires.
19
+ *
20
+ * Completions during a round are folded into the returned parts and do
21
+ * NOT fire `task-done`. Completions afterward fire normally — the agent
22
+ * listens and injects a system message into the next step.
23
+ */
24
+ var Tasks = class extends Emitter {
25
+ #map = /* @__PURE__ */ new Map();
26
+ graceMs = DEFAULT_GRACE_MS;
27
+ #tools = [];
28
+ /** Heartbeat interval in ms. When set, fires `heartbeat` events while
29
+ * at least one task is pending or running. Self-managing: starts on
30
+ * the first active task, stops when the last one finishes. Set to
31
+ * `undefined` to disable. */
32
+ #heartbeatMs;
33
+ #heartbeatTimer;
34
+ get heartbeatMs() {
35
+ return this.#heartbeatMs;
36
+ }
37
+ set heartbeatMs(value) {
38
+ this.#heartbeatMs = value;
39
+ this.#syncHeartbeat();
40
+ }
41
+ /** Snapshot of every active task (pending or running). */
42
+ running() {
43
+ return [...this.#map.values()].filter((t) => t.status !== "done").map((t) => toTaskInfo(t));
44
+ }
45
+ /** Snapshot of every task that has completed. Drops are by `remove()`
46
+ * or implicit retention; this list grows unless the caller prunes. */
47
+ finished() {
48
+ return [...this.#map.values()].filter((t) => t.status === "done").map((t) => toTaskInfo(t));
49
+ }
50
+ /** Look up a task by id. Useful for `task_stop` / `task_poll`. */
51
+ get(id) {
52
+ const t = this.#map.get(id);
53
+ return t ? toTaskInfo(t) : void 0;
54
+ }
55
+ /** Snapshot of every task in the registry — running, pending, and done. */
56
+ info() {
57
+ return [...this.#map.values()].map(toTaskInfo);
58
+ }
59
+ async tools() {
60
+ return typeof this.#tools === "function" ? this.#tools() : this.#tools;
61
+ }
62
+ set $tools(tools) {
63
+ this.#tools = tools;
64
+ }
65
+ /** Non-consuming check: does the underlying streamable have output
66
+ * the model hasn't seen since the last `poll()`? Returns false for
67
+ * tasks without a streamable, completed tasks, or streamables that
68
+ * don't implement the optional `hasNew` hook (no signal to give).
69
+ * Used by heartbeats to flag tasks worth polling. */
70
+ hasNewOutput(id) {
71
+ const t = this.#map.get(id);
72
+ if (t?.status !== "running" || !t.streamable) return false;
73
+ return t.streamable.hasNew?.() ?? false;
74
+ }
75
+ /** Poll a task's streamable for incremental output without changing
76
+ * its lifecycle state. Advances the streamable's cursor — the next
77
+ * call returns only what arrived since *this* one. Returns the same
78
+ * `ToolResult` shape the original tool produces (parts + meta).
79
+ * Errors when the task is unknown, already done, or has no streamable
80
+ * (the latter is the case for plain-Promise tools — there's no
81
+ * partial state to read). */
82
+ pollOutput(id) {
83
+ const t = this.#map.get(id);
84
+ if (!t) throw new AiError({
85
+ code: "NOT_FOUND",
86
+ data: { id },
87
+ message: `no task with id "${id}"`
88
+ });
89
+ if (t.status === "done") throw new AiError({
90
+ code: "TASK_DONE",
91
+ data: { id },
92
+ message: `task "${id}" has already completed; its result was injected as a system message`
93
+ });
94
+ if (!t.streamable) throw new AiError({
95
+ code: "NOT_STREAMABLE",
96
+ data: { id },
97
+ message: `task "${id}" has no incremental output — it's a plain-Promise tool whose result is delivered all-at-once when complete`
98
+ });
99
+ return t.streamable.poll();
100
+ }
101
+ /** Drop a task from the registry — does not abort. Caller is
102
+ * responsible for `abort()`-ing if the task is still running. */
103
+ remove(id) {
104
+ const task = this.#map.get(id);
105
+ if (!task) return false;
106
+ this.#map.delete(id);
107
+ this.emit("task-removed", { task: toTaskInfo(task) });
108
+ return true;
109
+ }
110
+ /** Mark a task done with a final result. Fires `task-done` UNLESS the
111
+ * task is currently owned by an in-flight `run()` round — in that
112
+ * case the round folds the completion into its returned parts and
113
+ * the event is suppressed. Starts any pending dependents. */
114
+ done(id, result) {
115
+ const task = this.#map.get(id);
116
+ if (!task || task.status === "done") return;
117
+ task.status = "done";
118
+ task.result = result;
119
+ task.finishedAt = Date.now();
120
+ task.resolveDone();
121
+ if (!task.ownerRound) this.emit("task-done", { task: toTaskInfo(task) });
122
+ this.#startReadyDependents(id, result);
123
+ this.#syncHeartbeat();
124
+ }
125
+ /** Abort a running task. Calls the streamable's `abort()` if present;
126
+ * pending tasks transition straight to `done` with a cancelled
127
+ * result. Idempotent. */
128
+ abort(id, reason = "aborted by request") {
129
+ const task = this.#map.get(id);
130
+ if (!task || task.status === "done") return;
131
+ if (task.streamable) task.streamable.abort();
132
+ if (task.status === "pending") this.done(id, toErrorResult(new AiError({
133
+ code: "TASK_ABORTED",
134
+ message: reason
135
+ })));
136
+ }
137
+ /** Abort every active task. Used on agent dispose. Awaits each
138
+ * task's `donePromise` so the registry is fully drained before
139
+ * resolving — so callers can rely on no pending I/O after `dispose`. */
140
+ async killAll() {
141
+ const active = [...this.#map.values()].filter((t) => t.status !== "done");
142
+ for (const t of active) {
143
+ if (t.streamable) t.streamable.abort();
144
+ if (t.status === "pending") this.done(t.id, toErrorResult(new AiError({
145
+ code: "TASK_ABORTED",
146
+ message: "agent disposed"
147
+ })));
148
+ }
149
+ await Promise.all(active.map((t) => t.donePromise.catch(() => void 0)));
150
+ this.#syncHeartbeat();
151
+ }
152
+ /** Start or stop the heartbeat timer based on whether any task is
153
+ * active and whether `heartbeatMs` is configured. Idempotent — called
154
+ * after every state transition (`add` / `done` / `killAll`). The
155
+ * timer is `unref`'d so it never holds the process open. */
156
+ #syncHeartbeat() {
157
+ const want = [...this.#map.values()].some((t) => t.status !== "done") && this.#heartbeatMs !== void 0;
158
+ if (want && !this.#heartbeatTimer) {
159
+ this.#heartbeatTimer = setInterval(() => {
160
+ this.emit("heartbeat", { running: this.info().filter((t) => t.status !== "done") });
161
+ }, this.#heartbeatMs);
162
+ this.#heartbeatTimer.unref();
163
+ } else if (!want && this.#heartbeatTimer) {
164
+ clearInterval(this.#heartbeatTimer);
165
+ this.#heartbeatTimer = void 0;
166
+ }
167
+ }
168
+ /**
169
+ * Execute a batch of assistant tool calls. Returns ToolResultPart[]
170
+ * mapped 1:1 with `calls`. Sync completions land in the result array
171
+ * immediately; long-running ones return a placeholder and the eventual
172
+ * completion fires `task-done` post-round (the agent injects a system
173
+ * message in response).
174
+ *
175
+ * The `messages` snapshot is what tools see on `ctx.messages` — pass
176
+ * the conversation up to (and including) the assistant message that
177
+ * produced these calls, so freshness checks can scan back through
178
+ * prior tool results.
179
+ */
180
+ async run(calls, ctx, opts = {}) {
181
+ const round = Symbol("round");
182
+ const tasks = [];
183
+ let chainHead;
184
+ const tools = await this.tools();
185
+ for (const call of calls) {
186
+ const tool = tools.find((t) => t.name === call.name);
187
+ if (!tool) {
188
+ tasks.push(this.#startSyncResult(call, unknownToolResult(call.name), round));
189
+ continue;
190
+ }
191
+ const parallel = tool.parallel ?? false;
192
+ if (chainHead && !parallel) {
193
+ tasks.push(this.#registerPending({
194
+ call,
195
+ ctxBase: ctx,
196
+ round,
197
+ tool,
198
+ waitingFor: chainHead
199
+ }));
200
+ chainHead = tasks[tasks.length - 1].id;
201
+ continue;
202
+ }
203
+ if (this.#needsPreflight(tool, ctx)) {
204
+ const preflight = await this.#preflight(tool, call, ctx);
205
+ if (preflight) {
206
+ tasks.push(this.#startSyncResult(call, preflight, round));
207
+ continue;
208
+ }
209
+ }
210
+ const task = this.#start({
211
+ call,
212
+ ctxBase: ctx,
213
+ round,
214
+ tool
215
+ });
216
+ tasks.push(task);
217
+ if (task.status === "running" && !parallel) chainHead = task.id;
218
+ }
219
+ const parts = [];
220
+ const resultPart = (idx) => {
221
+ if (parts[idx]) return parts[idx];
222
+ const task = tasks[idx];
223
+ if (task.status !== "done") task.ownerRound = void 0;
224
+ const call = calls[idx];
225
+ const part = this.#buildPart(call, task);
226
+ parts[idx] = part;
227
+ return part;
228
+ };
229
+ let onResult = opts.onResult;
230
+ const allDone = Promise.all(tasks.map(async (t, ti) => {
231
+ await t.donePromise;
232
+ onResult?.(calls[ti], resultPart(ti), ti);
233
+ }));
234
+ const grace = sleep(this.graceMs);
235
+ const aborted = ctx.signal ? new Promise((resolve) => {
236
+ if (ctx.signal.aborted) resolve();
237
+ else ctx.signal.addEventListener("abort", () => resolve(), { once: true });
238
+ }) : new Promise(() => void 0);
239
+ await Promise.race([
240
+ allDone,
241
+ grace,
242
+ aborted
243
+ ]);
244
+ onResult = void 0;
245
+ return tasks.map((_, i) => resultPart(i));
246
+ }
247
+ /** Build a "done" task synchronously from a precomputed result.
248
+ * Used for unknown-tool errors and validation failures. */
249
+ #startSyncResult(call, result, round) {
250
+ const task = this.#allocate({
251
+ id: uuidv7(),
252
+ type: call.name,
253
+ desc: call.name,
254
+ round
255
+ });
256
+ task.status = "done";
257
+ task.result = result;
258
+ task.finishedAt = task.startedAt;
259
+ task.resolveDone();
260
+ return task;
261
+ }
262
+ /** Allocate an internal task record, register it, and emit `task-added`. */
263
+ #allocate(opts) {
264
+ let resolveDone;
265
+ const donePromise = new Promise((r) => {
266
+ resolveDone = r;
267
+ });
268
+ const task = {
269
+ desc: opts.desc,
270
+ donePromise,
271
+ id: opts.id,
272
+ ownerRound: opts.round,
273
+ resolveDone,
274
+ startedAt: Date.now(),
275
+ status: "pending",
276
+ type: opts.type,
277
+ waitingFor: opts.waitingFor
278
+ };
279
+ this.#map.set(task.id, task);
280
+ this.emit("task-added", { task: toTaskInfo(task) });
281
+ this.#syncHeartbeat();
282
+ return task;
283
+ }
284
+ /** Register a pending task (chained behind `dependsOn`). Does not
285
+ * invoke the tool — that happens once the dependency completes. */
286
+ #registerPending(opts) {
287
+ const task = this.#allocate({
288
+ id: uuidv7(),
289
+ type: opts.tool.name,
290
+ desc: descOfCall(opts.call),
291
+ waitingFor: opts.waitingFor,
292
+ round: opts.round
293
+ });
294
+ task.pending = {
295
+ call: opts.call,
296
+ ctx: opts.ctxBase,
297
+ tool: opts.tool
298
+ };
299
+ return task;
300
+ }
301
+ /** Start a tool call. Allocates a task record, validates params,
302
+ * invokes `tool.call`, detects `Streamable`, and wires completion
303
+ * back to `done()`. Returns the task in its post-launch state
304
+ * (`done` for sync failures or already-resolved sync calls,
305
+ * `running` for everything else). */
306
+ #start(opts) {
307
+ const task = this.#allocate({
308
+ id: uuidv7(),
309
+ type: opts.tool.name,
310
+ desc: descOfCall(opts.call),
311
+ round: opts.round
312
+ });
313
+ this.#dispatch(task, opts.tool, opts.call, opts.ctxBase);
314
+ return task;
315
+ }
316
+ #needsPreflight(tool, ctx) {
317
+ return ctx.need !== void 0 || tool.preflight !== void 0;
318
+ }
319
+ /** Run schema validation and semantic/tool permission checks before a
320
+ * public task is allocated. Returns a formatted tool error when the
321
+ * call should short-circuit, otherwise undefined. */
322
+ async #preflight(tool, call, ctxBase) {
323
+ const ctx = {
324
+ ...ctxBase,
325
+ tasks: this
326
+ };
327
+ let params;
328
+ try {
329
+ params = await tool.validator.validateParams(call.params);
330
+ await ctx.need?.("tool", tool.name);
331
+ await tool.preflight?.(params, ctx);
332
+ } catch (error) {
333
+ return toErrorResult(error);
334
+ }
335
+ }
336
+ /** Validate / call / wire up completion for an existing task record.
337
+ * Used both for fresh tasks (`#start`) and pending-becoming-ready
338
+ * (`#startReadyDependents`). The task's id and registry entry are
339
+ * reused so identifiers stay stable across the pending → running
340
+ * transition.
341
+ *
342
+ * Delegates the validate/call/error machinery to `runTool` (in
343
+ * streaming mode) so there's one source of truth. The task's status
344
+ * is flipped to `running` synchronously — the round loop reads it
345
+ * right after `#start` returns to decide chain heads, and we want
346
+ * that read to see the post-launch state, not the allocation default.
347
+ * Async resolution either keeps it `running` (streamable returned)
348
+ * or transitions to `done` (sync result or error). */
349
+ #dispatch(task, tool, call, ctxBase) {
350
+ const ctx = {
351
+ ...ctxBase,
352
+ tasks: this
353
+ };
354
+ task.status = "running";
355
+ runTool(tool, call.params, ctx, {
356
+ preflight: false,
357
+ streaming: true
358
+ }).then((settled) => {
359
+ if (isStreamable(settled)) {
360
+ task.streamable = settled;
361
+ settled.done.then(() => {
362
+ const snap = settled.poll();
363
+ this.done(task.id, {
364
+ content: snap.content,
365
+ error: snap.error,
366
+ isError: snap.isError,
367
+ meta: snap.meta
368
+ });
369
+ }, (error) => this.done(task.id, toErrorResult(error)));
370
+ return;
371
+ }
372
+ this.done(task.id, settled);
373
+ }, (error) => this.done(task.id, toErrorResult(error)));
374
+ }
375
+ /** Walk pending tasks; start any whose `dependsOn` just completed.
376
+ * If the dependency failed, propagate cancellation: dependents land
377
+ * in `done` with an `UPSTREAM_FAILED` result. */
378
+ #startReadyDependents(completedId, completedResult) {
379
+ for (const t of this.#map.values()) {
380
+ if (t.status !== "pending" || t.waitingFor !== completedId) continue;
381
+ if (completedResult.isError) {
382
+ this.done(t.id, toErrorResult(new AiError({
383
+ code: "UPSTREAM_FAILED",
384
+ data: { dependsOn: completedId },
385
+ message: `skipped: upstream task ${completedId} failed`
386
+ })));
387
+ continue;
388
+ }
389
+ this.#startPending(t);
390
+ }
391
+ }
392
+ async #startPending(task) {
393
+ const pending = task.pending;
394
+ if (!pending) return;
395
+ if (!this.#needsPreflight(pending.tool, pending.ctx)) {
396
+ task.pending = void 0;
397
+ task.waitingFor = void 0;
398
+ task.ownerRound = void 0;
399
+ this.#dispatch(task, pending.tool, pending.call, pending.ctx);
400
+ return;
401
+ }
402
+ const preflight = await this.#preflight(pending.tool, pending.call, pending.ctx);
403
+ if (preflight) {
404
+ this.done(task.id, preflight);
405
+ return;
406
+ }
407
+ task.pending = void 0;
408
+ task.waitingFor = void 0;
409
+ task.ownerRound = void 0;
410
+ this.#dispatch(task, pending.tool, pending.call, pending.ctx);
411
+ }
412
+ /** Build a `ToolResultPart` for a task at round-end. Done tasks pass
413
+ * through their captured result; still-running tasks get a snapshot
414
+ * from the streamable (if any) plus a placeholder note for the model. */
415
+ #buildPart(call, task) {
416
+ if (task.status === "done" && task.result) return {
417
+ content: task.result.content,
418
+ error: task.result.error,
419
+ id: call.id,
420
+ isError: task.result.isError,
421
+ meta: stampTaskMeta(task.result.meta, task),
422
+ name: call.name,
423
+ type: "tool-result"
424
+ };
425
+ if (task.status === "pending") return {
426
+ content: [{
427
+ data: toTaskInfo(task),
428
+ tag: "task",
429
+ type: "meta"
430
+ }],
431
+ id: call.id,
432
+ isError: false,
433
+ meta: stampTaskMeta(void 0, task),
434
+ name: call.name,
435
+ type: "tool-result"
436
+ };
437
+ const snap = task.streamable?.poll();
438
+ const baseContent = partialContentFrom(snap);
439
+ const trailer = {
440
+ content: "Task is still running. Partial output above. The final result will arrive as a system message when the task completes; you do not need to poll. Continue with other work or wait.",
441
+ data: { ...toTaskInfo(task) },
442
+ tag: "task",
443
+ type: "meta"
444
+ };
445
+ const parts = [];
446
+ if (typeof baseContent === "string") {
447
+ if (baseContent !== "") parts.push({
448
+ text: baseContent,
449
+ type: "text"
450
+ });
451
+ } else parts.push(...baseContent);
452
+ parts.push(trailer);
453
+ return {
454
+ content: parts,
455
+ id: call.id,
456
+ isError: false,
457
+ meta: stampTaskMeta(snap?.meta, task),
458
+ name: call.name,
459
+ type: "tool-result"
460
+ };
461
+ }
462
+ };
463
+ /** Stamp `meta.task` with the current task state. Preserves any
464
+ * tool-attached `meta` fields. Used both on done results (so the
465
+ * model can correlate completion to the original call) and on
466
+ * partial / pending placeholders. */
467
+ function stampTaskMeta(base, task) {
468
+ return {
469
+ ...base,
470
+ task: {
471
+ dependsOn: task.waitingFor,
472
+ desc: task.desc,
473
+ durationMs: task.finishedAt ? task.finishedAt - task.startedAt : void 0,
474
+ id: task.id,
475
+ status: task.status,
476
+ type: task.type
477
+ }
478
+ };
479
+ }
480
+ /** Build the "running" placeholder content from a streamable snapshot.
481
+ * Empty string when there's nothing yet to show; copies the parts
482
+ * array so downstream mutation of the result message can't reach back
483
+ * into the streamable's accumulator. */
484
+ function partialContentFrom(snap) {
485
+ if (!snap) return "";
486
+ if (typeof snap.content === "string") return snap.content;
487
+ return [...snap.content];
488
+ }
489
+ /** Best-effort human-readable label for a tool call. Reads a
490
+ * `description` field from params if the tool defined one (bash does);
491
+ * otherwise falls back to the tool name. Kept short so log lines stay
492
+ * scannable. */
493
+ function descOfCall(call) {
494
+ const params = call.params;
495
+ if (params && typeof params === "object" && typeof params.description === "string") return params.description;
496
+ return call.name;
497
+ }
498
+ /** Synthesize the tool-result payload returned when the model calls a
499
+ * tool that wasn't registered for this turn. Same shape the previous
500
+ * `unknownToolResult` helper produced; inlined here to keep `tasks.ts`
501
+ * self-contained. */
502
+ function unknownToolResult(name) {
503
+ return toErrorResult(new AiError({
504
+ code: "UNKNOWN_TOOL",
505
+ message: `no tool named "${name}" is registered for this turn`
506
+ }));
507
+ }
508
+ function sleep(ms) {
509
+ return new Promise((r) => {
510
+ setTimeout(r, ms).unref();
511
+ });
512
+ }
513
+ /** Render an array of `TaskInfo` as a `<tasks>` MetaPart for the model
514
+ * (heartbeat pulses and `task_list`). Each task lands as one line of
515
+ * JSON. The `result` field on done tasks is stripped — listing should
516
+ * give an inventory, not historical output (the result was already
517
+ * injected as a system message at task-done time, and re-shipping it
518
+ * on every heartbeat / list call would be a token bomb). */
519
+ function taskInfoPart(info) {
520
+ if (info.length === 0) return {
521
+ data: "no active tasks",
522
+ tag: "tasks",
523
+ type: "meta"
524
+ };
525
+ return {
526
+ data: info.map((t) => safeStringify(t, omitResult)).join("\n"),
527
+ tag: "tasks",
528
+ type: "meta"
529
+ };
530
+ }
531
+ /** JSON.stringify replacer that drops the `result` field. Used by
532
+ * `taskInfoPart` to keep listing payloads bounded. */
533
+ function omitResult(key, value) {
534
+ return key === "result" ? void 0 : value;
535
+ }
536
+ function toTaskInfo(task) {
537
+ const { id, status, type, desc } = task;
538
+ const ms = (task.finishedAt ?? Date.now()) - task.startedAt;
539
+ if (status === "running") return {
540
+ desc,
541
+ elapsedMs: ms,
542
+ hasNewOutput: task.streamable?.hasNew?.(),
543
+ id,
544
+ status,
545
+ type
546
+ };
547
+ if (status === "pending") return {
548
+ desc,
549
+ elapsedMs: ms,
550
+ id,
551
+ status,
552
+ type,
553
+ waitingFor: task.waitingFor ?? ""
554
+ };
555
+ return {
556
+ desc,
557
+ durationMs: ms,
558
+ id,
559
+ result: task.result ?? toErrorResult(new AiError({
560
+ code: "INTERNAL",
561
+ message: `task "${task.id}" completed without a result`
562
+ })),
563
+ status,
564
+ type
565
+ };
566
+ }
567
+ /** Format a finished task as the parts of a system inject. Layout:
568
+ *
569
+ * <task>{id, type, desc, status: "done", durationMs}</task>
570
+ * {result body}
571
+ *
572
+ * The header is the standard `TaskInfo`-shaped JSON so consumers
573
+ * (model, TUI) can parse it the same way they parse `task_list` and
574
+ * heartbeat output — same shape everywhere a task surfaces.
575
+ *
576
+ * The body comes straight from `result.content`. For errors, that
577
+ * already includes the `<error>{code, message, ...}</error>` MetaPart
578
+ * + the formatted `❌ CODE: message` block (baked in by
579
+ * `toErrorResult`), so this function doesn't need to special-case
580
+ * errors — the structured tag and human body ride along with the
581
+ * result content for any tool failure, anywhere.
582
+ *
583
+ * System messages can't carry attachments, so any image/pdf/etc. in
584
+ * the result degrades to a `[image]` placeholder via
585
+ * `stringifyContent` — best-effort, model still sees *something* was
586
+ * there. */
587
+ function formatTaskCompletion(task) {
588
+ const { result, ...header } = task;
589
+ const parts = [{
590
+ data: header,
591
+ tag: "task",
592
+ type: "meta"
593
+ }];
594
+ const bodyText = stringifyContent(result.content);
595
+ if (bodyText !== "") parts.push({
596
+ text: bodyText,
597
+ type: "text"
598
+ });
599
+ return parts;
600
+ }
601
+ /** Build an `inject`-ready system message for a finished task. */
602
+ function taskCompletionMessage(task) {
603
+ return {
604
+ content: formatTaskCompletion(task),
605
+ meta: {
606
+ kind: "task",
607
+ taskId: task.id
608
+ },
609
+ role: "system"
610
+ };
611
+ }
612
+ //#endregion
613
+ export { taskCompletionMessage as n, taskInfoPart as r, Tasks as t };
614
+
615
+ //# sourceMappingURL=tasks-WmC270X2.mjs.map