@pi-unipi/subagents 0.1.13 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/config.test.d.ts +11 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.js +196 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/esc-propagation.test.d.ts +10 -0
- package/dist/__tests__/esc-propagation.test.d.ts.map +1 -0
- package/dist/__tests__/esc-propagation.test.js +140 -0
- package/dist/__tests__/esc-propagation.test.js.map +1 -0
- package/dist/__tests__/file-lock.test.d.ts +12 -0
- package/dist/__tests__/file-lock.test.d.ts.map +1 -0
- package/dist/__tests__/file-lock.test.js +187 -0
- package/dist/__tests__/file-lock.test.js.map +1 -0
- package/dist/__tests__/workflow-integration.test.d.ts +12 -0
- package/dist/__tests__/workflow-integration.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-integration.test.js +261 -0
- package/dist/__tests__/workflow-integration.test.js.map +1 -0
- package/dist/agent-manager.d.ts +4 -1
- package/dist/agent-manager.d.ts.map +1 -1
- package/dist/agent-manager.js +10 -0
- package/dist/agent-manager.js.map +1 -1
- package/dist/agent-runner.d.ts +2 -1
- package/dist/agent-runner.d.ts.map +1 -1
- package/dist/agent-runner.js +23 -7
- package/dist/agent-runner.js.map +1 -1
- package/dist/conversation-viewer.d.ts +40 -0
- package/dist/conversation-viewer.d.ts.map +1 -0
- package/dist/conversation-viewer.js +276 -0
- package/dist/conversation-viewer.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +410 -58
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +30 -0
- package/dist/types.js.map +1 -1
- package/dist/widget.d.ts +32 -3
- package/dist/widget.d.ts.map +1 -1
- package/dist/widget.js +298 -56
- package/dist/widget.js.map +1 -1
- package/package.json +1 -1
- package/src/agent-manager.ts +12 -1
- package/src/agent-runner.ts +23 -8
- package/src/conversation-viewer.ts +299 -0
- package/src/index.ts +411 -49
- package/src/types.ts +49 -0
- package/src/widget.ts +332 -72
package/dist/index.js
CHANGED
|
@@ -1,49 +1,169 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @pi-unipi/subagents — Extension entry
|
|
3
3
|
*
|
|
4
|
-
* Tools:
|
|
4
|
+
* Tools: spawn_helper, get_helper_result
|
|
5
|
+
* Features: renderCall/renderResult, message renderer, conversation viewer
|
|
5
6
|
* ESC propagation: all children abort on parent ESC
|
|
6
7
|
*/
|
|
7
8
|
import { defineTool } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
8
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";
|
|
9
15
|
import { AgentManager } from "./agent-manager.js";
|
|
10
16
|
import { initConfig } from "./config.js";
|
|
11
17
|
import { BUILTIN_TYPES } from "./types.js";
|
|
18
|
+
import { ConversationViewer } from "./conversation-viewer.js";
|
|
12
19
|
import { AgentWidget } from "./widget.js";
|
|
13
|
-
/**
|
|
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. */
|
|
14
83
|
function safeFormatTokens(session) {
|
|
15
84
|
if (!session)
|
|
16
85
|
return "";
|
|
17
86
|
try {
|
|
18
87
|
const stats = session.getSessionStats();
|
|
19
88
|
const total = stats.tokens?.total ?? 0;
|
|
20
|
-
|
|
21
|
-
return `${(total / 1_000_000).toFixed(1)}M`;
|
|
22
|
-
if (total >= 1_000)
|
|
23
|
-
return `${(total / 1_000).toFixed(1)}k`;
|
|
24
|
-
return `${total}`;
|
|
89
|
+
return formatTokens(total);
|
|
25
90
|
}
|
|
26
91
|
catch {
|
|
27
92
|
return "";
|
|
28
93
|
}
|
|
29
94
|
}
|
|
30
|
-
/**
|
|
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 */
|
|
31
107
|
function textResult(msg, details) {
|
|
32
108
|
return { content: [{ type: "text", text: msg }], details };
|
|
33
109
|
}
|
|
110
|
+
/** Escape XML for structured notifications. */
|
|
111
|
+
function escapeXml(s) {
|
|
112
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
+
}
|
|
34
123
|
export default function (pi) {
|
|
35
124
|
// Initialize config
|
|
36
125
|
const config = initConfig(process.cwd());
|
|
37
126
|
if (!config.enabled)
|
|
38
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");
|
|
39
133
|
// Activity tracking for widget
|
|
40
134
|
const agentActivity = new Map();
|
|
41
135
|
// Create manager with completion callback
|
|
42
136
|
const manager = new AgentManager((record) => {
|
|
43
|
-
// On complete: clean up activity, emit event
|
|
44
137
|
agentActivity.delete(record.id);
|
|
45
138
|
widget.markFinished(record.id);
|
|
46
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
|
+
}
|
|
47
167
|
pi.events.emit("subagents:completed", {
|
|
48
168
|
id: record.id,
|
|
49
169
|
type: record.type,
|
|
@@ -53,40 +173,150 @@ export default function (pi) {
|
|
|
53
173
|
error: record.error,
|
|
54
174
|
});
|
|
55
175
|
}, config.maxConcurrent, (record) => {
|
|
56
|
-
// On start: emit event
|
|
57
176
|
pi.events.emit("subagents:started", {
|
|
58
177
|
id: record.id,
|
|
59
178
|
type: record.type,
|
|
60
179
|
description: record.description,
|
|
61
180
|
});
|
|
62
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
|
+
});
|
|
63
244
|
// Create widget
|
|
64
245
|
const widget = new AgentWidget(manager, agentActivity);
|
|
65
|
-
//
|
|
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
|
|
66
300
|
pi.on("session_start", async (_event, ctx) => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
`• Global: ${globalConfig}\n` +
|
|
74
|
-
`• Global agents: ${globalAgents}\n` +
|
|
75
|
-
`• Workspace: ${workspaceConfig}\n` +
|
|
76
|
-
`• Workspace agents: ${workspaceAgents}`, "info");
|
|
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
|
+
});
|
|
77
307
|
});
|
|
78
308
|
// ESC propagation: abort all agents on session shutdown
|
|
79
309
|
pi.on("session_shutdown", async () => {
|
|
80
310
|
manager.abortAll();
|
|
81
311
|
manager.dispose();
|
|
82
312
|
});
|
|
83
|
-
// Wire UI context for widget
|
|
313
|
+
// Wire UI context for widget + age finished agents on new turn
|
|
84
314
|
pi.on("tool_execution_start", async (_event, ctx) => {
|
|
85
315
|
widget.setUICtx(ctx.ui);
|
|
86
|
-
widget.
|
|
316
|
+
widget.onTurnStart();
|
|
87
317
|
});
|
|
88
318
|
// Create activity tracker
|
|
89
|
-
function createActivityTracker(maxTurns) {
|
|
319
|
+
function createActivityTracker(maxTurns, onStreamUpdate) {
|
|
90
320
|
const state = {
|
|
91
321
|
activeTools: new Map(),
|
|
92
322
|
toolUses: 0,
|
|
@@ -109,20 +339,19 @@ export default function (pi) {
|
|
|
109
339
|
}
|
|
110
340
|
state.toolUses++;
|
|
111
341
|
}
|
|
112
|
-
|
|
342
|
+
state.tokens = safeFormatTokens(state.session);
|
|
343
|
+
onStreamUpdate?.();
|
|
113
344
|
},
|
|
114
345
|
onTextDelta: (_delta, fullText) => {
|
|
115
346
|
state.responseText = fullText;
|
|
116
|
-
|
|
347
|
+
onStreamUpdate?.();
|
|
117
348
|
},
|
|
118
349
|
onTurnEnd: (turnCount) => {
|
|
119
350
|
state.turnCount = turnCount;
|
|
120
|
-
|
|
351
|
+
onStreamUpdate?.();
|
|
121
352
|
},
|
|
122
353
|
onSessionCreated: (session) => {
|
|
123
354
|
state.session = session;
|
|
124
|
-
state.tokens = safeFormatTokens(session);
|
|
125
|
-
widget.update();
|
|
126
355
|
},
|
|
127
356
|
};
|
|
128
357
|
return { state, callbacks };
|
|
@@ -130,8 +359,8 @@ export default function (pi) {
|
|
|
130
359
|
// ---- Agent tool ----
|
|
131
360
|
const builtinTypes = BUILTIN_TYPES.join(", ");
|
|
132
361
|
pi.registerTool(defineTool({
|
|
133
|
-
name: "
|
|
134
|
-
label: "
|
|
362
|
+
name: "spawn_helper",
|
|
363
|
+
label: "Spawn Helper",
|
|
135
364
|
description: `Launch a sub-agent for parallel work.
|
|
136
365
|
|
|
137
366
|
Available agent types: ${builtinTypes}
|
|
@@ -156,7 +385,7 @@ Guidelines:
|
|
|
156
385
|
description: "A short (3-5 word) description of the task.",
|
|
157
386
|
}),
|
|
158
387
|
run_in_background: Type.Optional(Type.Boolean({
|
|
159
|
-
description: "Run in background. Returns
|
|
388
|
+
description: "Run in background. Returns helper ID immediately.",
|
|
160
389
|
})),
|
|
161
390
|
max_turns: Type.Optional(Type.Number({
|
|
162
391
|
description: "Max agentic turns before stopping.",
|
|
@@ -169,6 +398,78 @@ Guidelines:
|
|
|
169
398
|
description: "Thinking level: off, minimal, low, medium, high, xhigh. Omit to inherit parent.",
|
|
170
399
|
})),
|
|
171
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 ----
|
|
172
473
|
execute: async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
173
474
|
widget.setUICtx(ctx.ui);
|
|
174
475
|
const type = params.type;
|
|
@@ -178,10 +479,15 @@ Guidelines:
|
|
|
178
479
|
const maxTurns = params.max_turns;
|
|
179
480
|
const modelInput = params.model;
|
|
180
481
|
const thinkingLevel = params.thinking;
|
|
181
|
-
// Create activity tracker
|
|
182
|
-
const { state: bgState, callbacks: bgCallbacks } = createActivityTracker(maxTurns);
|
|
183
482
|
if (runInBackground) {
|
|
184
|
-
|
|
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
|
+
};
|
|
185
491
|
const id = manager.spawn(pi, ctx, type, prompt, {
|
|
186
492
|
description,
|
|
187
493
|
maxTurns,
|
|
@@ -204,33 +510,44 @@ Guidelines:
|
|
|
204
510
|
`\nYou will be notified when this agent completes.\n` +
|
|
205
511
|
`Use get_result to retrieve full results.`, { status: "background", agentId: id });
|
|
206
512
|
}
|
|
207
|
-
// Foreground execution
|
|
513
|
+
// Foreground execution — stream progress via onUpdate
|
|
208
514
|
let spinnerFrame = 0;
|
|
209
515
|
const startedAt = Date.now();
|
|
210
516
|
let fgId;
|
|
517
|
+
const { state: fgState, callbacks: fgCallbacks } = createActivityTracker(maxTurns);
|
|
211
518
|
const streamUpdate = () => {
|
|
212
519
|
onUpdate?.({
|
|
213
|
-
content: [{ type: "text", text: `${
|
|
520
|
+
content: [{ type: "text", text: `${fgState.toolUses} tool uses...` }],
|
|
214
521
|
details: {
|
|
215
522
|
status: "running",
|
|
216
|
-
toolUses:
|
|
217
|
-
tokens:
|
|
218
|
-
turnCount:
|
|
219
|
-
maxTurns:
|
|
523
|
+
toolUses: fgState.toolUses,
|
|
524
|
+
tokens: fgState.tokens,
|
|
525
|
+
turnCount: fgState.turnCount,
|
|
526
|
+
maxTurns: fgState.maxTurns,
|
|
220
527
|
durationMs: Date.now() - startedAt,
|
|
221
|
-
activity:
|
|
222
|
-
|
|
223
|
-
: "thinking…",
|
|
224
|
-
spinnerFrame: spinnerFrame % 10,
|
|
528
|
+
activity: describeActivity(fgState.activeTools, fgState.responseText),
|
|
529
|
+
spinnerFrame: spinnerFrame % SPINNER.length,
|
|
225
530
|
},
|
|
226
531
|
});
|
|
227
|
-
|
|
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
|
+
}
|
|
228
546
|
};
|
|
229
547
|
const spinnerInterval = setInterval(() => {
|
|
230
548
|
spinnerFrame++;
|
|
231
549
|
streamUpdate();
|
|
232
550
|
}, 80);
|
|
233
|
-
widget.ensureTimer();
|
|
234
551
|
streamUpdate();
|
|
235
552
|
const record = await manager.spawnAndWait(pi, ctx, type, prompt, {
|
|
236
553
|
description,
|
|
@@ -238,39 +555,74 @@ Guidelines:
|
|
|
238
555
|
modelInput,
|
|
239
556
|
modelRegistry: ctx.modelRegistry,
|
|
240
557
|
thinkingLevel,
|
|
241
|
-
...
|
|
558
|
+
...fgCallbacks,
|
|
242
559
|
});
|
|
243
560
|
clearInterval(spinnerInterval);
|
|
561
|
+
// Clean up foreground agent from widget
|
|
244
562
|
if (fgId) {
|
|
245
563
|
agentActivity.delete(fgId);
|
|
246
564
|
widget.markFinished(fgId);
|
|
565
|
+
widget.update();
|
|
247
566
|
}
|
|
248
|
-
const tokenText = safeFormatTokens(
|
|
567
|
+
const tokenText = safeFormatTokens(fgState.session);
|
|
249
568
|
const durationMs = (record.completedAt ?? Date.now()) - record.startedAt;
|
|
250
569
|
if (record.status === "error") {
|
|
251
|
-
return textResult(`Agent failed: ${record.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
|
+
});
|
|
252
577
|
}
|
|
253
578
|
return textResult(`Agent completed in ${(durationMs / 1000).toFixed(1)}s (${record.toolUses} tool uses${tokenText ? `, ${tokenText} tokens` : ""}).\n\n` +
|
|
254
|
-
(record.result?.trim() || "No output.")
|
|
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
|
+
});
|
|
255
587
|
},
|
|
256
588
|
}));
|
|
257
|
-
// ----
|
|
589
|
+
// ---- get_helper_result tool ----
|
|
258
590
|
pi.registerTool(defineTool({
|
|
259
|
-
name: "
|
|
260
|
-
label: "Get
|
|
261
|
-
description: "Check status and retrieve results from a background agent.",
|
|
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.",
|
|
262
594
|
parameters: Type.Object({
|
|
263
595
|
agent_id: Type.String({
|
|
264
|
-
description: "The
|
|
596
|
+
description: "The helper ID to check.",
|
|
265
597
|
}),
|
|
266
598
|
wait: Type.Optional(Type.Boolean({
|
|
267
599
|
description: "Wait for completion. Default: false.",
|
|
268
600
|
})),
|
|
601
|
+
view: Type.Optional(Type.Boolean({
|
|
602
|
+
description: "Open a live conversation viewer overlay. Default: false.",
|
|
603
|
+
})),
|
|
269
604
|
}),
|
|
270
|
-
execute: async (_toolCallId, params) => {
|
|
605
|
+
execute: async (_toolCallId, params, _signal, _onUpdate, ctx) => {
|
|
271
606
|
const record = manager.getRecord(params.agent_id);
|
|
272
607
|
if (!record) {
|
|
273
|
-
return textResult(`
|
|
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
|
+
});
|
|
274
626
|
}
|
|
275
627
|
if (params.wait && record.status === "running" && record.promise) {
|
|
276
628
|
record.resultConsumed = true;
|