@pi-unipi/subagents 2.6.0 → 2.6.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.
Files changed (107) hide show
  1. package/README.md +89 -78
  2. package/agents/delegate.md +14 -0
  3. package/agents/oracle.md +78 -0
  4. package/agents/researcher.md +52 -0
  5. package/agents/reviewer.md +79 -0
  6. package/agents/scout.md +50 -0
  7. package/agents/worker.md +59 -0
  8. package/package.json +19 -17
  9. package/prompts/council.md +48 -0
  10. package/prompts/gather-context-and-clarify.md +13 -0
  11. package/prompts/parallel-cleanup.md +59 -0
  12. package/prompts/parallel-research.md +50 -0
  13. package/prompts/parallel-review.md +54 -0
  14. package/prompts/review-loop.md +43 -0
  15. package/skills/council-mode/SKILL.md +230 -0
  16. package/skills/subagents/SKILL.md +49 -0
  17. package/skills/subagents/references/constraints-and-recipes.md +259 -0
  18. package/skills/subagents/references/execution-controls.md +454 -0
  19. package/skills/subagents/references/management-authoring-rpc.md +161 -0
  20. package/skills/subagents/references/multi-lane-orchestration.md +39 -0
  21. package/skills/subagents/references/prompting-and-roles.md +267 -0
  22. package/src/acceptance.ts +331 -0
  23. package/src/agent-manager.ts +405 -0
  24. package/src/agent-memory.ts +300 -0
  25. package/src/agent-overrides.ts +183 -0
  26. package/src/agent-runner.ts +265 -0
  27. package/src/async-runner.ts +417 -0
  28. package/src/authority-policy.ts +47 -0
  29. package/src/budgets.ts +285 -0
  30. package/src/child-safety.ts +140 -0
  31. package/src/config.ts +226 -0
  32. package/src/conversation-viewer.ts +281 -0
  33. package/src/core-compat.ts +94 -0
  34. package/src/custom-agents.ts +323 -0
  35. package/src/file-system-retry.ts +50 -0
  36. package/src/fleet-data.ts +61 -0
  37. package/src/fleet-view.ts +316 -0
  38. package/src/foreground-detach.ts +59 -0
  39. package/src/fork-context.ts +285 -0
  40. package/src/global.d.ts +7 -0
  41. package/src/guide.ts +129 -0
  42. package/src/index.ts +1149 -0
  43. package/src/mission-state.ts +133 -0
  44. package/src/mission-store.ts +434 -0
  45. package/src/model-resolver.ts +79 -0
  46. package/src/output-limits.ts +142 -0
  47. package/src/parity-types.ts +315 -0
  48. package/src/pi-args.ts +161 -0
  49. package/src/pi-spawn.ts +147 -0
  50. package/src/result-files.ts +232 -0
  51. package/src/result-watcher.ts +221 -0
  52. package/src/retained-children.ts +176 -0
  53. package/src/run-fanout-budget.ts +356 -0
  54. package/src/scheduled-runs.ts +351 -0
  55. package/src/schemas.ts +263 -0
  56. package/src/slash-commands.ts +77 -0
  57. package/src/supervisor-channel.ts +249 -0
  58. package/src/tool-handler.ts +1144 -0
  59. package/src/types.ts +238 -0
  60. package/src/widget.ts +447 -0
  61. package/src/workflow-script.ts +737 -0
  62. package/src/workflow-worker.ts +384 -0
  63. package/src/worktree.ts +614 -0
  64. package/dist/agent-manager.d.ts +0 -81
  65. package/dist/agent-manager.d.ts.map +0 -1
  66. package/dist/agent-manager.js +0 -295
  67. package/dist/agent-manager.js.map +0 -1
  68. package/dist/agent-runner.d.ts +0 -51
  69. package/dist/agent-runner.d.ts.map +0 -1
  70. package/dist/agent-runner.js +0 -262
  71. package/dist/agent-runner.js.map +0 -1
  72. package/dist/config.d.ts +0 -24
  73. package/dist/config.d.ts.map +0 -1
  74. package/dist/config.js +0 -132
  75. package/dist/config.js.map +0 -1
  76. package/dist/conversation-viewer.d.ts +0 -40
  77. package/dist/conversation-viewer.d.ts.map +0 -1
  78. package/dist/conversation-viewer.js +0 -276
  79. package/dist/conversation-viewer.js.map +0 -1
  80. package/dist/core-compat.d.ts +0 -21
  81. package/dist/core-compat.d.ts.map +0 -1
  82. package/dist/core-compat.js +0 -86
  83. package/dist/core-compat.js.map +0 -1
  84. package/dist/custom-agents.d.ts +0 -14
  85. package/dist/custom-agents.d.ts.map +0 -1
  86. package/dist/custom-agents.js +0 -113
  87. package/dist/custom-agents.js.map +0 -1
  88. package/dist/file-lock.d.ts +0 -42
  89. package/dist/file-lock.d.ts.map +0 -1
  90. package/dist/file-lock.js +0 -91
  91. package/dist/file-lock.js.map +0 -1
  92. package/dist/index.d.ts +0 -10
  93. package/dist/index.d.ts.map +0 -1
  94. package/dist/index.js +0 -758
  95. package/dist/index.js.map +0 -1
  96. package/dist/model-resolver.d.ts +0 -19
  97. package/dist/model-resolver.d.ts.map +0 -1
  98. package/dist/model-resolver.js +0 -61
  99. package/dist/model-resolver.js.map +0 -1
  100. package/dist/types.d.ts +0 -98
  101. package/dist/types.d.ts.map +0 -1
  102. package/dist/types.js +0 -47
  103. package/dist/types.js.map +0 -1
  104. package/dist/widget.d.ts +0 -56
  105. package/dist/widget.d.ts.map +0 -1
  106. package/dist/widget.js +0 -396
  107. package/dist/widget.js.map +0 -1
package/dist/index.js DELETED
@@ -1,758 +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 "@earendil-works/pi-coding-agent";
9
- import { Text } from "@earendil-works/pi-tui";
10
- import { Type } from "typebox";
11
- import { existsSync, readdirSync } from "node:fs";
12
- import { join } from "node:path";
13
- import { homedir } from "node:os";
14
- import { boundHelperOutput, emitEvent, MODULES, UNIPI_EVENTS, withHerdrBlocked } from "./core-compat.js";
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
- return globalThis.__unipi_info_registry;
23
- }
24
- // ---- Formatting helpers (shared between renderers and inline text) ----
25
- const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
26
- /** Tool name → human-readable action. */
27
- const TOOL_DISPLAY = {
28
- read: "reading",
29
- bash: "running command",
30
- edit: "editing",
31
- write: "writing",
32
- grep: "searching",
33
- find: "finding files",
34
- ls: "listing",
35
- };
36
- function formatTokens(count) {
37
- if (count >= 1_000_000)
38
- return `${(count / 1_000_000).toFixed(1)}M token`;
39
- if (count >= 1_000)
40
- return `${(count / 1_000).toFixed(1)}k token`;
41
- return `${count} token`;
42
- }
43
- function formatTurns(turn, max) {
44
- return max != null ? `⟳${turn}≤${max}` : `⟳${turn}`;
45
- }
46
- function formatMs(ms) {
47
- if (ms >= 60_000)
48
- return `${(ms / 60_000).toFixed(1)}m`;
49
- if (ms >= 1_000)
50
- return `${(ms / 1_000).toFixed(1)}s`;
51
- return `${ms}ms`;
52
- }
53
- /** Build activity description from active tools. */
54
- function describeActivity(activeTools, responseText) {
55
- if (activeTools.size > 0) {
56
- const groups = new Map();
57
- for (const toolName of activeTools.values()) {
58
- const action = TOOL_DISPLAY[toolName] ?? toolName;
59
- groups.set(action, (groups.get(action) ?? 0) + 1);
60
- }
61
- const parts = [];
62
- for (const [action, count] of groups) {
63
- if (count > 1) {
64
- parts.push(`${action} ${count} ${action === "searching" ? "patterns" : "files"}`);
65
- }
66
- else {
67
- parts.push(action);
68
- }
69
- }
70
- return parts.join(", ") + "…";
71
- }
72
- if (responseText && responseText.trim().length > 0) {
73
- const line = responseText.split("\n").find((l) => l.trim())?.trim() ?? "";
74
- if (line.length > 60)
75
- return line.slice(0, 60) + "…";
76
- if (line.length > 0)
77
- return line;
78
- }
79
- return "thinking…";
80
- }
81
- /** Format tokens safely from session. */
82
- function safeFormatTokens(session) {
83
- if (!session)
84
- return "";
85
- try {
86
- const stats = session.getSessionStats();
87
- const total = stats.tokens?.total ?? 0;
88
- return formatTokens(total);
89
- }
90
- catch {
91
- return "";
92
- }
93
- }
94
- /** Get raw token count from session. */
95
- function safeTokenCount(session) {
96
- if (!session)
97
- return 0;
98
- try {
99
- return session.getSessionStats().tokens?.total ?? 0;
100
- }
101
- catch {
102
- return 0;
103
- }
104
- }
105
- /** Build result text */
106
- function textResult(msg, details) {
107
- return { content: [{ type: "text", text: msg }], details };
108
- }
109
- /** Escape XML for structured notifications. */
110
- function escapeXml(s) {
111
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
112
- }
113
- /** Human-readable status label. */
114
- function getStatusLabel(status, error) {
115
- switch (status) {
116
- case "error": return `Error: ${error ?? "unknown"}`;
117
- case "aborted": return "Aborted (max turns exceeded)";
118
- case "stopped": return "Stopped";
119
- default: return "Done";
120
- }
121
- }
122
- export default function (pi) {
123
- // Initialize config
124
- const config = initConfig(process.cwd());
125
- if (!config.enabled)
126
- return;
127
- // Compute paths at factory time
128
- const homeDir = homedir();
129
- const cwd = process.cwd();
130
- const globalAgentsDir = join(homeDir, ".unipi", "config", "agents");
131
- const workspaceAgentsDir = join(cwd, ".unipi", "config", "agents");
132
- // Activity tracking for widget
133
- const agentActivity = new Map();
134
- /**
135
- * Set once `session_shutdown` fires — after which `pi` must not be touched.
136
- *
137
- * Pi disposes the session as soon as the shutdown handlers resolve, and
138
- * `AgentSession.dispose()` invalidates the extension runtime. Every
139
- * `assertActive`-gated method (`sendMessage`, `setSessionName`,
140
- * `appendEntry`, `setModel`, …) then throws "This extension ctx is stale
141
- * after session replacement or reload".
142
- *
143
- * Background agents outlive that moment: `abortAll()` only signals their
144
- * AbortController, so the in-flight promise settles a microtask *later* and
145
- * fires this completion callback against a dead runtime — an unhandled
146
- * throw that crashed the process on exit.
147
- *
148
- * Scoped to the extension factory rather than module scope on purpose:
149
- * `session_shutdown` also fires for `/new`, `/fork` and `/resume` (reasons
150
- * "new" / "fork" / "resume"), and pi re-invokes the extension factory for
151
- * the replacement session. A fresh closure therefore starts with
152
- * `sessionEnded = false`, so the guard can never latch permanently.
153
- * Verified: `/new` emits `shutdown reason=new` then re-runs the factory.
154
- *
155
- * `pi.events` is NOT gated, so cross-module events still fire.
156
- */
157
- let sessionEnded = false;
158
- // Create manager with completion callback
159
- const manager = new AgentManager((record) => {
160
- agentActivity.delete(record.id);
161
- // After shutdown the UI is gone and the runtime is stale — nothing here
162
- // is deliverable, and touching `pi` would throw.
163
- if (sessionEnded)
164
- return;
165
- widget.markFinished(record.id);
166
- widget.update();
167
- // Build notification details
168
- const details = buildNotificationDetails(record, agentActivity.get(record.id));
169
- // Badge generation: extract name from agent result and set directly.
170
- // Mark resultConsumed BEFORE the notification check so the main agent
171
- // never sees this subagent.
172
- if (record.description === "Generate session name" && record.result && record.status === "completed") {
173
- const name = record.result.split("\n")[0]?.trim().slice(0, 50) ?? "";
174
- if (name && !name.startsWith("Error") && !name.includes("error")) {
175
- try {
176
- pi.setSessionName(name);
177
- }
178
- catch { /* best effort */ }
179
- }
180
- record.resultConsumed = true;
181
- }
182
- // Send styled notification via message renderer
183
- const status = getStatusLabel(record.status, record.error);
184
- const durationMs = record.completedAt ? record.completedAt - record.startedAt : 0;
185
- const resultPreview = record.result
186
- ? record.result.length > 500
187
- ? record.result.slice(0, 500) + "…"
188
- : record.result
189
- : "No output.";
190
- const notificationXml = [
191
- `<task-notification>`,
192
- `<task-id>${record.id}</task-id>`,
193
- `<status>${escapeXml(status)}</status>`,
194
- `<summary>Agent "${escapeXml(record.description)}" ${record.status}</summary>`,
195
- `<result>${escapeXml(resultPreview)}</result>`,
196
- `<usage><total_tokens>${details.totalTokens}</total_tokens><tool_uses>${record.toolUses}</tool_uses><duration_ms>${durationMs}</duration_ms></usage>`,
197
- `</task-notification>`,
198
- ].join("\n");
199
- if (!record.resultConsumed) {
200
- // Defence in depth: `sessionEnded` covers the ordinary shutdown path,
201
- // but a session can also be replaced mid-flight. Delivering a
202
- // notification is best-effort — it must never take the process down.
203
- try {
204
- pi.sendMessage({
205
- customType: "subagent-notification",
206
- content: notificationXml,
207
- display: true,
208
- details,
209
- }, { deliverAs: "followUp", triggerTurn: true });
210
- }
211
- catch {
212
- // Runtime went stale between the guard and here — nothing to notify.
213
- }
214
- }
215
- pi.events.emit("subagents:completed", {
216
- id: record.id,
217
- type: record.type,
218
- description: record.description,
219
- status: record.status,
220
- result: record.result,
221
- error: record.error,
222
- });
223
- }, config.maxConcurrent, (record) => {
224
- pi.events.emit("subagents:started", {
225
- id: record.id,
226
- type: record.type,
227
- description: record.description,
228
- });
229
- }, config.types);
230
- // Build notification details for the message renderer
231
- function buildNotificationDetails(record, activity) {
232
- return {
233
- id: record.id,
234
- description: record.description,
235
- status: record.status,
236
- toolUses: record.toolUses,
237
- turnCount: activity?.turnCount ?? 0,
238
- maxTurns: activity?.maxTurns,
239
- totalTokens: safeTokenCount(record.session),
240
- durationMs: record.completedAt ? record.completedAt - record.startedAt : 0,
241
- error: record.error,
242
- resultPreview: record.result
243
- ? record.result.length > 200
244
- ? record.result.slice(0, 200) + "…"
245
- : record.result
246
- : "No output.",
247
- };
248
- }
249
- // ---- Register custom notification renderer ----
250
- pi.registerMessageRenderer("subagent-notification", (message, { expanded }, theme) => {
251
- const d = message.details;
252
- if (!d)
253
- return undefined;
254
- function renderOne(d) {
255
- const isError = d.status === "error" || d.status === "stopped" || d.status === "aborted";
256
- const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
257
- const statusText = isError
258
- ? d.status
259
- : d.status === "steered"
260
- ? "completed (steered)"
261
- : "completed";
262
- // Line 1: icon + agent description + status
263
- let line = `${icon} ${theme.bold(d.description)} ${theme.fg("dim", statusText)}`;
264
- // Line 2: stats
265
- const parts = [];
266
- if (d.turnCount > 0)
267
- parts.push(formatTurns(d.turnCount, d.maxTurns));
268
- if (d.toolUses > 0)
269
- parts.push(`${d.toolUses} tool use${d.toolUses === 1 ? "" : "s"}`);
270
- if (d.totalTokens > 0)
271
- parts.push(formatTokens(d.totalTokens));
272
- if (d.durationMs > 0)
273
- parts.push(formatMs(d.durationMs));
274
- if (parts.length) {
275
- line += "\n " + parts.map((p) => theme.fg("dim", p)).join(" " + theme.fg("dim", "·") + " ");
276
- }
277
- // Line 3: result preview (collapsed) or full (expanded)
278
- if (expanded) {
279
- const lines = d.resultPreview.split("\n").slice(0, 30);
280
- for (const l of lines)
281
- line += "\n" + theme.fg("dim", ` ${l}`);
282
- }
283
- else {
284
- const preview = d.resultPreview.split("\n")[0]?.slice(0, 80) ?? "";
285
- line += "\n " + theme.fg("dim", `⎿ ${preview}`);
286
- }
287
- return line;
288
- }
289
- const all = [d, ...(d.others ?? [])];
290
- return new Text(all.map(renderOne).join("\n"), 0, 0);
291
- });
292
- // Create widget
293
- const widget = new AgentWidget(manager, agentActivity);
294
- // Register info group at factory time (not session_start)
295
- const registry = getInfoRegistry();
296
- if (registry) {
297
- registry.registerGroup({
298
- id: "subagents",
299
- name: "Subagents",
300
- icon: "🤖",
301
- priority: 80,
302
- config: {
303
- showByDefault: true,
304
- stats: [
305
- { id: "maxConcurrent", label: "Max Concurrent", show: true },
306
- { id: "activeCount", label: "Active Agents", show: true },
307
- { id: "enabled", label: "Enabled", show: true },
308
- { id: "types", label: "Available Types", show: true },
309
- ],
310
- },
311
- dataProvider: async () => {
312
- const types = config.types || {};
313
- const builtinTypes = ["explore", "work"];
314
- const customTypes = [];
315
- for (const dir of [globalAgentsDir, workspaceAgentsDir]) {
316
- try {
317
- if (existsSync(dir)) {
318
- for (const file of readdirSync(dir)) {
319
- if (file.endsWith(".md") && !customTypes.includes(file.replace(".md", ""))) {
320
- customTypes.push(file.replace(".md", ""));
321
- }
322
- }
323
- }
324
- }
325
- catch { /* ignore */ }
326
- }
327
- const allTypes = [...new Set([...builtinTypes, ...Object.keys(types), ...customTypes])];
328
- const typeList = allTypes.map((t) => {
329
- const isEnabled = types[t]?.enabled !== false;
330
- const isBuiltin = builtinTypes.includes(t);
331
- const scope = customTypes.includes(t) ? "project" : "global";
332
- return `${t}(${scope})${isEnabled ? "" : " [disabled]"}`;
333
- }).join(", ");
334
- const activeAgents = manager.listAgents().filter((a) => a.status === "running").length;
335
- return {
336
- maxConcurrent: { value: String(manager.getMaxConcurrent()) },
337
- activeCount: { value: String(activeAgents) },
338
- enabled: { value: config.enabled ? "yes" : "no" },
339
- types: {
340
- value: allTypes.length > 0 ? allTypes[0] : "none",
341
- detail: allTypes.length > 1 ? typeList : undefined,
342
- },
343
- };
344
- },
345
- });
346
- }
347
- // Store session context for badge generation
348
- let sessionCtx = null;
349
- // Session start: emit MODULE_READY + capture context
350
- pi.on("session_start", async (_event, ctx) => {
351
- sessionCtx = ctx;
352
- emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
353
- name: MODULES.SUBAGENTS || "subagents",
354
- version: "0.2.0",
355
- commands: [],
356
- tools: ["spawn_helper", "get_helper_result"],
357
- });
358
- });
359
- // Listen for badge generation requests — spawn background agent
360
- pi.events.on(UNIPI_EVENTS.BADGE_GENERATE_REQUEST, async (data) => {
361
- const event = data;
362
- if (!sessionCtx)
363
- return;
364
- const summary = event?.conversationSummary ?? "";
365
- const prompt = summary
366
- ? `Based on this conversation, generate a concise session title (MAX 5 WORDS). Reply with ONLY the title. No quotes, no explanation, no punctuation.\n\nConversation:\n${summary}`
367
- : `Generate a concise session title (MAX 5 WORDS) for this session. Reply with ONLY the title. No quotes, no explanation, no punctuation.`;
368
- // Try with configured model, fallback to inherit
369
- let modelInput = undefined;
370
- try {
371
- const fs = await import("node:fs");
372
- const path = await import("node:path");
373
- const configPath = path.resolve(process.cwd(), ".unipi/config/badge.json");
374
- if (fs.existsSync(configPath)) {
375
- const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
376
- if (typeof parsed.generationModel === "string" && parsed.generationModel !== "inherit") {
377
- modelInput = parsed.generationModel;
378
- }
379
- }
380
- }
381
- catch { /* ignore — inherit parent model */ }
382
- let resolvedModel = undefined;
383
- // Check if model is available
384
- if (modelInput && sessionCtx.modelRegistry) {
385
- const { resolveModel } = await import("./model-resolver.js");
386
- const result = resolveModel(modelInput, sessionCtx.modelRegistry);
387
- if (typeof result !== "string") {
388
- resolvedModel = result;
389
- }
390
- // If result is a string (error), resolvedModel stays undefined → inherit parent
391
- }
392
- manager.spawn(pi, sessionCtx, "name-gen", prompt, {
393
- description: "Generate session name",
394
- model: resolvedModel,
395
- isBackground: true,
396
- isolated: true,
397
- maxTurns: 1,
398
- });
399
- });
400
- // ESC propagation: abort all agents on session shutdown.
401
- // Set the guard FIRST: abortAll() settles in-flight promises, whose
402
- // completion callbacks would otherwise reach a runtime that pi is about to
403
- // invalidate.
404
- pi.on("session_shutdown", async () => {
405
- sessionEnded = true;
406
- manager.abortAll();
407
- manager.dispose();
408
- });
409
- // Wire UI context for widget + age finished agents on new turn
410
- pi.on("tool_execution_start", async (_event, ctx) => {
411
- widget.setUICtx(ctx.ui);
412
- widget.onTurnStart();
413
- });
414
- // Create activity tracker
415
- function createActivityTracker(maxTurns, onStreamUpdate) {
416
- const state = {
417
- activeTools: new Map(),
418
- toolUses: 0,
419
- turnCount: 1,
420
- maxTurns,
421
- tokens: "",
422
- responseText: "",
423
- };
424
- const callbacks = {
425
- onToolActivity: (activity) => {
426
- if (activity.type === "start") {
427
- state.activeTools.set(activity.toolName + "_" + Date.now(), activity.toolName);
428
- }
429
- else {
430
- for (const [key, name] of state.activeTools) {
431
- if (name === activity.toolName) {
432
- state.activeTools.delete(key);
433
- break;
434
- }
435
- }
436
- state.toolUses++;
437
- }
438
- state.tokens = safeFormatTokens(state.session);
439
- onStreamUpdate?.();
440
- },
441
- onTextDelta: (_delta, fullText) => {
442
- state.responseText = fullText;
443
- onStreamUpdate?.();
444
- },
445
- onTurnEnd: (turnCount) => {
446
- state.turnCount = turnCount;
447
- onStreamUpdate?.();
448
- },
449
- onSessionCreated: (session) => {
450
- state.session = session;
451
- },
452
- };
453
- return { state, callbacks };
454
- }
455
- // ---- Agent tool ----
456
- const builtinTypes = BUILTIN_TYPES.join(", ");
457
- const enabledTypes = manager.getKnownTypes().filter((type) => manager.isTypeEnabled(type));
458
- const availableTypes = enabledTypes.join(", ") || "none";
459
- pi.registerTool(defineTool({
460
- name: "spawn_helper",
461
- label: "Spawn Helper",
462
- description: `Launch a sub-agent for parallel work.
463
-
464
- Available agent types: ${availableTypes}
465
- Custom types can be defined in:
466
- - ~/.unipi/config/agents/<name>.md (global)
467
- - <workspace>/.unipi/config/agents/<name>.md (project)
468
-
469
- Guidelines:
470
- - Use "explore" for parallel file reads
471
- - Use "work" for parallel file writes (transparent locking)
472
- - Use run_in_background for work you don't need immediately
473
- - ESC kills all running agents immediately
474
- - Agents inherit the parent model by default`,
475
- parameters: Type.Object({
476
- type: Type.String({
477
- description: `Enabled agent type: ${availableTypes}`,
478
- }),
479
- prompt: Type.String({
480
- description: "The task for the agent to perform.",
481
- }),
482
- description: Type.String({
483
- description: "A short (3-5 word) description of the task.",
484
- }),
485
- run_in_background: Type.Optional(Type.Boolean({
486
- description: "Run in background. Returns helper ID immediately.",
487
- })),
488
- max_turns: Type.Optional(Type.Number({
489
- description: "Max agentic turns before stopping.",
490
- minimum: 1,
491
- })),
492
- model: Type.Optional(Type.String({
493
- description: 'Model override. Accepts "provider/modelId" or fuzzy name (e.g. "haiku", "sonnet"). Omit to inherit parent model.',
494
- })),
495
- thinking: Type.Optional(Type.String({
496
- description: "Thinking level: off, minimal, low, medium, high, xhigh. Omit to inherit parent.",
497
- })),
498
- }),
499
- // ---- Rich inline rendering ----
500
- renderCall(args, theme) {
501
- const displayName = args.type ? args.type : "Agent";
502
- const desc = args.description ?? "";
503
- return new Text("▸ " + theme.fg("toolTitle", theme.bold(displayName)) + (desc ? " " + theme.fg("muted", desc) : ""), 0, 0);
504
- },
505
- renderResult(result, { expanded, isPartial }, theme) {
506
- const details = result.details;
507
- if (!details) {
508
- const text = result.content[0]?.type === "text" ? result.content[0].text : "";
509
- return new Text(text, 0, 0);
510
- }
511
- // Stats helper
512
- const stats = (d) => {
513
- const parts = [];
514
- if (d.turnCount != null && d.turnCount > 0)
515
- parts.push(formatTurns(d.turnCount, d.maxTurns));
516
- if (d.toolUses > 0)
517
- parts.push(`${d.toolUses} tool use${d.toolUses === 1 ? "" : "s"}`);
518
- if (d.tokens)
519
- parts.push(d.tokens);
520
- return parts.map((p) => theme.fg("dim", p)).join(" " + theme.fg("dim", "·") + " ");
521
- };
522
- // Running
523
- if (isPartial || details.status === "running") {
524
- const frame = SPINNER[details.spinnerFrame ?? 0];
525
- const s = stats(details);
526
- let line = theme.fg("accent", frame) + (s ? " " + s : "");
527
- line += "\n" + theme.fg("dim", ` ⎿ ${details.activity ?? "thinking…"}`);
528
- return new Text(line, 0, 0);
529
- }
530
- // Background launched
531
- if (details.status === "background") {
532
- return new Text(theme.fg("dim", ` ⎿ Running in background (ID: ${details.agentId})`), 0, 0);
533
- }
534
- // Completed
535
- if (details.status === "completed") {
536
- const duration = formatMs(details.durationMs);
537
- const s = stats(details);
538
- let line = theme.fg("success", "✓") + (s ? " " + s : "");
539
- line += " " + theme.fg("dim", "·") + " " + theme.fg("dim", duration);
540
- if (expanded) {
541
- const resultText = result.content[0]?.type === "text" ? result.content[0].text : "";
542
- if (resultText) {
543
- const rlines = resultText.split("\n").slice(0, 50);
544
- for (const l of rlines) {
545
- line += "\n" + theme.fg("dim", ` ${l}`);
546
- }
547
- }
548
- }
549
- else {
550
- line += "\n" + theme.fg("dim", " ⎿ Done");
551
- }
552
- return new Text(line, 0, 0);
553
- }
554
- // Error / Aborted / Stopped
555
- const isError = details.status === "error";
556
- const isStopped = details.status === "stopped";
557
- const s = stats(details);
558
- let line = (isStopped ? theme.fg("dim", "■") : theme.fg("error", "✗")) + (s ? " " + s : "");
559
- if (isError) {
560
- line += "\n" + theme.fg("error", ` ⎿ Error: ${details.error ?? "unknown"}`);
561
- }
562
- else if (isStopped) {
563
- line += "\n" + theme.fg("dim", " ⎿ Stopped");
564
- }
565
- else {
566
- line += "\n" + theme.fg("warning", " ⎿ Aborted (max turns exceeded)");
567
- }
568
- return new Text(line, 0, 0);
569
- },
570
- // ---- Execute ----
571
- execute: async (toolCallId, params, signal, onUpdate, ctx) => {
572
- widget.setUICtx(ctx.ui);
573
- const type = params.type;
574
- const prompt = params.prompt;
575
- const description = params.description;
576
- const runInBackground = params.run_in_background;
577
- const maxTurns = params.max_turns;
578
- const modelInput = params.model;
579
- const thinkingLevel = params.thinking;
580
- if (runInBackground) {
581
- const { state: bgState, callbacks: bgCallbacks } = createActivityTracker(maxTurns);
582
- // Wrap onSessionCreated to sync tokens
583
- const origOnSession = bgCallbacks.onSessionCreated;
584
- bgCallbacks.onSessionCreated = (session) => {
585
- origOnSession(session);
586
- bgState.tokens = safeFormatTokens(session);
587
- widget.update();
588
- };
589
- const id = manager.spawn(pi, ctx, type, prompt, {
590
- description,
591
- maxTurns,
592
- modelInput,
593
- modelRegistry: ctx.modelRegistry,
594
- thinkingLevel,
595
- isBackground: true,
596
- ...bgCallbacks,
597
- });
598
- agentActivity.set(id, bgState);
599
- widget.ensureTimer();
600
- widget.update();
601
- const record = manager.getRecord(id);
602
- const isQueued = record?.status === "queued";
603
- return textResult(`Agent ${isQueued ? "queued" : "started"} in background.\n` +
604
- `ID: ${id}\n` +
605
- `Type: ${type}\n` +
606
- `Description: ${description}\n` +
607
- (isQueued ? `Position: queued (max ${manager.getMaxConcurrent()} concurrent)\n` : "") +
608
- `\nYou will be notified when this agent completes.\n` +
609
- `Use get_result to retrieve full results.`, { status: "background", agentId: id });
610
- }
611
- // Foreground execution — stream progress via onUpdate
612
- let spinnerFrame = 0;
613
- const startedAt = Date.now();
614
- let fgId;
615
- const { state: fgState, callbacks: fgCallbacks } = createActivityTracker(maxTurns);
616
- const streamUpdate = () => {
617
- onUpdate?.({
618
- content: [{ type: "text", text: `${fgState.toolUses} tool uses...` }],
619
- details: {
620
- status: "running",
621
- toolUses: fgState.toolUses,
622
- tokens: fgState.tokens,
623
- turnCount: fgState.turnCount,
624
- maxTurns: fgState.maxTurns,
625
- durationMs: Date.now() - startedAt,
626
- activity: describeActivity(fgState.activeTools, fgState.responseText),
627
- spinnerFrame: spinnerFrame % SPINNER.length,
628
- },
629
- });
630
- };
631
- // Wire session to register in widget
632
- const origOnSession = fgCallbacks.onSessionCreated;
633
- fgCallbacks.onSessionCreated = (session) => {
634
- origOnSession(session);
635
- fgState.tokens = safeFormatTokens(session);
636
- for (const a of manager.listAgents()) {
637
- if (a.session === session) {
638
- fgId = a.id;
639
- agentActivity.set(a.id, fgState);
640
- widget.ensureTimer();
641
- break;
642
- }
643
- }
644
- };
645
- const spinnerInterval = setInterval(() => {
646
- spinnerFrame++;
647
- streamUpdate();
648
- }, 80);
649
- streamUpdate();
650
- const record = await manager.spawnAndWait(pi, ctx, type, prompt, {
651
- description,
652
- maxTurns,
653
- modelInput,
654
- modelRegistry: ctx.modelRegistry,
655
- thinkingLevel,
656
- ...fgCallbacks,
657
- });
658
- clearInterval(spinnerInterval);
659
- // Clean up foreground agent from widget
660
- if (fgId) {
661
- agentActivity.delete(fgId);
662
- widget.markFinished(fgId);
663
- widget.update();
664
- }
665
- const tokenText = safeFormatTokens(fgState.session);
666
- const durationMs = (record.completedAt ?? Date.now()) - record.startedAt;
667
- if (record.status === "error") {
668
- return textResult(`Agent failed: ${record.error}`, {
669
- status: "error",
670
- toolUses: record.toolUses,
671
- tokens: tokenText,
672
- durationMs,
673
- error: record.error,
674
- });
675
- }
676
- const output = boundHelperOutput(record.result?.trim() || "No output.");
677
- record.resultArtifactPath = output.artifactPath;
678
- return textResult(`Agent completed in ${(durationMs / 1000).toFixed(1)}s (${record.toolUses} tool uses${tokenText ? `, ${tokenText} tokens` : ""}).\n\n` +
679
- output.text, {
680
- status: "completed",
681
- toolUses: record.toolUses,
682
- tokens: tokenText,
683
- durationMs,
684
- turnCount: fgState.turnCount,
685
- maxTurns: fgState.maxTurns,
686
- truncated: output.truncated,
687
- originalBytes: output.originalBytes,
688
- artifactPath: output.artifactPath,
689
- });
690
- },
691
- }));
692
- // ---- get_helper_result tool ----
693
- pi.registerTool(defineTool({
694
- name: "get_helper_result",
695
- label: "Get Helper Result",
696
- description: "Check status and retrieve results from a background agent. Use view: true to open a live conversation overlay.",
697
- parameters: Type.Object({
698
- agent_id: Type.String({
699
- description: "The helper ID to check.",
700
- }),
701
- wait: Type.Optional(Type.Boolean({
702
- description: "Wait for completion. Default: false.",
703
- })),
704
- view: Type.Optional(Type.Boolean({
705
- description: "Open a live conversation viewer overlay. Default: false.",
706
- })),
707
- }),
708
- execute: async (_toolCallId, params, _signal, _onUpdate, ctx) => {
709
- const record = manager.getRecord(params.agent_id);
710
- if (!record) {
711
- return textResult(`Helper not found: "${params.agent_id}". It may have been cleaned up.`);
712
- }
713
- // Open conversation viewer overlay if requested
714
- if (params.view && record.session) {
715
- const activity = agentActivity.get(record.id);
716
- await withHerdrBlocked(pi, "helper viewer", () => ctx.ui.custom((tui, theme, _keybindings, done) => {
717
- return new ConversationViewer(tui, record.session, {
718
- type: record.type,
719
- description: record.description,
720
- status: record.status,
721
- toolUses: record.toolUses,
722
- startedAt: record.startedAt,
723
- completedAt: record.completedAt,
724
- }, activity, theme, done);
725
- }, {
726
- overlay: true,
727
- overlayOptions: { anchor: "center", width: "90%" },
728
- }));
729
- }
730
- if (params.wait && record.status === "running" && record.promise) {
731
- record.resultConsumed = true;
732
- await record.promise;
733
- }
734
- const duration = record.completedAt
735
- ? `${((record.completedAt - record.startedAt) / 1000).toFixed(1)}s`
736
- : "running";
737
- let output = `Agent: ${record.id}\n` +
738
- `Type: ${record.type} | Status: ${record.status} | Tool uses: ${record.toolUses} | Duration: ${duration}\n` +
739
- `Description: ${record.description}\n\n`;
740
- if (record.status === "running") {
741
- output += "Agent is still running. Use wait: true or check back later.";
742
- }
743
- else if (record.status === "error") {
744
- output += `Error: ${record.error}`;
745
- }
746
- else {
747
- const bounded = boundHelperOutput(record.result?.trim() || "No output.", 64 * 1024, record.resultArtifactPath);
748
- record.resultArtifactPath = bounded.artifactPath;
749
- output += bounded.text;
750
- }
751
- if (record.status !== "running" && record.status !== "queued") {
752
- record.resultConsumed = true;
753
- }
754
- return textResult(output);
755
- },
756
- }));
757
- }
758
- //# sourceMappingURL=index.js.map