@pi-unipi/subagents 0.2.2 → 0.2.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 (63) hide show
  1. package/package.json +1 -1
  2. package/src/__tests__/badge-generation.test.ts +244 -0
  3. package/src/index.ts +28 -7
  4. package/dist/__tests__/config.test.d.ts +0 -11
  5. package/dist/__tests__/config.test.d.ts.map +0 -1
  6. package/dist/__tests__/config.test.js +0 -196
  7. package/dist/__tests__/config.test.js.map +0 -1
  8. package/dist/__tests__/esc-propagation.test.d.ts +0 -10
  9. package/dist/__tests__/esc-propagation.test.d.ts.map +0 -1
  10. package/dist/__tests__/esc-propagation.test.js +0 -140
  11. package/dist/__tests__/esc-propagation.test.js.map +0 -1
  12. package/dist/__tests__/file-lock.test.d.ts +0 -12
  13. package/dist/__tests__/file-lock.test.d.ts.map +0 -1
  14. package/dist/__tests__/file-lock.test.js +0 -187
  15. package/dist/__tests__/file-lock.test.js.map +0 -1
  16. package/dist/__tests__/workflow-integration.test.d.ts +0 -12
  17. package/dist/__tests__/workflow-integration.test.d.ts.map +0 -1
  18. package/dist/__tests__/workflow-integration.test.js +0 -261
  19. package/dist/__tests__/workflow-integration.test.js.map +0 -1
  20. package/dist/agent-manager.d.ts +0 -75
  21. package/dist/agent-manager.d.ts.map +0 -1
  22. package/dist/agent-manager.js +0 -268
  23. package/dist/agent-manager.js.map +0 -1
  24. package/dist/agent-runner.d.ts +0 -51
  25. package/dist/agent-runner.d.ts.map +0 -1
  26. package/dist/agent-runner.js +0 -254
  27. package/dist/agent-runner.js.map +0 -1
  28. package/dist/config.d.ts +0 -24
  29. package/dist/config.d.ts.map +0 -1
  30. package/dist/config.js +0 -132
  31. package/dist/config.js.map +0 -1
  32. package/dist/conversation-viewer.d.ts +0 -40
  33. package/dist/conversation-viewer.d.ts.map +0 -1
  34. package/dist/conversation-viewer.js +0 -276
  35. package/dist/conversation-viewer.js.map +0 -1
  36. package/dist/custom-agents.d.ts +0 -14
  37. package/dist/custom-agents.d.ts.map +0 -1
  38. package/dist/custom-agents.js +0 -106
  39. package/dist/custom-agents.js.map +0 -1
  40. package/dist/file-lock.d.ts +0 -42
  41. package/dist/file-lock.d.ts.map +0 -1
  42. package/dist/file-lock.js +0 -91
  43. package/dist/file-lock.js.map +0 -1
  44. package/dist/index.d.ts +0 -10
  45. package/dist/index.d.ts.map +0 -1
  46. package/dist/index.js +0 -653
  47. package/dist/index.js.map +0 -1
  48. package/dist/model-resolver.d.ts +0 -19
  49. package/dist/model-resolver.d.ts.map +0 -1
  50. package/dist/model-resolver.js +0 -61
  51. package/dist/model-resolver.js.map +0 -1
  52. package/dist/prompts.d.ts +0 -13
  53. package/dist/prompts.d.ts.map +0 -1
  54. package/dist/prompts.js +0 -31
  55. package/dist/prompts.js.map +0 -1
  56. package/dist/types.d.ts +0 -96
  57. package/dist/types.d.ts.map +0 -1
  58. package/dist/types.js +0 -36
  59. package/dist/types.js.map +0 -1
  60. package/dist/widget.d.ts +0 -55
  61. package/dist/widget.d.ts.map +0 -1
  62. package/dist/widget.js +0 -404
  63. package/dist/widget.js.map +0 -1
package/dist/index.js DELETED
@@ -1,653 +0,0 @@
1
- /**
2
- * @pi-unipi/subagents — Extension entry
3
- *
4
- * Tools: spawn_helper, get_helper_result
5
- * Features: renderCall/renderResult, message renderer, conversation viewer
6
- * ESC propagation: all children abort on parent ESC
7
- */
8
- import { defineTool } from "@mariozechner/pi-coding-agent";
9
- import { Text } from "@mariozechner/pi-tui";
10
- import { Type } from "@sinclair/typebox";
11
- import { existsSync, readdirSync } from "node:fs";
12
- import { join } from "node:path";
13
- import { homedir } from "node:os";
14
- import { emitEvent, MODULES, UNIPI_EVENTS } from "@pi-unipi/core";
15
- import { AgentManager } from "./agent-manager.js";
16
- import { initConfig } from "./config.js";
17
- import { BUILTIN_TYPES } from "./types.js";
18
- import { ConversationViewer } from "./conversation-viewer.js";
19
- import { AgentWidget } from "./widget.js";
20
- /** Get info registry from global */
21
- function getInfoRegistry() {
22
- const g = globalThis;
23
- return g.__unipi_info_registry;
24
- }
25
- // ---- Formatting helpers (shared between renderers and inline text) ----
26
- const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
27
- /** Tool name → human-readable action. */
28
- const TOOL_DISPLAY = {
29
- read: "reading",
30
- bash: "running command",
31
- edit: "editing",
32
- write: "writing",
33
- grep: "searching",
34
- find: "finding files",
35
- ls: "listing",
36
- };
37
- function formatTokens(count) {
38
- if (count >= 1_000_000)
39
- return `${(count / 1_000_000).toFixed(1)}M token`;
40
- if (count >= 1_000)
41
- return `${(count / 1_000).toFixed(1)}k token`;
42
- return `${count} token`;
43
- }
44
- function formatTurns(turn, max) {
45
- return max != null ? `⟳${turn}≤${max}` : `⟳${turn}`;
46
- }
47
- function formatMs(ms) {
48
- if (ms >= 60_000)
49
- return `${(ms / 60_000).toFixed(1)}m`;
50
- if (ms >= 1_000)
51
- return `${(ms / 1_000).toFixed(1)}s`;
52
- return `${ms}ms`;
53
- }
54
- /** Build activity description from active tools. */
55
- function describeActivity(activeTools, responseText) {
56
- if (activeTools.size > 0) {
57
- const groups = new Map();
58
- for (const toolName of activeTools.values()) {
59
- const action = TOOL_DISPLAY[toolName] ?? toolName;
60
- groups.set(action, (groups.get(action) ?? 0) + 1);
61
- }
62
- const parts = [];
63
- for (const [action, count] of groups) {
64
- if (count > 1) {
65
- parts.push(`${action} ${count} ${action === "searching" ? "patterns" : "files"}`);
66
- }
67
- else {
68
- parts.push(action);
69
- }
70
- }
71
- return parts.join(", ") + "…";
72
- }
73
- if (responseText && responseText.trim().length > 0) {
74
- const line = responseText.split("\n").find((l) => l.trim())?.trim() ?? "";
75
- if (line.length > 60)
76
- return line.slice(0, 60) + "…";
77
- if (line.length > 0)
78
- return line;
79
- }
80
- return "thinking…";
81
- }
82
- /** Format tokens safely from session. */
83
- function safeFormatTokens(session) {
84
- if (!session)
85
- return "";
86
- try {
87
- const stats = session.getSessionStats();
88
- const total = stats.tokens?.total ?? 0;
89
- return formatTokens(total);
90
- }
91
- catch {
92
- return "";
93
- }
94
- }
95
- /** Get raw token count from session. */
96
- function safeTokenCount(session) {
97
- if (!session)
98
- return 0;
99
- try {
100
- return session.getSessionStats().tokens?.total ?? 0;
101
- }
102
- catch {
103
- return 0;
104
- }
105
- }
106
- /** Build result text */
107
- function textResult(msg, details) {
108
- return { content: [{ type: "text", text: msg }], details };
109
- }
110
- /** Escape XML for structured notifications. */
111
- function escapeXml(s) {
112
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
113
- }
114
- /** Human-readable status label. */
115
- function getStatusLabel(status, error) {
116
- switch (status) {
117
- case "error": return `Error: ${error ?? "unknown"}`;
118
- case "aborted": return "Aborted (max turns exceeded)";
119
- case "stopped": return "Stopped";
120
- default: return "Done";
121
- }
122
- }
123
- export default function (pi) {
124
- // Initialize config
125
- const config = initConfig(process.cwd());
126
- if (!config.enabled)
127
- return;
128
- // Compute paths at factory time
129
- const homeDir = homedir();
130
- const cwd = process.cwd();
131
- const globalAgentsDir = join(homeDir, ".unipi", "config", "agents");
132
- const workspaceAgentsDir = join(cwd, ".unipi", "config", "agents");
133
- // Activity tracking for widget
134
- const agentActivity = new Map();
135
- // Create manager with completion callback
136
- const manager = new AgentManager((record) => {
137
- agentActivity.delete(record.id);
138
- widget.markFinished(record.id);
139
- widget.update();
140
- // Build notification details
141
- const details = buildNotificationDetails(record, agentActivity.get(record.id));
142
- // Send styled notification via message renderer
143
- const status = getStatusLabel(record.status, record.error);
144
- const durationMs = record.completedAt ? record.completedAt - record.startedAt : 0;
145
- const resultPreview = record.result
146
- ? record.result.length > 500
147
- ? record.result.slice(0, 500) + "…"
148
- : record.result
149
- : "No output.";
150
- const notificationXml = [
151
- `<task-notification>`,
152
- `<task-id>${record.id}</task-id>`,
153
- `<status>${escapeXml(status)}</status>`,
154
- `<summary>Agent "${escapeXml(record.description)}" ${record.status}</summary>`,
155
- `<result>${escapeXml(resultPreview)}</result>`,
156
- `<usage><total_tokens>${details.totalTokens}</total_tokens><tool_uses>${record.toolUses}</tool_uses><duration_ms>${durationMs}</duration_ms></usage>`,
157
- `</task-notification>`,
158
- ].join("\n");
159
- if (!record.resultConsumed) {
160
- pi.sendMessage({
161
- customType: "subagent-notification",
162
- content: notificationXml,
163
- display: true,
164
- details,
165
- }, { deliverAs: "followUp", triggerTurn: true });
166
- }
167
- pi.events.emit("subagents:completed", {
168
- id: record.id,
169
- type: record.type,
170
- description: record.description,
171
- status: record.status,
172
- result: record.result,
173
- error: record.error,
174
- });
175
- }, config.maxConcurrent, (record) => {
176
- pi.events.emit("subagents:started", {
177
- id: record.id,
178
- type: record.type,
179
- description: record.description,
180
- });
181
- });
182
- // Build notification details for the message renderer
183
- function buildNotificationDetails(record, activity) {
184
- return {
185
- id: record.id,
186
- description: record.description,
187
- status: record.status,
188
- toolUses: record.toolUses,
189
- turnCount: activity?.turnCount ?? 0,
190
- maxTurns: activity?.maxTurns,
191
- totalTokens: safeTokenCount(record.session),
192
- durationMs: record.completedAt ? record.completedAt - record.startedAt : 0,
193
- error: record.error,
194
- resultPreview: record.result
195
- ? record.result.length > 200
196
- ? record.result.slice(0, 200) + "…"
197
- : record.result
198
- : "No output.",
199
- };
200
- }
201
- // ---- Register custom notification renderer ----
202
- pi.registerMessageRenderer("subagent-notification", (message, { expanded }, theme) => {
203
- const d = message.details;
204
- if (!d)
205
- return undefined;
206
- function renderOne(d) {
207
- const isError = d.status === "error" || d.status === "stopped" || d.status === "aborted";
208
- const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
209
- const statusText = isError
210
- ? d.status
211
- : d.status === "steered"
212
- ? "completed (steered)"
213
- : "completed";
214
- // Line 1: icon + agent description + status
215
- let line = `${icon} ${theme.bold(d.description)} ${theme.fg("dim", statusText)}`;
216
- // Line 2: stats
217
- const parts = [];
218
- if (d.turnCount > 0)
219
- parts.push(formatTurns(d.turnCount, d.maxTurns));
220
- if (d.toolUses > 0)
221
- parts.push(`${d.toolUses} tool use${d.toolUses === 1 ? "" : "s"}`);
222
- if (d.totalTokens > 0)
223
- parts.push(formatTokens(d.totalTokens));
224
- if (d.durationMs > 0)
225
- parts.push(formatMs(d.durationMs));
226
- if (parts.length) {
227
- line += "\n " + parts.map((p) => theme.fg("dim", p)).join(" " + theme.fg("dim", "·") + " ");
228
- }
229
- // Line 3: result preview (collapsed) or full (expanded)
230
- if (expanded) {
231
- const lines = d.resultPreview.split("\n").slice(0, 30);
232
- for (const l of lines)
233
- line += "\n" + theme.fg("dim", ` ${l}`);
234
- }
235
- else {
236
- const preview = d.resultPreview.split("\n")[0]?.slice(0, 80) ?? "";
237
- line += "\n " + theme.fg("dim", `⎿ ${preview}`);
238
- }
239
- return line;
240
- }
241
- const all = [d, ...(d.others ?? [])];
242
- return new Text(all.map(renderOne).join("\n"), 0, 0);
243
- });
244
- // Create widget
245
- const widget = new AgentWidget(manager, agentActivity);
246
- // Register info group at factory time (not session_start)
247
- const registry = getInfoRegistry();
248
- if (registry) {
249
- registry.registerGroup({
250
- id: "subagents",
251
- name: "Subagents",
252
- icon: "🤖",
253
- priority: 80,
254
- config: {
255
- showByDefault: true,
256
- stats: [
257
- { id: "maxConcurrent", label: "Max Concurrent", show: true },
258
- { id: "activeCount", label: "Active Agents", show: true },
259
- { id: "enabled", label: "Enabled", show: true },
260
- { id: "types", label: "Available Types", show: true },
261
- ],
262
- },
263
- dataProvider: async () => {
264
- const types = config.types || {};
265
- const builtinTypes = ["explore", "work"];
266
- const customTypes = [];
267
- for (const dir of [globalAgentsDir, workspaceAgentsDir]) {
268
- try {
269
- if (existsSync(dir)) {
270
- for (const file of readdirSync(dir)) {
271
- if (file.endsWith(".md") && !customTypes.includes(file.replace(".md", ""))) {
272
- customTypes.push(file.replace(".md", ""));
273
- }
274
- }
275
- }
276
- }
277
- catch { /* ignore */ }
278
- }
279
- const allTypes = [...new Set([...builtinTypes, ...Object.keys(types), ...customTypes])];
280
- const typeList = allTypes.map((t) => {
281
- const isEnabled = types[t]?.enabled !== false;
282
- const isBuiltin = builtinTypes.includes(t);
283
- const scope = customTypes.includes(t) ? "project" : "global";
284
- return `${t}(${scope})${isEnabled ? "" : " [disabled]"}`;
285
- }).join(", ");
286
- const activeAgents = manager.listAgents().filter((a) => a.status === "running").length;
287
- return {
288
- maxConcurrent: { value: String(manager.getMaxConcurrent()) },
289
- activeCount: { value: String(activeAgents) },
290
- enabled: { value: config.enabled ? "yes" : "no" },
291
- types: {
292
- value: allTypes.length > 0 ? allTypes[0] : "none",
293
- detail: allTypes.length > 1 ? typeList : undefined,
294
- },
295
- };
296
- },
297
- });
298
- }
299
- // Session start: emit MODULE_READY
300
- pi.on("session_start", async (_event, ctx) => {
301
- emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
302
- name: MODULES.SUBAGENTS || "subagents",
303
- version: "0.2.0",
304
- commands: [],
305
- tools: ["spawn_helper", "get_helper_result"],
306
- });
307
- });
308
- // ESC propagation: abort all agents on session shutdown
309
- pi.on("session_shutdown", async () => {
310
- manager.abortAll();
311
- manager.dispose();
312
- });
313
- // Wire UI context for widget + age finished agents on new turn
314
- pi.on("tool_execution_start", async (_event, ctx) => {
315
- widget.setUICtx(ctx.ui);
316
- widget.onTurnStart();
317
- });
318
- // Create activity tracker
319
- function createActivityTracker(maxTurns, onStreamUpdate) {
320
- const state = {
321
- activeTools: new Map(),
322
- toolUses: 0,
323
- turnCount: 1,
324
- maxTurns,
325
- tokens: "",
326
- responseText: "",
327
- };
328
- const callbacks = {
329
- onToolActivity: (activity) => {
330
- if (activity.type === "start") {
331
- state.activeTools.set(activity.toolName + "_" + Date.now(), activity.toolName);
332
- }
333
- else {
334
- for (const [key, name] of state.activeTools) {
335
- if (name === activity.toolName) {
336
- state.activeTools.delete(key);
337
- break;
338
- }
339
- }
340
- state.toolUses++;
341
- }
342
- state.tokens = safeFormatTokens(state.session);
343
- onStreamUpdate?.();
344
- },
345
- onTextDelta: (_delta, fullText) => {
346
- state.responseText = fullText;
347
- onStreamUpdate?.();
348
- },
349
- onTurnEnd: (turnCount) => {
350
- state.turnCount = turnCount;
351
- onStreamUpdate?.();
352
- },
353
- onSessionCreated: (session) => {
354
- state.session = session;
355
- },
356
- };
357
- return { state, callbacks };
358
- }
359
- // ---- Agent tool ----
360
- const builtinTypes = BUILTIN_TYPES.join(", ");
361
- pi.registerTool(defineTool({
362
- name: "spawn_helper",
363
- label: "Spawn Helper",
364
- description: `Launch a sub-agent for parallel work.
365
-
366
- Available agent types: ${builtinTypes}
367
- Custom types can be defined in:
368
- - ~/.unipi/config/agents/<name>.md (global)
369
- - <workspace>/.unipi/config/agents/<name>.md (project)
370
-
371
- Guidelines:
372
- - Use "explore" for parallel file reads
373
- - Use "work" for parallel file writes (transparent locking)
374
- - Use run_in_background for work you don't need immediately
375
- - ESC kills all running agents immediately
376
- - Agents inherit the parent model by default`,
377
- parameters: Type.Object({
378
- type: Type.String({
379
- description: `Agent type: ${builtinTypes}, or custom type from ~/.unipi/config/agents/*.md`,
380
- }),
381
- prompt: Type.String({
382
- description: "The task for the agent to perform.",
383
- }),
384
- description: Type.String({
385
- description: "A short (3-5 word) description of the task.",
386
- }),
387
- run_in_background: Type.Optional(Type.Boolean({
388
- description: "Run in background. Returns helper ID immediately.",
389
- })),
390
- max_turns: Type.Optional(Type.Number({
391
- description: "Max agentic turns before stopping.",
392
- minimum: 1,
393
- })),
394
- model: Type.Optional(Type.String({
395
- description: 'Model override. Accepts "provider/modelId" or fuzzy name (e.g. "haiku", "sonnet"). Omit to inherit parent model.',
396
- })),
397
- thinking: Type.Optional(Type.String({
398
- description: "Thinking level: off, minimal, low, medium, high, xhigh. Omit to inherit parent.",
399
- })),
400
- }),
401
- // ---- Rich inline rendering ----
402
- renderCall(args, theme) {
403
- const displayName = args.type ? args.type : "Agent";
404
- const desc = args.description ?? "";
405
- return new Text("▸ " + theme.fg("toolTitle", theme.bold(displayName)) + (desc ? " " + theme.fg("muted", desc) : ""), 0, 0);
406
- },
407
- renderResult(result, { expanded, isPartial }, theme) {
408
- const details = result.details;
409
- if (!details) {
410
- const text = result.content[0]?.type === "text" ? result.content[0].text : "";
411
- return new Text(text, 0, 0);
412
- }
413
- // Stats helper
414
- const stats = (d) => {
415
- const parts = [];
416
- if (d.turnCount != null && d.turnCount > 0)
417
- parts.push(formatTurns(d.turnCount, d.maxTurns));
418
- if (d.toolUses > 0)
419
- parts.push(`${d.toolUses} tool use${d.toolUses === 1 ? "" : "s"}`);
420
- if (d.tokens)
421
- parts.push(d.tokens);
422
- return parts.map((p) => theme.fg("dim", p)).join(" " + theme.fg("dim", "·") + " ");
423
- };
424
- // Running
425
- if (isPartial || details.status === "running") {
426
- const frame = SPINNER[details.spinnerFrame ?? 0];
427
- const s = stats(details);
428
- let line = theme.fg("accent", frame) + (s ? " " + s : "");
429
- line += "\n" + theme.fg("dim", ` ⎿ ${details.activity ?? "thinking…"}`);
430
- return new Text(line, 0, 0);
431
- }
432
- // Background launched
433
- if (details.status === "background") {
434
- return new Text(theme.fg("dim", ` ⎿ Running in background (ID: ${details.agentId})`), 0, 0);
435
- }
436
- // Completed
437
- if (details.status === "completed") {
438
- const duration = formatMs(details.durationMs);
439
- const s = stats(details);
440
- let line = theme.fg("success", "✓") + (s ? " " + s : "");
441
- line += " " + theme.fg("dim", "·") + " " + theme.fg("dim", duration);
442
- if (expanded) {
443
- const resultText = result.content[0]?.type === "text" ? result.content[0].text : "";
444
- if (resultText) {
445
- const rlines = resultText.split("\n").slice(0, 50);
446
- for (const l of rlines) {
447
- line += "\n" + theme.fg("dim", ` ${l}`);
448
- }
449
- }
450
- }
451
- else {
452
- line += "\n" + theme.fg("dim", " ⎿ Done");
453
- }
454
- return new Text(line, 0, 0);
455
- }
456
- // Error / Aborted / Stopped
457
- const isError = details.status === "error";
458
- const isStopped = details.status === "stopped";
459
- const s = stats(details);
460
- let line = (isStopped ? theme.fg("dim", "■") : theme.fg("error", "✗")) + (s ? " " + s : "");
461
- if (isError) {
462
- line += "\n" + theme.fg("error", ` ⎿ Error: ${details.error ?? "unknown"}`);
463
- }
464
- else if (isStopped) {
465
- line += "\n" + theme.fg("dim", " ⎿ Stopped");
466
- }
467
- else {
468
- line += "\n" + theme.fg("warning", " ⎿ Aborted (max turns exceeded)");
469
- }
470
- return new Text(line, 0, 0);
471
- },
472
- // ---- Execute ----
473
- execute: async (toolCallId, params, signal, onUpdate, ctx) => {
474
- widget.setUICtx(ctx.ui);
475
- const type = params.type;
476
- const prompt = params.prompt;
477
- const description = params.description;
478
- const runInBackground = params.run_in_background;
479
- const maxTurns = params.max_turns;
480
- const modelInput = params.model;
481
- const thinkingLevel = params.thinking;
482
- if (runInBackground) {
483
- const { state: bgState, callbacks: bgCallbacks } = createActivityTracker(maxTurns);
484
- // Wrap onSessionCreated to sync tokens
485
- const origOnSession = bgCallbacks.onSessionCreated;
486
- bgCallbacks.onSessionCreated = (session) => {
487
- origOnSession(session);
488
- bgState.tokens = safeFormatTokens(session);
489
- widget.update();
490
- };
491
- const id = manager.spawn(pi, ctx, type, prompt, {
492
- description,
493
- maxTurns,
494
- modelInput,
495
- modelRegistry: ctx.modelRegistry,
496
- thinkingLevel,
497
- isBackground: true,
498
- ...bgCallbacks,
499
- });
500
- agentActivity.set(id, bgState);
501
- widget.ensureTimer();
502
- widget.update();
503
- const record = manager.getRecord(id);
504
- const isQueued = record?.status === "queued";
505
- return textResult(`Agent ${isQueued ? "queued" : "started"} in background.\n` +
506
- `ID: ${id}\n` +
507
- `Type: ${type}\n` +
508
- `Description: ${description}\n` +
509
- (isQueued ? `Position: queued (max ${manager.getMaxConcurrent()} concurrent)\n` : "") +
510
- `\nYou will be notified when this agent completes.\n` +
511
- `Use get_result to retrieve full results.`, { status: "background", agentId: id });
512
- }
513
- // Foreground execution — stream progress via onUpdate
514
- let spinnerFrame = 0;
515
- const startedAt = Date.now();
516
- let fgId;
517
- const { state: fgState, callbacks: fgCallbacks } = createActivityTracker(maxTurns);
518
- const streamUpdate = () => {
519
- onUpdate?.({
520
- content: [{ type: "text", text: `${fgState.toolUses} tool uses...` }],
521
- details: {
522
- status: "running",
523
- toolUses: fgState.toolUses,
524
- tokens: fgState.tokens,
525
- turnCount: fgState.turnCount,
526
- maxTurns: fgState.maxTurns,
527
- durationMs: Date.now() - startedAt,
528
- activity: describeActivity(fgState.activeTools, fgState.responseText),
529
- spinnerFrame: spinnerFrame % SPINNER.length,
530
- },
531
- });
532
- };
533
- // Wire session to register in widget
534
- const origOnSession = fgCallbacks.onSessionCreated;
535
- fgCallbacks.onSessionCreated = (session) => {
536
- origOnSession(session);
537
- fgState.tokens = safeFormatTokens(session);
538
- for (const a of manager.listAgents()) {
539
- if (a.session === session) {
540
- fgId = a.id;
541
- agentActivity.set(a.id, fgState);
542
- widget.ensureTimer();
543
- break;
544
- }
545
- }
546
- };
547
- const spinnerInterval = setInterval(() => {
548
- spinnerFrame++;
549
- streamUpdate();
550
- }, 80);
551
- streamUpdate();
552
- const record = await manager.spawnAndWait(pi, ctx, type, prompt, {
553
- description,
554
- maxTurns,
555
- modelInput,
556
- modelRegistry: ctx.modelRegistry,
557
- thinkingLevel,
558
- ...fgCallbacks,
559
- });
560
- clearInterval(spinnerInterval);
561
- // Clean up foreground agent from widget
562
- if (fgId) {
563
- agentActivity.delete(fgId);
564
- widget.markFinished(fgId);
565
- widget.update();
566
- }
567
- const tokenText = safeFormatTokens(fgState.session);
568
- const durationMs = (record.completedAt ?? Date.now()) - record.startedAt;
569
- if (record.status === "error") {
570
- return textResult(`Agent failed: ${record.error}`, {
571
- status: "error",
572
- toolUses: record.toolUses,
573
- tokens: tokenText,
574
- durationMs,
575
- error: record.error,
576
- });
577
- }
578
- return textResult(`Agent completed in ${(durationMs / 1000).toFixed(1)}s (${record.toolUses} tool uses${tokenText ? `, ${tokenText} tokens` : ""}).\n\n` +
579
- (record.result?.trim() || "No output."), {
580
- status: "completed",
581
- toolUses: record.toolUses,
582
- tokens: tokenText,
583
- durationMs,
584
- turnCount: fgState.turnCount,
585
- maxTurns: fgState.maxTurns,
586
- });
587
- },
588
- }));
589
- // ---- get_helper_result tool ----
590
- pi.registerTool(defineTool({
591
- name: "get_helper_result",
592
- label: "Get Helper Result",
593
- description: "Check status and retrieve results from a background agent. Use view: true to open a live conversation overlay.",
594
- parameters: Type.Object({
595
- agent_id: Type.String({
596
- description: "The helper ID to check.",
597
- }),
598
- wait: Type.Optional(Type.Boolean({
599
- description: "Wait for completion. Default: false.",
600
- })),
601
- view: Type.Optional(Type.Boolean({
602
- description: "Open a live conversation viewer overlay. Default: false.",
603
- })),
604
- }),
605
- execute: async (_toolCallId, params, _signal, _onUpdate, ctx) => {
606
- const record = manager.getRecord(params.agent_id);
607
- if (!record) {
608
- return textResult(`Helper not found: "${params.agent_id}". It may have been cleaned up.`);
609
- }
610
- // Open conversation viewer overlay if requested
611
- if (params.view && record.session) {
612
- const activity = agentActivity.get(record.id);
613
- await ctx.ui.custom((tui, theme, _keybindings, done) => {
614
- return new ConversationViewer(tui, record.session, {
615
- type: record.type,
616
- description: record.description,
617
- status: record.status,
618
- toolUses: record.toolUses,
619
- startedAt: record.startedAt,
620
- completedAt: record.completedAt,
621
- }, activity, theme, done);
622
- }, {
623
- overlay: true,
624
- overlayOptions: { anchor: "center", width: "90%" },
625
- });
626
- }
627
- if (params.wait && record.status === "running" && record.promise) {
628
- record.resultConsumed = true;
629
- await record.promise;
630
- }
631
- const duration = record.completedAt
632
- ? `${((record.completedAt - record.startedAt) / 1000).toFixed(1)}s`
633
- : "running";
634
- let output = `Agent: ${record.id}\n` +
635
- `Type: ${record.type} | Status: ${record.status} | Tool uses: ${record.toolUses} | Duration: ${duration}\n` +
636
- `Description: ${record.description}\n\n`;
637
- if (record.status === "running") {
638
- output += "Agent is still running. Use wait: true or check back later.";
639
- }
640
- else if (record.status === "error") {
641
- output += `Error: ${record.error}`;
642
- }
643
- else {
644
- output += record.result?.trim() || "No output.";
645
- }
646
- if (record.status !== "running" && record.status !== "queued") {
647
- record.resultConsumed = true;
648
- }
649
- return textResult(output);
650
- },
651
- }));
652
- }
653
- //# sourceMappingURL=index.js.map