@ilya-lesikov/pi-pi 0.9.0 → 0.11.0
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/3p/pi-ask-user/index.ts +71 -124
- package/3p/pi-ask-user/single-select-layout.ts +3 -14
- package/3p/pi-subagents/package.json +12 -7
- package/3p/pi-subagents/src/agent-manager.ts +243 -79
- package/3p/pi-subagents/src/agent-runner.ts +501 -365
- package/3p/pi-subagents/src/agent-types.ts +46 -57
- package/3p/pi-subagents/src/cross-extension-rpc.ts +52 -46
- package/3p/pi-subagents/src/custom-agents.ts +30 -6
- package/3p/pi-subagents/src/default-agents.ts +25 -43
- package/3p/pi-subagents/src/enabled-models.ts +180 -0
- package/3p/pi-subagents/src/index.ts +599 -839
- package/3p/pi-subagents/src/model-resolver.ts +26 -7
- package/3p/pi-subagents/src/output-file.ts +21 -2
- package/3p/pi-subagents/src/prompts.ts +17 -3
- package/3p/pi-subagents/src/schedule-store.ts +153 -0
- package/3p/pi-subagents/src/schedule.ts +365 -0
- package/3p/pi-subagents/src/settings.ts +261 -0
- package/3p/pi-subagents/src/skill-loader.ts +77 -54
- package/3p/pi-subagents/src/status-note.ts +25 -0
- package/3p/pi-subagents/src/types.ts +97 -6
- package/3p/pi-subagents/src/ui/agent-widget.ts +94 -21
- package/3p/pi-subagents/src/ui/conversation-viewer.ts +147 -35
- package/3p/pi-subagents/src/ui/schedule-menu.ts +104 -0
- package/3p/pi-subagents/src/ui/viewer-keys.ts +39 -0
- package/3p/pi-subagents/src/usage.ts +60 -0
- package/3p/pi-subagents/src/worktree.ts +49 -20
- package/extensions/orchestrator/agents/advisor.ts +13 -8
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +8 -3
- package/extensions/orchestrator/agents/code-reviewer.ts +8 -3
- package/extensions/orchestrator/agents/constraints.test.ts +26 -0
- package/extensions/orchestrator/agents/constraints.ts +12 -2
- package/extensions/orchestrator/agents/deep-debugger.ts +13 -8
- package/extensions/orchestrator/agents/explore.ts +12 -6
- package/extensions/orchestrator/agents/librarian.ts +13 -5
- package/extensions/orchestrator/agents/plan-reviewer.ts +8 -3
- package/extensions/orchestrator/agents/planner.ts +8 -3
- package/extensions/orchestrator/agents/pools.test.ts +56 -0
- package/extensions/orchestrator/agents/prompts.test.ts +52 -10
- package/extensions/orchestrator/agents/registry.test.ts +245 -0
- package/extensions/orchestrator/agents/registry.ts +145 -10
- package/extensions/orchestrator/agents/reviewer.ts +14 -8
- package/extensions/orchestrator/agents/task.ts +12 -6
- package/extensions/orchestrator/agents/tool-routing.ts +213 -85
- package/extensions/orchestrator/agents/wait-for-completion.test.ts +171 -0
- package/extensions/orchestrator/ast-search.test.ts +124 -0
- package/extensions/orchestrator/billing-spoof.test.ts +67 -0
- package/extensions/orchestrator/billing-spoof.ts +95 -0
- package/extensions/orchestrator/cbm.more.test.ts +358 -0
- package/extensions/orchestrator/command-handlers.ts +6 -0
- package/extensions/orchestrator/commands.test.ts +15 -2
- package/extensions/orchestrator/commands.ts +1 -1
- package/extensions/orchestrator/config.test.ts +89 -1
- package/extensions/orchestrator/config.ts +102 -19
- package/extensions/orchestrator/context.test.ts +46 -0
- package/extensions/orchestrator/context.ts +18 -5
- package/extensions/orchestrator/custom-footer.test.ts +24 -10
- package/extensions/orchestrator/custom-footer.ts +4 -2
- package/extensions/orchestrator/doctor.more.test.ts +315 -0
- package/extensions/orchestrator/doctor.ts +6 -2
- package/extensions/orchestrator/event-handlers.more.test.ts +561 -0
- package/extensions/orchestrator/event-handlers.test.ts +96 -9
- package/extensions/orchestrator/event-handlers.ts +344 -151
- package/extensions/orchestrator/exa.more.test.ts +118 -0
- package/extensions/orchestrator/flant-infra.more.test.ts +381 -0
- package/extensions/orchestrator/flant-infra.test.ts +127 -0
- package/extensions/orchestrator/flant-infra.ts +126 -41
- package/extensions/orchestrator/index.test.ts +76 -0
- package/extensions/orchestrator/index.ts +2 -0
- package/extensions/orchestrator/integration.test.ts +183 -65
- package/extensions/orchestrator/model-registry.test.ts +2 -1
- package/extensions/orchestrator/model-registry.ts +12 -2
- package/extensions/orchestrator/orchestrator.test.ts +67 -0
- package/extensions/orchestrator/orchestrator.ts +119 -27
- package/extensions/orchestrator/phases/brainstorm.test.ts +30 -1
- package/extensions/orchestrator/phases/brainstorm.ts +43 -6
- package/extensions/orchestrator/phases/implementation.ts +2 -0
- package/extensions/orchestrator/phases/machine.test.ts +17 -1
- package/extensions/orchestrator/phases/machine.ts +4 -1
- package/extensions/orchestrator/phases/planning.test.ts +47 -1
- package/extensions/orchestrator/phases/planning.ts +18 -3
- package/extensions/orchestrator/phases/review-task.ts +5 -0
- package/extensions/orchestrator/phases/review.test.ts +10 -0
- package/extensions/orchestrator/phases/review.ts +22 -7
- package/extensions/orchestrator/phases/spawn-blocking.test.ts +1 -0
- package/extensions/orchestrator/phases/verdict.ts +6 -5
- package/extensions/orchestrator/plannotator-open-failure.test.ts +67 -0
- package/extensions/orchestrator/plannotator.test.ts +38 -1
- package/extensions/orchestrator/plannotator.ts +50 -3
- package/extensions/orchestrator/pp-menu.leaves.test.ts +590 -0
- package/extensions/orchestrator/pp-menu.more.test.ts +524 -0
- package/extensions/orchestrator/pp-menu.test.ts +114 -7
- package/extensions/orchestrator/pp-menu.ts +580 -91
- package/extensions/orchestrator/pp-state-tools.test.ts +80 -0
- package/extensions/orchestrator/pp-state-tools.ts +65 -0
- package/extensions/orchestrator/rate-limit-fallback.more.test.ts +241 -0
- package/extensions/orchestrator/rate-limit-fallback.test.ts +20 -1
- package/extensions/orchestrator/rate-limit-fallback.ts +12 -0
- package/extensions/orchestrator/review-files.test.ts +26 -0
- package/extensions/orchestrator/review-files.ts +3 -0
- package/extensions/orchestrator/state.test.ts +73 -1
- package/extensions/orchestrator/state.ts +95 -23
- package/extensions/orchestrator/suppress-pierre-theme-spam.test.ts +56 -0
- package/extensions/orchestrator/suppress-pierre-theme-spam.ts +45 -0
- package/extensions/orchestrator/validate-artifacts.ts +1 -1
- package/package.json +9 -2
- package/scripts/lib/smoke-resolve.mjs +99 -0
- package/scripts/test-3p.sh +52 -0
- package/scripts/test-package.sh +62 -0
|
@@ -10,26 +10,31 @@
|
|
|
10
10
|
* /agents — Interactive agent management menu
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { existsSync,
|
|
14
|
-
import { homedir } from "node:os";
|
|
13
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
15
14
|
import { join } from "node:path";
|
|
16
|
-
import type
|
|
17
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
15
|
+
import { defineTool, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { Container, Text } from "@earendil-works/pi-tui";
|
|
18
17
|
import { Type } from "@sinclair/typebox";
|
|
19
18
|
import { AgentManager } from "./agent-manager.js";
|
|
20
|
-
import { getAgentConversation, getDefaultMaxTurns,
|
|
21
|
-
import { BUILTIN_TOOL_NAMES,
|
|
19
|
+
import { getAgentConversation, getDefaultMaxTurns, normalizeMaxTurns, SUBAGENT_TOOL_NAMES, steerAgent } from "./agent-runner.js";
|
|
20
|
+
import { BUILTIN_TOOL_NAMES, clearExtensionAgents, getAgentConfig, getAvailableTypes, registerAgents, registerExtensionAgents, resolveType, setExtensionOnlyMode, unregisterExtensionAgents, unregisterExtensionAgentsByPrefix } from "./agent-types.js";
|
|
22
21
|
import { registerRpcHandlers } from "./cross-extension-rpc.js";
|
|
23
22
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
23
|
+
import { isModelInScope, readEnabledModels, resolveEnabledModels } from "./enabled-models.js";
|
|
24
24
|
import { GroupJoinManager } from "./group-join.js";
|
|
25
25
|
import { resolveAgentInvocationConfig, resolveJoinMode } from "./invocation-config.js";
|
|
26
|
-
import {
|
|
26
|
+
import { resolveModel } from "./model-resolver.js";
|
|
27
27
|
import { createOutputFilePath, streamToOutputFile, writeInitialEntry } from "./output-file.js";
|
|
28
|
-
import {
|
|
28
|
+
import { SubagentScheduler } from "./schedule.js";
|
|
29
|
+
import { resolveStorePath, ScheduleStore } from "./schedule-store.js";
|
|
30
|
+
import { type ToolDescriptionMode } from "./settings.js";
|
|
31
|
+
import { getStatusNote } from "./status-note.js";
|
|
32
|
+
import { type AgentConfig, type AgentInvocation, type AgentRecord, type JoinMode, type NotificationDetails, type SubagentType, type WidgetMode } from "./types.js";
|
|
29
33
|
import {
|
|
30
34
|
type AgentActivity,
|
|
31
35
|
type AgentDetails,
|
|
32
36
|
AgentWidget,
|
|
37
|
+
buildInvocationTags,
|
|
33
38
|
describeActivity,
|
|
34
39
|
formatDuration,
|
|
35
40
|
formatMs,
|
|
@@ -38,8 +43,10 @@ import {
|
|
|
38
43
|
getDisplayName,
|
|
39
44
|
getPromptModeLabel,
|
|
40
45
|
SPINNER,
|
|
46
|
+
type Theme,
|
|
41
47
|
type UICtx,
|
|
42
48
|
} from "./ui/agent-widget.js";
|
|
49
|
+
import { addUsage, getLifetimeTotal, getSessionContextPercent, type LifetimeUsage } from "./usage.js";
|
|
43
50
|
|
|
44
51
|
// ---- Shared helpers ----
|
|
45
52
|
|
|
@@ -48,28 +55,22 @@ function textResult(msg: string, details?: AgentDetails) {
|
|
|
48
55
|
return { content: [{ type: "text" as const, text: msg }], details: details as any };
|
|
49
56
|
}
|
|
50
57
|
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
export function renderRunningAgentStatus(
|
|
59
|
+
frame: string,
|
|
60
|
+
statsText: string,
|
|
61
|
+
activity: string,
|
|
62
|
+
theme: Pick<Theme, "fg">,
|
|
63
|
+
): Container {
|
|
64
|
+
const container = new Container();
|
|
65
|
+
container.addChild(new Text(theme.fg("accent", frame) + (statsText ? " " + statsText : ""), 0, 0));
|
|
66
|
+
container.addChild(new Text(theme.fg("dim", ` ⎿ ${activity}`), 0, 0));
|
|
67
|
+
return container;
|
|
58
68
|
}
|
|
59
69
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const line = JSON.stringify({ timestamp: new Date().toISOString(), event, ...detail }) + "\n";
|
|
65
|
-
require("node:fs").appendFileSync(join(cwd, ".pp", "subagent-interactive-trace.jsonl"), line, "utf-8");
|
|
66
|
-
} catch {}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Safe token formatting — wraps session.getSessionStats() in try-catch. */
|
|
70
|
-
function safeFormatTokens(session: { getSessionStats(): { tokens: { total: number } } } | undefined): string {
|
|
71
|
-
if (!session) return "";
|
|
72
|
-
try { return formatTokens(session.getSessionStats().tokens.total); } catch { return ""; }
|
|
70
|
+
/** Format an agent's lifetime token total, or "" when zero. */
|
|
71
|
+
function formatLifetimeTokens(o: { lifetimeUsage: LifetimeUsage }): string {
|
|
72
|
+
const t = getLifetimeTotal(o.lifetimeUsage);
|
|
73
|
+
return t > 0 ? formatTokens(t) : "";
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
/**
|
|
@@ -77,7 +78,15 @@ function safeFormatTokens(session: { getSessionStats(): { tokens: { total: numbe
|
|
|
77
78
|
* Used by both foreground and background paths to avoid duplication.
|
|
78
79
|
*/
|
|
79
80
|
function createActivityTracker(maxTurns?: number, onStreamUpdate?: () => void) {
|
|
80
|
-
const state: AgentActivity = {
|
|
81
|
+
const state: AgentActivity = {
|
|
82
|
+
activeTools: new Map(),
|
|
83
|
+
toolUses: 0,
|
|
84
|
+
turnCount: 1,
|
|
85
|
+
maxTurns,
|
|
86
|
+
responseText: "",
|
|
87
|
+
session: undefined,
|
|
88
|
+
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
|
|
89
|
+
};
|
|
81
90
|
|
|
82
91
|
const callbacks = {
|
|
83
92
|
onToolActivity: (activity: { type: "start" | "end"; toolName: string }) => {
|
|
@@ -89,7 +98,6 @@ function createActivityTracker(maxTurns?: number, onStreamUpdate?: () => void) {
|
|
|
89
98
|
}
|
|
90
99
|
state.toolUses++;
|
|
91
100
|
}
|
|
92
|
-
state.tokens = safeFormatTokens(state.session);
|
|
93
101
|
onStreamUpdate?.();
|
|
94
102
|
},
|
|
95
103
|
onTextDelta: (_delta: string, fullText: string) => {
|
|
@@ -103,6 +111,10 @@ function createActivityTracker(maxTurns?: number, onStreamUpdate?: () => void) {
|
|
|
103
111
|
onSessionCreated: (session: any) => {
|
|
104
112
|
state.session = session;
|
|
105
113
|
},
|
|
114
|
+
onAssistantUsage: (usage: { input: number; output: number; cacheWrite: number }) => {
|
|
115
|
+
addUsage(state.lifetimeUsage, usage);
|
|
116
|
+
onStreamUpdate?.();
|
|
117
|
+
},
|
|
106
118
|
};
|
|
107
119
|
|
|
108
120
|
return { state, callbacks };
|
|
@@ -119,16 +131,6 @@ function getStatusLabel(status: string, error?: string): string {
|
|
|
119
131
|
}
|
|
120
132
|
}
|
|
121
133
|
|
|
122
|
-
/** Parenthetical status note for completed agent result text. */
|
|
123
|
-
function getStatusNote(status: string): string {
|
|
124
|
-
switch (status) {
|
|
125
|
-
case "aborted": return " (aborted — max turns exceeded, output may be incomplete)";
|
|
126
|
-
case "steered": return " (wrapped up — reached turn limit)";
|
|
127
|
-
case "stopped": return " (stopped by user)";
|
|
128
|
-
default: return "";
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
134
|
/** Escape XML special characters to prevent injection in structured notifications. */
|
|
133
135
|
function escapeXml(s: string): string {
|
|
134
136
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -138,13 +140,10 @@ function escapeXml(s: string): string {
|
|
|
138
140
|
function formatTaskNotification(record: AgentRecord, resultMaxLen: number): string {
|
|
139
141
|
const status = getStatusLabel(record.status, record.error);
|
|
140
142
|
const durationMs = record.completedAt ? record.completedAt - record.startedAt : 0;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
totalTokens = stats.tokens?.total ?? 0;
|
|
146
|
-
}
|
|
147
|
-
} catch { /* session stats unavailable */ }
|
|
143
|
+
const totalTokens = getLifetimeTotal(record.lifetimeUsage);
|
|
144
|
+
const contextPercent = getSessionContextPercent(record.session);
|
|
145
|
+
const ctxXml = contextPercent !== null ? `<context_percent>${Math.round(contextPercent)}</context_percent>` : "";
|
|
146
|
+
const compactXml = record.compactionCount ? `<compactions>${record.compactionCount}</compactions>` : "";
|
|
148
147
|
|
|
149
148
|
const resultPreview = record.result
|
|
150
149
|
? record.result.length > resultMaxLen
|
|
@@ -158,9 +157,9 @@ function formatTaskNotification(record: AgentRecord, resultMaxLen: number): stri
|
|
|
158
157
|
record.toolCallId ? `<tool-use-id>${escapeXml(record.toolCallId)}</tool-use-id>` : null,
|
|
159
158
|
record.outputFile ? `<output-file>${escapeXml(record.outputFile)}</output-file>` : null,
|
|
160
159
|
`<status>${escapeXml(status)}</status>`,
|
|
161
|
-
`<summary>Agent "${escapeXml(record.description)}" ${record.status}</summary>`,
|
|
160
|
+
`<summary>Agent "${escapeXml(record.description)}" ${record.status}${getStatusNote(record.status)}</summary>`,
|
|
162
161
|
`<result>${escapeXml(resultPreview)}</result>`,
|
|
163
|
-
`<usage><total_tokens>${totalTokens}</total_tokens><tool_uses>${record.toolUses}</tool_uses
|
|
162
|
+
`<usage><total_tokens>${totalTokens}</total_tokens><tool_uses>${record.toolUses}</tool_uses>${ctxXml}${compactXml}<duration_ms>${durationMs}</duration_ms></usage>`,
|
|
164
163
|
`</task-notification>`,
|
|
165
164
|
].filter(Boolean).join('\n');
|
|
166
165
|
}
|
|
@@ -168,14 +167,14 @@ function formatTaskNotification(record: AgentRecord, resultMaxLen: number): stri
|
|
|
168
167
|
/** Build AgentDetails from a base + record-specific fields. */
|
|
169
168
|
function buildDetails(
|
|
170
169
|
base: Pick<AgentDetails, "displayName" | "description" | "subagentType" | "modelName" | "tags">,
|
|
171
|
-
record: { toolUses: number; startedAt: number; completedAt?: number; status: string; error?: string; id?: string; session?: any },
|
|
170
|
+
record: { toolUses: number; startedAt: number; completedAt?: number; status: string; error?: string; id?: string; session?: any; lifetimeUsage: LifetimeUsage },
|
|
172
171
|
activity?: AgentActivity,
|
|
173
172
|
overrides?: Partial<AgentDetails>,
|
|
174
173
|
): AgentDetails {
|
|
175
174
|
return {
|
|
176
175
|
...base,
|
|
177
176
|
toolUses: record.toolUses,
|
|
178
|
-
tokens:
|
|
177
|
+
tokens: formatLifetimeTokens(record),
|
|
179
178
|
turnCount: activity?.turnCount,
|
|
180
179
|
maxTurns: activity?.maxTurns,
|
|
181
180
|
durationMs: (record.completedAt ?? Date.now()) - record.startedAt,
|
|
@@ -188,10 +187,7 @@ function buildDetails(
|
|
|
188
187
|
|
|
189
188
|
/** Build notification details for the custom message renderer. */
|
|
190
189
|
function buildNotificationDetails(record: AgentRecord, resultMaxLen: number, activity?: AgentActivity): NotificationDetails {
|
|
191
|
-
|
|
192
|
-
try {
|
|
193
|
-
if (record.session) totalTokens = record.session.getSessionStats().tokens?.total ?? 0;
|
|
194
|
-
} catch {}
|
|
190
|
+
const totalTokens = getLifetimeTotal(record.lifetimeUsage);
|
|
195
191
|
|
|
196
192
|
return {
|
|
197
193
|
id: record.id,
|
|
@@ -262,16 +258,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
262
258
|
}
|
|
263
259
|
);
|
|
264
260
|
|
|
265
|
-
|
|
261
|
+
// The Agent tool's `subagent_type` description enumerates the currently
|
|
262
|
+
// available agent types. It is built once at tool registration, but sibling
|
|
263
|
+
// extensions (e.g. pi-pi) register dynamic agents LATER via
|
|
264
|
+
// `subagents:register-agents`. The host reads `parameters.…description` by
|
|
265
|
+
// reference fresh on each request, so mutating this field in place refreshes
|
|
266
|
+
// the advertised list without re-registering the tool.
|
|
267
|
+
let subagentTypeSchema: { description?: string } | undefined;
|
|
268
|
+
const buildSubagentTypeDesc = (): string =>
|
|
269
|
+
`The type of specialized agent to use. Available types: ${getAvailableTypes().join(", ")}. Custom agents from .pi/agents/*.md (project) or ${getAgentDir()}/agents/*.md (global) are also available.`;
|
|
266
270
|
|
|
267
271
|
/** Reload agents from .pi/agents/*.md and merge with defaults (called on init and each Agent invocation). */
|
|
268
|
-
const getProjectCwd = () => currentProjectCwd;
|
|
269
|
-
|
|
270
272
|
const reloadCustomAgents = () => {
|
|
271
|
-
const
|
|
272
|
-
if (!cwd) return;
|
|
273
|
-
const userAgents = loadCustomAgents(cwd);
|
|
273
|
+
const userAgents = loadCustomAgents(process.cwd());
|
|
274
274
|
registerAgents(userAgents);
|
|
275
|
+
if (subagentTypeSchema) subagentTypeSchema.description = buildSubagentTypeDesc();
|
|
275
276
|
};
|
|
276
277
|
|
|
277
278
|
// Initial load
|
|
@@ -290,7 +291,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
290
291
|
cancelNudge(key);
|
|
291
292
|
pendingNudges.set(key, setTimeout(() => {
|
|
292
293
|
pendingNudges.delete(key);
|
|
293
|
-
send();
|
|
294
|
+
try { send(); } catch { /* ignore stale completion side-effect errors */ }
|
|
294
295
|
}, delay));
|
|
295
296
|
}
|
|
296
297
|
|
|
@@ -306,17 +307,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
306
307
|
function emitIndividualNudge(record: AgentRecord) {
|
|
307
308
|
if (record.resultConsumed) return; // re-check at send time
|
|
308
309
|
|
|
309
|
-
const shouldTriggerTurn = !manager.hasRunning();
|
|
310
310
|
const notification = formatTaskNotification(record, 500);
|
|
311
|
-
const footer = record.outputFile ? `
|
|
312
|
-
Full transcript available at: ${record.outputFile}` : '';
|
|
311
|
+
const footer = record.outputFile ? `\nFull transcript available at: ${record.outputFile}` : '';
|
|
313
312
|
|
|
314
313
|
pi.sendMessage<NotificationDetails>({
|
|
315
314
|
customType: "subagent-notification",
|
|
316
315
|
content: notification + footer,
|
|
317
316
|
display: true,
|
|
318
317
|
details: buildNotificationDetails(record, 500, agentActivity.get(record.id)),
|
|
319
|
-
}, { deliverAs: "followUp", triggerTurn:
|
|
318
|
+
}, { deliverAs: "followUp", triggerTurn: true });
|
|
320
319
|
}
|
|
321
320
|
|
|
322
321
|
function sendIndividualNudge(record: AgentRecord) {
|
|
@@ -348,17 +347,12 @@ Full transcript available at: ${record.outputFile}` : '';
|
|
|
348
347
|
details.others = rest.map(r => buildNotificationDetails(r, 300, agentActivity.get(r.id)));
|
|
349
348
|
}
|
|
350
349
|
|
|
351
|
-
const shouldTriggerTurn = !partial && !manager.hasRunning();
|
|
352
350
|
pi.sendMessage<NotificationDetails>({
|
|
353
351
|
customType: "subagent-notification",
|
|
354
|
-
content: `Background agent group completed: ${label}
|
|
355
|
-
|
|
356
|
-
${notifications}
|
|
357
|
-
|
|
358
|
-
Use get_subagent_result for full output.`,
|
|
352
|
+
content: `Background agent group completed: ${label}\n\n${notifications}\n\nUse get_subagent_result for full output.`,
|
|
359
353
|
display: true,
|
|
360
354
|
details,
|
|
361
|
-
}, { deliverAs: "followUp", triggerTurn:
|
|
355
|
+
}, { deliverAs: "followUp", triggerTurn: true });
|
|
362
356
|
});
|
|
363
357
|
widget.update();
|
|
364
358
|
},
|
|
@@ -368,24 +362,15 @@ Use get_subagent_result for full output.`,
|
|
|
368
362
|
/** Helper: build event data for lifecycle events from an AgentRecord. */
|
|
369
363
|
function buildEventData(record: AgentRecord) {
|
|
370
364
|
const durationMs = record.completedAt ? record.completedAt - record.startedAt : Date.now() - record.startedAt;
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
total: stats.tokens?.total ?? 0,
|
|
381
|
-
cost: typeof stats.cost === "number" ? stats.cost : 0,
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
} catch { /* session stats unavailable */ }
|
|
385
|
-
let modelId: string | undefined;
|
|
386
|
-
try {
|
|
387
|
-
modelId = record.session?.model?.id;
|
|
388
|
-
} catch {}
|
|
365
|
+
// All three fields are lifetime-accumulated (Σ over every assistant message_end),
|
|
366
|
+
// so they survive compaction together — input + output ≤ total always.
|
|
367
|
+
// tokens is omitted when nothing was ever produced (e.g. agent errored before
|
|
368
|
+
// any message_end fired), preserving prior payload shape.
|
|
369
|
+
const u = record.lifetimeUsage;
|
|
370
|
+
const total = getLifetimeTotal(u);
|
|
371
|
+
const tokens = total > 0
|
|
372
|
+
? { input: u.input, output: u.output, total }
|
|
373
|
+
: undefined;
|
|
389
374
|
return {
|
|
390
375
|
id: record.id,
|
|
391
376
|
type: record.type,
|
|
@@ -395,39 +380,27 @@ Use get_subagent_result for full output.`,
|
|
|
395
380
|
status: record.status,
|
|
396
381
|
toolUses: record.toolUses,
|
|
397
382
|
durationMs,
|
|
398
|
-
modelId,
|
|
399
383
|
tokens,
|
|
400
|
-
toolCallId: record.toolCallId,
|
|
401
384
|
};
|
|
402
385
|
}
|
|
403
386
|
|
|
404
387
|
// Background completion: route through group join or send individual nudge
|
|
405
388
|
const manager = new AgentManager((record) => {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
try {
|
|
414
|
-
if (isError) {
|
|
415
|
-
pi.events.emit("subagents:failed", eventData);
|
|
416
|
-
} else {
|
|
417
|
-
pi.events.emit("subagents:completed", eventData);
|
|
418
|
-
}
|
|
419
|
-
} catch (error) {
|
|
420
|
-
}
|
|
389
|
+
// Emit lifecycle event based on terminal status
|
|
390
|
+
const isError = record.status === "error" || record.status === "stopped" || record.status === "aborted";
|
|
391
|
+
const eventData = buildEventData(record);
|
|
392
|
+
if (isError) {
|
|
393
|
+
pi.events.emit("subagents:failed", eventData);
|
|
394
|
+
} else {
|
|
395
|
+
pi.events.emit("subagents:completed", eventData);
|
|
421
396
|
}
|
|
422
397
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
} catch (error) {
|
|
430
|
-
}
|
|
398
|
+
// Persist final record for cross-extension history reconstruction
|
|
399
|
+
pi.appendEntry("subagents:record", {
|
|
400
|
+
id: record.id, type: record.type, description: record.description,
|
|
401
|
+
status: record.status, result: record.result, error: record.error,
|
|
402
|
+
startedAt: record.startedAt, completedAt: record.completedAt,
|
|
403
|
+
});
|
|
431
404
|
|
|
432
405
|
// Skip notification if result was already consumed via get_subagent_result
|
|
433
406
|
if (record.resultConsumed) {
|
|
@@ -453,62 +426,113 @@ Use get_subagent_result for full output.`,
|
|
|
453
426
|
widget.update();
|
|
454
427
|
}, undefined, (record) => {
|
|
455
428
|
// Emit started event when agent transitions to running (including from queue)
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
429
|
+
pi.events.emit("subagents:started", {
|
|
430
|
+
id: record.id,
|
|
431
|
+
type: record.type,
|
|
432
|
+
description: record.description,
|
|
433
|
+
});
|
|
434
|
+
}, (record, info) => {
|
|
435
|
+
// Emit compacted event when agent's session compacts (preserves count on record).
|
|
436
|
+
pi.events.emit("subagents:compacted", {
|
|
437
|
+
id: record.id,
|
|
438
|
+
type: record.type,
|
|
439
|
+
description: record.description,
|
|
440
|
+
reason: info.reason,
|
|
441
|
+
tokensBefore: info.tokensBefore,
|
|
442
|
+
compactionCount: record.compactionCount,
|
|
443
|
+
});
|
|
463
444
|
});
|
|
464
445
|
|
|
465
|
-
// Expose manager via Symbol.for() global registry for
|
|
466
|
-
// Standard Node.js pattern for cross-package singletons
|
|
446
|
+
// Expose manager + running-agents menu via Symbol.for() global registry for
|
|
447
|
+
// cross-package access. Standard Node.js pattern for cross-package singletons
|
|
448
|
+
// (used by OpenTelemetry, etc.).
|
|
467
449
|
const MANAGER_KEY = Symbol.for("pi-subagents:manager");
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
450
|
+
const MENU_KEY = Symbol.for("pi-subagents:menu");
|
|
451
|
+
// Nested in-process subagent sessions re-run this factory (the resource loader
|
|
452
|
+
// disables module caching, so every session's reload() re-executes extension
|
|
453
|
+
// factories). Only the first/owning invocation may publish these globals: a
|
|
454
|
+
// nested re-run must leave them pointing at the root session's manager. If it
|
|
455
|
+
// overwrote them with its own fresh, empty manager, `/pp > Subagents` (which
|
|
456
|
+
// reads globalThis[MENU_KEY]) would bind to that empty manager while the real
|
|
457
|
+
// agents live in the original one — the "background agents never show" bug.
|
|
458
|
+
const ownsGlobals = (globalThis as any)[MANAGER_KEY] === undefined;
|
|
459
|
+
if (ownsGlobals) {
|
|
460
|
+
(globalThis as any)[MANAGER_KEY] = {
|
|
461
|
+
waitForAll: () => manager.waitForAll(),
|
|
462
|
+
hasRunning: () => manager.hasRunning(),
|
|
463
|
+
spawn: (piRef: any, ctx: any, type: string, prompt: string, options: any) =>
|
|
464
|
+
manager.spawn(piRef, ctx, type, prompt, options),
|
|
465
|
+
getRecord: (id: string) => manager.getRecord(id),
|
|
466
|
+
// Refresh the above-editor widget from outside an LLM turn. Agents spawned
|
|
467
|
+
// via cross-extension RPC (e.g. pi-pi orchestrator planners/reviewers)
|
|
468
|
+
// start while the main agent is only waiting, so no tool_execution_start
|
|
469
|
+
// fires to hand the widget a UICtx. Callers pass their own ctx.ui so the
|
|
470
|
+
// widget can register and render. Restored after the v0.13.0 subtree
|
|
471
|
+
// update dropped this local patch.
|
|
472
|
+
refreshWidget: (uiCtx?: any) => {
|
|
473
|
+
if (uiCtx) widget.setUICtx(uiCtx as UICtx);
|
|
474
|
+
widget.ensureTimer();
|
|
475
|
+
widget.update();
|
|
476
|
+
},
|
|
477
|
+
// Runtime control of the background-concurrency limit from outside this
|
|
478
|
+
// package (pi-pi orchestrator applies its configured
|
|
479
|
+
// agents.maxConcurrentSubagents here). setMaxConcurrent clamps >=1 and
|
|
480
|
+
// drains the queue, so raising it mid-session starts queued agents
|
|
481
|
+
// immediately. Local patch — restore after a subtree update drops it.
|
|
482
|
+
setMaxConcurrent: (n: number) => manager.setMaxConcurrent(n),
|
|
483
|
+
getMaxConcurrent: () => manager.getMaxConcurrent(),
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
(globalThis as any)[MENU_KEY] = {
|
|
487
|
+
showFleet: (ctx: any) => showRunningAgents(ctx),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
479
490
|
|
|
480
491
|
// --- Cross-extension RPC via pi.events ---
|
|
481
492
|
let currentCtx: ExtensionContext | undefined;
|
|
482
493
|
|
|
483
|
-
//
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
494
|
+
// ---- Subagent scheduler ----
|
|
495
|
+
// Session-scoped: store is constructed inside session_start once sessionId
|
|
496
|
+
// is available. Mirrors pi-chonky-tasks's session-scoped task store —
|
|
497
|
+
// schedules reset on /new, restore on /resume.
|
|
498
|
+
const scheduler = new SubagentScheduler();
|
|
499
|
+
|
|
500
|
+
function startScheduler(ctx: ExtensionContext) {
|
|
501
|
+
try {
|
|
502
|
+
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
503
|
+
if (!sessionId) return; // sessionId not yet available — try again on next event
|
|
504
|
+
const path = resolveStorePath(ctx.cwd, sessionId);
|
|
505
|
+
const store = new ScheduleStore(path);
|
|
506
|
+
scheduler.start(pi, ctx, manager, store);
|
|
507
|
+
pi.events.emit("subagents:scheduler_ready", { sessionId, jobCount: store.list().length });
|
|
508
|
+
} catch (err) {
|
|
509
|
+
// Scheduling is non-essential — log and move on so the rest of the
|
|
510
|
+
// extension keeps working if e.g. .pi/ is unwritable.
|
|
511
|
+
console.warn("[pi-subagents] Failed to start scheduler:", err);
|
|
489
512
|
}
|
|
490
|
-
|
|
491
|
-
});
|
|
513
|
+
}
|
|
492
514
|
|
|
493
|
-
|
|
494
|
-
pi.
|
|
495
|
-
|
|
515
|
+
// Capture ctx from session_start for RPC spawn handler + start the scheduler.
|
|
516
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
517
|
+
currentCtx = ctx;
|
|
518
|
+
manager.clearCompleted(true);
|
|
519
|
+
if (isSchedulingEnabled() && !scheduler.isActive()) startScheduler(ctx);
|
|
496
520
|
});
|
|
497
521
|
|
|
498
|
-
pi.on("
|
|
499
|
-
|
|
522
|
+
pi.on("session_before_switch", () => {
|
|
523
|
+
manager.clearCompleted(true);
|
|
524
|
+
scheduler.stop();
|
|
500
525
|
});
|
|
501
526
|
|
|
502
527
|
const { unsubPing: unsubPingRpc, unsubSpawn: unsubSpawnRpc, unsubStop: unsubStopRpc } = registerRpcHandlers({
|
|
503
528
|
events: pi.events,
|
|
504
529
|
pi,
|
|
505
530
|
getCtx: () => currentCtx,
|
|
506
|
-
isSubagentSession: (ctx: unknown) => Boolean((ctx as any)?.sessionManager?.getEntries?.().some?.((entry: any) => entry?.type === "custom" && entry?.customType === "subagent-session")),
|
|
507
531
|
manager,
|
|
508
532
|
});
|
|
509
533
|
|
|
510
|
-
// Allow other extensions to take full control of agent registry
|
|
511
|
-
pi.events.on("subagents:set-extension-only", (data: any) => {
|
|
534
|
+
// Allow other extensions to take full control of the agent registry
|
|
535
|
+
const unsubExtensionOnly = pi.events.on("subagents:set-extension-only", (data: any) => {
|
|
512
536
|
if (typeof data?.enabled === "boolean") {
|
|
513
537
|
setExtensionOnlyMode(data.enabled);
|
|
514
538
|
reloadCustomAgents();
|
|
@@ -516,21 +540,21 @@ Use get_subagent_result for full output.`,
|
|
|
516
540
|
});
|
|
517
541
|
|
|
518
542
|
// Allow other extensions to register in-memory agent types
|
|
519
|
-
pi.events.on("subagents:register-agents", (data: any) => {
|
|
520
|
-
if (data?.agents
|
|
543
|
+
const unsubRegisterAgents = pi.events.on("subagents:register-agents", (data: any) => {
|
|
544
|
+
if (data?.agents instanceof Map) {
|
|
521
545
|
registerExtensionAgents(data.agents);
|
|
522
546
|
reloadCustomAgents();
|
|
523
547
|
}
|
|
524
548
|
});
|
|
525
549
|
|
|
526
|
-
pi.events.on("subagents:unregister-agents", (data: any) => {
|
|
550
|
+
const unsubUnregisterAgents = pi.events.on("subagents:unregister-agents", (data: any) => {
|
|
527
551
|
if (data?.all === true) {
|
|
528
552
|
clearExtensionAgents();
|
|
529
553
|
reloadCustomAgents();
|
|
530
|
-
} else if (
|
|
554
|
+
} else if (Array.isArray(data?.names)) {
|
|
531
555
|
unregisterExtensionAgents(data.names);
|
|
532
556
|
reloadCustomAgents();
|
|
533
|
-
} else if (
|
|
557
|
+
} else if (typeof data?.prefix === "string") {
|
|
534
558
|
unregisterExtensionAgentsByPrefix(data.prefix);
|
|
535
559
|
reloadCustomAgents();
|
|
536
560
|
}
|
|
@@ -539,20 +563,57 @@ Use get_subagent_result for full output.`,
|
|
|
539
563
|
// Broadcast readiness so extensions loaded after us can discover us
|
|
540
564
|
pi.events.emit("subagents:ready", {});
|
|
541
565
|
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
// background sessions can finish and still be surfaced in the next parent session.
|
|
566
|
+
// On shutdown, abort all agents immediately and clean up.
|
|
567
|
+
// If the session is going down, there's nothing left to consume agent results.
|
|
545
568
|
pi.on("session_shutdown", async () => {
|
|
546
|
-
|
|
569
|
+
unsubSpawnRpc();
|
|
570
|
+
unsubStopRpc();
|
|
571
|
+
unsubPingRpc();
|
|
572
|
+
unsubExtensionOnly();
|
|
573
|
+
unsubRegisterAgents();
|
|
574
|
+
unsubUnregisterAgents();
|
|
575
|
+
currentCtx = undefined;
|
|
576
|
+
// Only the owning (root) invocation may retract the shared globals; a nested
|
|
577
|
+
// subagent session shutting down must not delete the root session's manager.
|
|
578
|
+
if (ownsGlobals) {
|
|
579
|
+
delete (globalThis as any)[MANAGER_KEY];
|
|
580
|
+
delete (globalThis as any)[MENU_KEY];
|
|
581
|
+
}
|
|
582
|
+
scheduler.stop();
|
|
583
|
+
manager.abortAll();
|
|
584
|
+
for (const timer of pendingNudges.values()) clearTimeout(timer);
|
|
585
|
+
pendingNudges.clear();
|
|
586
|
+
manager.dispose();
|
|
547
587
|
});
|
|
548
588
|
|
|
549
|
-
// Live widget: show running agents above editor
|
|
550
|
-
|
|
589
|
+
// Live widget: show running agents above editor.
|
|
590
|
+
// widgetMode (default "background") selects what the widget shows: "all" =
|
|
591
|
+
// every agent; "background" = hide foreground (they already render inline as
|
|
592
|
+
// the Agent tool result, so showing them here too is a duplicate, #118), keep
|
|
593
|
+
// everything else; "off" = hide the widget entirely. Read live at render time.
|
|
594
|
+
const widgetMode: WidgetMode = "background";
|
|
595
|
+
function getWidgetMode(): WidgetMode { return widgetMode; }
|
|
596
|
+
const widget = new AgentWidget(manager, agentActivity, getWidgetMode);
|
|
551
597
|
|
|
552
598
|
// ---- Join mode configuration ----
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
599
|
+
const defaultJoinMode: JoinMode = 'smart';
|
|
600
|
+
|
|
601
|
+
// Master switch for the schedule subagent feature.
|
|
602
|
+
const schedulingEnabled = true;
|
|
603
|
+
function isSchedulingEnabled(): boolean { return schedulingEnabled; }
|
|
604
|
+
|
|
605
|
+
// ---- Scope models configuration ----
|
|
606
|
+
// When enabled, subagent model choices are validated against `enabledModels`
|
|
607
|
+
// from pi's settings — both global `<agentDir>/settings.json` and
|
|
608
|
+
// project-local `<cwd>/.pi/settings.json` (project overrides global).
|
|
609
|
+
const scopeModelsEnabled = false;
|
|
610
|
+
function isScopeModelsEnabled(): boolean { return scopeModelsEnabled; }
|
|
611
|
+
|
|
612
|
+
// ---- Agent tool description mode ----
|
|
613
|
+
// "full" keeps the rich Claude Code-style description; read once at tool
|
|
614
|
+
// registration.
|
|
615
|
+
const toolDescriptionMode: ToolDescriptionMode = "full";
|
|
616
|
+
function getToolDescriptionMode(): ToolDescriptionMode { return toolDescriptionMode; }
|
|
556
617
|
|
|
557
618
|
// ---- Batch tracking for smart join mode ----
|
|
558
619
|
// Collects background agent IDs spawned in the current turn for smart grouping.
|
|
@@ -599,43 +660,45 @@ Use get_subagent_result for full output.`,
|
|
|
599
660
|
|
|
600
661
|
// Grab UI context from first tool execution + clear lingering widget on new turn
|
|
601
662
|
pi.on("tool_execution_start", async (_event, ctx) => {
|
|
602
|
-
const hadUiCtx = Boolean((widget as any).uiCtx);
|
|
603
663
|
widget.setUICtx(ctx.ui as UICtx);
|
|
604
|
-
if (!hadUiCtx) widget.update();
|
|
605
664
|
widget.onTurnStart();
|
|
606
665
|
});
|
|
607
666
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
if (!
|
|
612
|
-
|
|
667
|
+
/** Format an agent's tool scope: "*" when it has all built-ins, else a comma-separated list. */
|
|
668
|
+
const formatToolsSuffix = (cfg: AgentConfig | undefined): string => {
|
|
669
|
+
const tools = cfg?.builtinToolNames;
|
|
670
|
+
if (!tools || tools.length === 0) return "*";
|
|
671
|
+
const isFullSet =
|
|
672
|
+
tools.length === BUILTIN_TOOL_NAMES.length
|
|
673
|
+
&& BUILTIN_TOOL_NAMES.every((t) => tools.includes(t));
|
|
674
|
+
return isFullSet ? "*" : tools.join(", ");
|
|
675
|
+
};
|
|
613
676
|
|
|
614
|
-
/** Build the full type list text dynamically from
|
|
677
|
+
/** Build the full type list text dynamically from available agents only. */
|
|
615
678
|
const buildTypeListText = () => {
|
|
616
|
-
const
|
|
617
|
-
const userNames = getUserAgentNames();
|
|
679
|
+
const available = getAvailableTypes();
|
|
618
680
|
|
|
619
|
-
|
|
681
|
+
return available.map((name) => {
|
|
620
682
|
const cfg = getAgentConfig(name);
|
|
621
683
|
const modelSuffix = cfg?.model ? ` (${getModelLabelFromConfig(cfg.model)})` : "";
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
const cfg = getAgentConfig(name);
|
|
627
|
-
return `- ${name}: ${cfg?.description ?? name}`;
|
|
628
|
-
});
|
|
684
|
+
const toolsSuffix = ` (Tools: ${formatToolsSuffix(cfg)})`;
|
|
685
|
+
return `- ${name}: ${cfg?.description ?? name}${modelSuffix}${toolsSuffix}`;
|
|
686
|
+
}).join("\n");
|
|
687
|
+
};
|
|
629
688
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
"",
|
|
635
|
-
"Custom agents can be defined in .pi/agents/<name>.md (project) or ~/.pi/agent/agents/<name>.md (global) — they are picked up automatically. Project-level agents override global ones. Creating a .md file with the same name as a default agent overrides it.",
|
|
636
|
-
].join("\n");
|
|
689
|
+
/** First sentence of an agent description — for the compact type list. */
|
|
690
|
+
const firstSentence = (text: string): string => {
|
|
691
|
+
const match = text.match(/^.*?[.!?](?=\s|$)/s);
|
|
692
|
+
return (match ? match[0] : text).replace(/\s+/g, " ").trim();
|
|
637
693
|
};
|
|
638
694
|
|
|
695
|
+
/** Compact type list: one line per agent, first sentence only. */
|
|
696
|
+
const buildCompactTypeListText = () =>
|
|
697
|
+
getAvailableTypes().map((name) => {
|
|
698
|
+
const cfg = getAgentConfig(name);
|
|
699
|
+
return `- ${name}: ${firstSentence(cfg?.description ?? name)} (Tools: ${formatToolsSuffix(cfg)})`;
|
|
700
|
+
}).join("\n");
|
|
701
|
+
|
|
639
702
|
/** Derive a short model label from a model string. */
|
|
640
703
|
function getModelLabelFromConfig(model: string): string {
|
|
641
704
|
// Strip provider prefix (e.g. "anthropic/claude-sonnet-4-6" → "claude-sonnet-4-6")
|
|
@@ -644,34 +707,148 @@ Use get_subagent_result for full output.`,
|
|
|
644
707
|
return name.replace(/-\d{8}$/, "");
|
|
645
708
|
}
|
|
646
709
|
|
|
647
|
-
const typeListText = buildTypeListText();
|
|
648
|
-
|
|
649
710
|
// ---- Agent tool ----
|
|
650
711
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
712
|
+
// Schedule param + its guideline are gated on `schedulingEnabled` (read once
|
|
713
|
+
// at registration; flipping the setting later requires next pi session for
|
|
714
|
+
// the schema to update). Defining the shape once and spreading it via Partial
|
|
715
|
+
// preserves Type.Object's inference when present and produces a
|
|
716
|
+
// `schedule`-free schema when absent — zero LLM-context cost in disabled mode.
|
|
717
|
+
const scheduleParamShape = {
|
|
718
|
+
schedule: Type.Optional(
|
|
719
|
+
Type.String({
|
|
720
|
+
description:
|
|
721
|
+
'Opt-in only — fire later instead of now. Omit to run immediately (the default, almost always correct). ' +
|
|
722
|
+
'Formats: 6-field cron ("0 0 9 * * 1" = 9am Mon), interval ("5m"/"1h"), one-shot ("+10m" or ISO). ' +
|
|
723
|
+
'Forces run_in_background; incompatible with inherit_context and resume. Returns job ID.',
|
|
724
|
+
}),
|
|
725
|
+
),
|
|
726
|
+
};
|
|
727
|
+
const scheduleParam: Partial<typeof scheduleParamShape> =
|
|
728
|
+
isSchedulingEnabled() ? scheduleParamShape : {};
|
|
729
|
+
|
|
730
|
+
const scheduleGuideline = isSchedulingEnabled()
|
|
731
|
+
? `\n- Use \`schedule\` only when the user explicitly asked for scheduled / recurring / delayed execution (e.g. "every Monday", "in an hour"). Don't auto-schedule from vague intent like "monitor X" — run once now or ask.`
|
|
732
|
+
: "";
|
|
733
|
+
|
|
734
|
+
// Compact Agent tool description (#91, `toolDescriptionMode: "compact"`) —
|
|
735
|
+
// the same load-bearing facts as the full version at ~75% fewer tokens, for
|
|
736
|
+
// small/local models. Per-option details live in the param descriptions.
|
|
737
|
+
const compactAgentToolDescription = `Launch an autonomous agent for complex, multi-step tasks. Agent types:
|
|
738
|
+
${buildCompactTypeListText()}
|
|
739
|
+
|
|
740
|
+
Custom agents: .pi/agents/<name>.md (project) or ${getAgentDir()}/agents/<name>.md (global).
|
|
741
|
+
|
|
742
|
+
Notes:
|
|
743
|
+
- description: 3-5 words (shown in UI). Prompts must be self-contained — the agent has not seen this conversation.
|
|
744
|
+
- Parallel work: one message, multiple Agent calls, run_in_background: true on each. You are notified when background agents finish — never poll or sleep.
|
|
745
|
+
- The result is not shown to the user — summarize it for them. Verify an agent's claimed code changes before reporting work done.
|
|
746
|
+
- resume continues a previous agent by ID; steer_subagent messages a running one.
|
|
747
|
+
- isolation: "worktree" runs the agent in an isolated git worktree; changes land on a branch.`;
|
|
748
|
+
|
|
749
|
+
const fullAgentToolDescription = `Launch a new agent to handle complex, multi-step tasks autonomously. Each agent type has specific capabilities and tools available to it.
|
|
750
|
+
|
|
751
|
+
Available agent types and the tools they have access to:
|
|
752
|
+
${buildTypeListText()}
|
|
753
|
+
|
|
754
|
+
Additional agent types may be registered dynamically at runtime — see the subagent_type parameter for the authoritative, always-current list.
|
|
755
|
+
|
|
756
|
+
Custom agents can be defined in .pi/agents/<name>.md (project) or ${getAgentDir()}/agents/<name>.md (global) — they are picked up automatically. Project-level agents override global ones. Creating a .md file with the same name as a default agent overrides it.
|
|
757
|
+
|
|
758
|
+
When using the Agent tool, specify a subagent_type parameter to select which agent type to use.
|
|
759
|
+
|
|
760
|
+
## When not to use
|
|
761
|
+
|
|
762
|
+
If the target is already known, use a direct tool — \`read\` for a known path, \`grep\`/\`find\` for a specific symbol or string. Reserve this tool for open-ended questions that span the codebase, or tasks that match an available agent type.
|
|
763
|
+
|
|
764
|
+
## Usage notes
|
|
765
|
+
|
|
766
|
+
- Always include a short (3-5 word) description summarizing what the agent will do (shown in UI).
|
|
767
|
+
- When you launch multiple agents for independent work, send them in a single message with multiple tool uses, with run_in_background: true on each, so they run concurrently. If the user specifies that they want agents run "in parallel", you MUST send a single message with multiple tool calls. Foreground calls run sequentially — only one executes at a time.
|
|
768
|
+
- When the agent is done, it returns a single message back to you. The result is not visible to the user — to show the user, send a text message with a concise summary.
|
|
769
|
+
- Trust but verify: an agent's summary describes what it intended to do, not necessarily what it did. When an agent writes or edits code, check the actual changes before reporting work as done.
|
|
770
|
+
- Use run_in_background for work you don't need immediately. You will be notified when it completes — do NOT poll or sleep waiting for it. Continue with other work or respond to the user instead.
|
|
771
|
+
- Foreground vs background: use foreground (default) when you need the agent's results before you can proceed. Use background when you have genuinely independent work to do in parallel.
|
|
772
|
+
- Use resume with an agent ID to continue a previous agent's work. A new (non-resume) Agent call starts a fresh agent with no memory of prior runs, so the prompt must be self-contained.
|
|
670
773
|
- Use steer_subagent to send mid-run messages to a running background agent.
|
|
774
|
+
- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, etc.), since it is not aware of the user's intent.
|
|
775
|
+
- If an agent's description says it should be used proactively, try to use it without the user having to ask for it first.
|
|
671
776
|
- Use model to specify a different model (as "provider/modelId", or fuzzy e.g. "haiku", "sonnet").
|
|
672
777
|
- Use thinking to control extended thinking level.
|
|
673
778
|
- Use inherit_context if the agent needs the parent conversation history.
|
|
674
|
-
- Use isolation: "worktree" to run the agent in an isolated git worktree (safe parallel file modifications)
|
|
779
|
+
- Use isolation: "worktree" to run the agent in an isolated git worktree (safe parallel file modifications). The worktree is automatically cleaned up if the agent makes no changes; otherwise the path and branch are returned in the result.${scheduleGuideline}
|
|
780
|
+
|
|
781
|
+
## Writing the prompt
|
|
782
|
+
|
|
783
|
+
Provide clear, detailed prompts so the agent can work autonomously. Brief it like a smart colleague who just walked into the room — it hasn't seen this conversation, doesn't know what you've tried, doesn't understand why this task matters.
|
|
784
|
+
- Explain what you're trying to accomplish and why.
|
|
785
|
+
- Describe what you've already learned or ruled out.
|
|
786
|
+
- Give enough context about the surrounding problem that the agent can make judgment calls rather than just following a narrow instruction.
|
|
787
|
+
- If you need a short response, say so ("report in under 200 words").
|
|
788
|
+
- Lookups: hand over the exact command. Investigations: hand over the question — prescribed steps become dead weight when the premise is wrong.
|
|
789
|
+
|
|
790
|
+
Terse command-style prompts produce shallow, generic work.
|
|
791
|
+
|
|
792
|
+
**Never delegate understanding.** Don't write "based on your findings, fix the bug" or "based on the research, implement it." Those phrases push synthesis onto the agent instead of doing it yourself. Write prompts that prove you understood: include file paths, line numbers, what specifically to change.`;
|
|
793
|
+
|
|
794
|
+
// `toolDescriptionMode: "custom"` — user-authored description with live
|
|
795
|
+
// dynamic parts. Project file wins over global; missing/empty falls back to
|
|
796
|
+
// "full" (a stale fallback beats a blank tool description). Only the prose
|
|
797
|
+
// is customizable — the parameter schema stays code-owned.
|
|
798
|
+
const renderToolDescriptionTemplate = (template: string): string => {
|
|
799
|
+
const vars: Record<string, () => string> = {
|
|
800
|
+
typeList: buildTypeListText,
|
|
801
|
+
compactTypeList: buildCompactTypeListText,
|
|
802
|
+
agentDir: getAgentDir,
|
|
803
|
+
scheduleGuideline: () => scheduleGuideline,
|
|
804
|
+
};
|
|
805
|
+
// Replacement callback (not a string) — agent descriptions may contain `$&` etc.
|
|
806
|
+
return template.replace(/\{\{(\w+)\}\}/g, (raw, name: string) => {
|
|
807
|
+
if (vars[name]) return vars[name]();
|
|
808
|
+
console.warn(`[pi-subagents] agent-tool-description.md: unknown placeholder ${raw} left as-is`);
|
|
809
|
+
return raw;
|
|
810
|
+
});
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
const loadCustomToolDescription = (): string | undefined => {
|
|
814
|
+
for (const path of [
|
|
815
|
+
join(process.cwd(), ".pi", "agent-tool-description.md"),
|
|
816
|
+
join(getAgentDir(), "agent-tool-description.md"),
|
|
817
|
+
]) {
|
|
818
|
+
try {
|
|
819
|
+
if (!existsSync(path)) continue;
|
|
820
|
+
const text = readFileSync(path, "utf-8").trim();
|
|
821
|
+
if (text) return renderToolDescriptionTemplate(text);
|
|
822
|
+
console.warn(`[pi-subagents] ${path} is empty — ignoring`);
|
|
823
|
+
} catch (err) {
|
|
824
|
+
console.warn(`[pi-subagents] failed to read ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return undefined;
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
const agentToolDescription = (() => {
|
|
831
|
+
const mode = getToolDescriptionMode();
|
|
832
|
+
if (mode === "compact") return compactAgentToolDescription;
|
|
833
|
+
if (mode === "custom") {
|
|
834
|
+
const custom = loadCustomToolDescription();
|
|
835
|
+
if (custom) return custom;
|
|
836
|
+
console.warn('[pi-subagents] toolDescriptionMode is "custom" but no agent-tool-description.md found — using "full"');
|
|
837
|
+
}
|
|
838
|
+
return fullAgentToolDescription;
|
|
839
|
+
})();
|
|
840
|
+
|
|
841
|
+
pi.registerTool(defineTool({
|
|
842
|
+
name: SUBAGENT_TOOL_NAMES.AGENT,
|
|
843
|
+
label: "Agent",
|
|
844
|
+
description: agentToolDescription,
|
|
845
|
+
promptSnippet: "Launch autonomous sub-agents for complex multi-step tasks",
|
|
846
|
+
promptGuidelines: [
|
|
847
|
+
"Use Agent with specialized agents when the task matches an agent type's description. Subagents are valuable for parallelizing independent queries or for protecting the main context window from excessive results, but should not be used excessively when not needed. Importantly, avoid duplicating work that subagents are already doing — if you delegate research to a subagent, do not also perform the same searches yourself.",
|
|
848
|
+
"For broad codebase exploration or research, spawn Agent with an appropriate subagent_type (e.g. Explore). Otherwise use direct tools (read, grep, find) when the target is already known.",
|
|
849
|
+
"When an agent runs in the background, you will be notified on completion — do not poll or sleep waiting for it. Continue with other work instead.",
|
|
850
|
+
"Trust but verify: an agent's summary describes intent, not outcome. When an agent writes or edits code, check the actual changes before reporting work as done.",
|
|
851
|
+
],
|
|
675
852
|
parameters: Type.Object({
|
|
676
853
|
prompt: Type.String({
|
|
677
854
|
description: "The task for the agent to perform.",
|
|
@@ -679,9 +856,9 @@ Guidelines:
|
|
|
679
856
|
description: Type.String({
|
|
680
857
|
description: "A short (3-5 word) description of the task (shown in UI).",
|
|
681
858
|
}),
|
|
682
|
-
subagent_type: Type.String({
|
|
683
|
-
description:
|
|
684
|
-
}),
|
|
859
|
+
subagent_type: (subagentTypeSchema = Type.String({
|
|
860
|
+
description: buildSubagentTypeDesc(),
|
|
861
|
+
})),
|
|
685
862
|
model: Type.Optional(
|
|
686
863
|
Type.String({
|
|
687
864
|
description:
|
|
@@ -724,6 +901,7 @@ Guidelines:
|
|
|
724
901
|
description: 'Set to "worktree" to run the agent in a temporary git worktree (isolated copy of the repo). Changes are saved to a branch on completion.',
|
|
725
902
|
}),
|
|
726
903
|
),
|
|
904
|
+
...scheduleParam,
|
|
727
905
|
}),
|
|
728
906
|
|
|
729
907
|
// ---- Custom rendering: Claude Code style ----
|
|
@@ -741,7 +919,7 @@ Guidelines:
|
|
|
741
919
|
return new Text(text, 0, 0);
|
|
742
920
|
}
|
|
743
921
|
|
|
744
|
-
// Helper: build "haiku · thinking: high ·
|
|
922
|
+
// Helper: build "haiku · thinking: high · ↻5≤30 · 3 tool uses · 33.8k tokens" stats string
|
|
745
923
|
const stats = (d: AgentDetails) => {
|
|
746
924
|
const parts: string[] = [];
|
|
747
925
|
if (d.modelName) parts.push(d.modelName);
|
|
@@ -758,9 +936,7 @@ Guidelines:
|
|
|
758
936
|
if (isPartial || details.status === "running") {
|
|
759
937
|
const frame = SPINNER[details.spinnerFrame ?? 0];
|
|
760
938
|
const s = stats(details);
|
|
761
|
-
|
|
762
|
-
line += "\n" + theme.fg("dim", ` ⎿ ${details.activity ?? "thinking…"}`);
|
|
763
|
-
return new Text(line, 0, 0);
|
|
939
|
+
return renderRunningAgentStatus(frame, s, details.activity ?? "thinking…", theme);
|
|
764
940
|
}
|
|
765
941
|
|
|
766
942
|
// ---- Background agent launched ----
|
|
@@ -849,34 +1025,111 @@ Guidelines:
|
|
|
849
1025
|
}
|
|
850
1026
|
}
|
|
851
1027
|
|
|
1028
|
+
// Scope validation: the effective resolved model is checked against the
|
|
1029
|
+
// user's enabledModels list (read in `enabled-models.ts`).
|
|
1030
|
+
//
|
|
1031
|
+
// Design: scopeModels guards against *runtime* LLM choices, not user-level config.
|
|
1032
|
+
// - Caller-supplied out-of-scope → hard error (the orchestrator made an explicit
|
|
1033
|
+
// out-of-scope choice; surface it so it picks differently).
|
|
1034
|
+
// - Frontmatter-pinned or parent-inherited out-of-scope → warn but proceed (the
|
|
1035
|
+
// user authored/installed this agent or chose the parent's model; trust it).
|
|
1036
|
+
// See SubagentsSettings.scopeModels docstring for the full policy.
|
|
1037
|
+
if (isScopeModelsEnabled() && model) {
|
|
1038
|
+
const allowed = resolveEnabledModels(readEnabledModels(ctx.cwd), ctx.modelRegistry, ctx.cwd);
|
|
1039
|
+
if (allowed && !isModelInScope(model, allowed)) {
|
|
1040
|
+
if (resolvedConfig.modelFromParams) {
|
|
1041
|
+
const list = [...allowed].sort().map(m => ` ${m}`).join("\n");
|
|
1042
|
+
return textResult(
|
|
1043
|
+
`Model not in scope: "${resolvedConfig.modelInput}".\n\n` +
|
|
1044
|
+
`Allowed models (from enabledModels):\n${list}`,
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
// Frontmatter-pinned or parent-inherited: warn + proceed.
|
|
1048
|
+
const agentLabel = customConfig?.displayName ?? subagentType;
|
|
1049
|
+
const modelLabel = resolvedConfig.modelInput ?? `${model.provider}/${model.id}`;
|
|
1050
|
+
ctx.ui.notify(
|
|
1051
|
+
`Agent "${agentLabel}" using out-of-scope model "${modelLabel}"`,
|
|
1052
|
+
"warning",
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
852
1057
|
const thinking = resolvedConfig.thinking;
|
|
853
1058
|
const inheritContext = resolvedConfig.inheritContext;
|
|
854
1059
|
const runInBackground = resolvedConfig.runInBackground;
|
|
855
1060
|
const isolated = resolvedConfig.isolated;
|
|
856
1061
|
const isolation = resolvedConfig.isolation;
|
|
857
1062
|
|
|
858
|
-
// Build display tags for non-default config
|
|
859
1063
|
const parentModelId = ctx.model?.id;
|
|
860
1064
|
const effectiveModelId = model?.id;
|
|
861
|
-
const
|
|
1065
|
+
const modelName = effectiveModelId && effectiveModelId !== parentModelId
|
|
862
1066
|
? (model?.name ?? effectiveModelId).replace(/^Claude\s+/i, "").toLowerCase()
|
|
863
1067
|
: undefined;
|
|
864
|
-
const agentTags: string[] = [];
|
|
865
|
-
const modeLabel = getPromptModeLabel(subagentType);
|
|
866
|
-
if (modeLabel) agentTags.push(modeLabel);
|
|
867
|
-
if (thinking) agentTags.push(`thinking: ${thinking}`);
|
|
868
|
-
if (isolated) agentTags.push("isolated");
|
|
869
|
-
if (isolation === "worktree") agentTags.push("worktree");
|
|
870
1068
|
const effectiveMaxTurns = normalizeMaxTurns(resolvedConfig.maxTurns ?? getDefaultMaxTurns());
|
|
871
|
-
|
|
1069
|
+
const agentInvocation: AgentInvocation = {
|
|
1070
|
+
modelName,
|
|
1071
|
+
thinking,
|
|
1072
|
+
// Explicit value only — the default fallback would just add noise.
|
|
1073
|
+
// Normalize so `0` (unlimited) doesn't surface as a misleading "max turns: 0".
|
|
1074
|
+
maxTurns: normalizeMaxTurns(resolvedConfig.maxTurns),
|
|
1075
|
+
isolated,
|
|
1076
|
+
inheritContext,
|
|
1077
|
+
runInBackground,
|
|
1078
|
+
isolation,
|
|
1079
|
+
};
|
|
1080
|
+
// Tool-result render shows the mode label too; viewer's header already does.
|
|
1081
|
+
const modeLabel = getPromptModeLabel(subagentType);
|
|
1082
|
+
const { tags: invocationTags } = buildInvocationTags(agentInvocation);
|
|
1083
|
+
const agentTags = modeLabel ? [modeLabel, ...invocationTags] : invocationTags;
|
|
872
1084
|
const detailBase = {
|
|
873
1085
|
displayName,
|
|
874
1086
|
description: params.description,
|
|
875
1087
|
subagentType,
|
|
876
|
-
modelName
|
|
1088
|
+
modelName,
|
|
877
1089
|
tags: agentTags.length > 0 ? agentTags : undefined,
|
|
878
1090
|
};
|
|
879
1091
|
|
|
1092
|
+
// ---- Schedule: register a job, don't spawn now ----
|
|
1093
|
+
if (params.schedule) {
|
|
1094
|
+
if (!isSchedulingEnabled()) {
|
|
1095
|
+
return textResult("Scheduling is disabled in this project. Enable via /agents → Settings → Scheduling.");
|
|
1096
|
+
}
|
|
1097
|
+
if (params.resume) {
|
|
1098
|
+
return textResult("Cannot combine `schedule` with `resume` — schedules create fresh agents.");
|
|
1099
|
+
}
|
|
1100
|
+
if (params.inherit_context) {
|
|
1101
|
+
return textResult("Cannot combine `schedule` with `inherit_context` — there is no parent conversation at fire time.");
|
|
1102
|
+
}
|
|
1103
|
+
if (params.run_in_background === false) {
|
|
1104
|
+
return textResult("Cannot combine `schedule` with `run_in_background: false` — scheduled jobs always run in background.");
|
|
1105
|
+
}
|
|
1106
|
+
if (!scheduler.isActive()) {
|
|
1107
|
+
return textResult("Scheduler is not active in this session yet. Try again after the session has fully started.");
|
|
1108
|
+
}
|
|
1109
|
+
try {
|
|
1110
|
+
const job = scheduler.addJob({
|
|
1111
|
+
name: params.description as string,
|
|
1112
|
+
description: params.description as string,
|
|
1113
|
+
schedule: params.schedule as string,
|
|
1114
|
+
subagent_type: subagentType,
|
|
1115
|
+
prompt: params.prompt as string,
|
|
1116
|
+
model: params.model as string | undefined,
|
|
1117
|
+
thinking: thinking,
|
|
1118
|
+
max_turns: effectiveMaxTurns,
|
|
1119
|
+
isolated: isolated,
|
|
1120
|
+
isolation: isolation,
|
|
1121
|
+
});
|
|
1122
|
+
const next = scheduler.getNextRun(job.id);
|
|
1123
|
+
return textResult(
|
|
1124
|
+
`Scheduled "${job.name}" (id: ${job.id}, type: ${job.scheduleType}). ` +
|
|
1125
|
+
`Next run: ${next ?? "(unknown)"}. ` +
|
|
1126
|
+
`Manage via /agents → Scheduled jobs.`,
|
|
1127
|
+
);
|
|
1128
|
+
} catch (err) {
|
|
1129
|
+
return textResult(err instanceof Error ? err.message : String(err));
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
880
1133
|
// Resume existing agent
|
|
881
1134
|
if (params.resume) {
|
|
882
1135
|
const existing = manager.getRecord(params.resume);
|
|
@@ -900,14 +1153,10 @@ Guidelines:
|
|
|
900
1153
|
if (runInBackground) {
|
|
901
1154
|
const { state: bgState, callbacks: bgCallbacks } = createActivityTracker(effectiveMaxTurns);
|
|
902
1155
|
|
|
903
|
-
let id: string;
|
|
904
|
-
// first_tool/first_turn are now emitted centrally by AgentManager.startAgent
|
|
905
|
-
// (the single choke point for ALL spawn paths, including RPC-spawned panels),
|
|
906
|
-
// so no per-branch wiring is needed here.
|
|
907
|
-
|
|
908
1156
|
// Wrap onSessionCreated to wire output file streaming.
|
|
909
1157
|
// The callback lazily reads record.outputFile (set right after spawn)
|
|
910
1158
|
// rather than closing over a value that doesn't exist yet.
|
|
1159
|
+
let id: string;
|
|
911
1160
|
const origBgOnSession = bgCallbacks.onSessionCreated;
|
|
912
1161
|
bgCallbacks.onSessionCreated = (session: any) => {
|
|
913
1162
|
origBgOnSession(session);
|
|
@@ -917,25 +1166,22 @@ Guidelines:
|
|
|
917
1166
|
}
|
|
918
1167
|
};
|
|
919
1168
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
description: params.description,
|
|
937
|
-
isBackground: true,
|
|
938
|
-
});
|
|
1169
|
+
try {
|
|
1170
|
+
id = manager.spawn(pi, ctx, subagentType, params.prompt, {
|
|
1171
|
+
description: params.description,
|
|
1172
|
+
model,
|
|
1173
|
+
maxTurns: effectiveMaxTurns,
|
|
1174
|
+
isolated,
|
|
1175
|
+
inheritContext,
|
|
1176
|
+
thinkingLevel: thinking,
|
|
1177
|
+
isBackground: true,
|
|
1178
|
+
isolation,
|
|
1179
|
+
invocation: agentInvocation,
|
|
1180
|
+
...bgCallbacks,
|
|
1181
|
+
});
|
|
1182
|
+
} catch (err) {
|
|
1183
|
+
return textResult(err instanceof Error ? err.message : String(err));
|
|
1184
|
+
}
|
|
939
1185
|
|
|
940
1186
|
// Set output file + join mode synchronously after spawn, before the
|
|
941
1187
|
// event loop yields — onSessionCreated is async so this is safe.
|
|
@@ -944,11 +1190,8 @@ Guidelines:
|
|
|
944
1190
|
if (record && joinMode) {
|
|
945
1191
|
record.joinMode = joinMode;
|
|
946
1192
|
record.toolCallId = toolCallId;
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
record.outputFile = createOutputFilePath(ctx.cwd, id, sessionId);
|
|
950
|
-
writeInitialEntry(record.outputFile, id, params.prompt, ctx.cwd);
|
|
951
|
-
}
|
|
1193
|
+
record.outputFile = createOutputFilePath(ctx.cwd, id, ctx.sessionManager.getSessionId());
|
|
1194
|
+
writeInitialEntry(record.outputFile, id, params.prompt, ctx.cwd);
|
|
952
1195
|
}
|
|
953
1196
|
|
|
954
1197
|
if (joinMode == null || joinMode === 'async') {
|
|
@@ -966,6 +1209,14 @@ Guidelines:
|
|
|
966
1209
|
widget.ensureTimer();
|
|
967
1210
|
widget.update();
|
|
968
1211
|
|
|
1212
|
+
// Emit created event
|
|
1213
|
+
pi.events.emit("subagents:created", {
|
|
1214
|
+
id,
|
|
1215
|
+
type: subagentType,
|
|
1216
|
+
description: params.description,
|
|
1217
|
+
isBackground: true,
|
|
1218
|
+
});
|
|
1219
|
+
|
|
969
1220
|
const isQueued = record?.status === "queued";
|
|
970
1221
|
return textResult(
|
|
971
1222
|
`Agent ${isQueued ? "queued" : "started"} in background.\n` +
|
|
@@ -990,7 +1241,7 @@ Guidelines:
|
|
|
990
1241
|
const details: AgentDetails = {
|
|
991
1242
|
...detailBase,
|
|
992
1243
|
toolUses: fgState.toolUses,
|
|
993
|
-
tokens: fgState
|
|
1244
|
+
tokens: formatLifetimeTokens(fgState),
|
|
994
1245
|
turnCount: fgState.turnCount,
|
|
995
1246
|
maxTurns: fgState.maxTurns,
|
|
996
1247
|
durationMs: Date.now() - startedAt,
|
|
@@ -1006,7 +1257,9 @@ Guidelines:
|
|
|
1006
1257
|
|
|
1007
1258
|
const { state: fgState, callbacks: fgCallbacks } = createActivityTracker(effectiveMaxTurns, streamUpdate);
|
|
1008
1259
|
|
|
1009
|
-
// Wire session creation
|
|
1260
|
+
// Wire session creation: register in widget + stream to output file.
|
|
1261
|
+
// The output file path is set synchronously after spawn (below),
|
|
1262
|
+
// before onSessionCreated fires — same pattern as background agents.
|
|
1010
1263
|
const origOnSession = fgCallbacks.onSessionCreated;
|
|
1011
1264
|
fgCallbacks.onSessionCreated = (session: any) => {
|
|
1012
1265
|
origOnSession(session);
|
|
@@ -1018,6 +1271,13 @@ Guidelines:
|
|
|
1018
1271
|
break;
|
|
1019
1272
|
}
|
|
1020
1273
|
}
|
|
1274
|
+
// Stream conversation to output file (foreground agent logging)
|
|
1275
|
+
if (fgId) {
|
|
1276
|
+
const rec = manager.getRecord(fgId);
|
|
1277
|
+
if (rec?.outputFile) {
|
|
1278
|
+
rec.outputCleanup = streamToOutputFile(session, rec.outputFile, fgId, ctx.cwd);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1021
1281
|
};
|
|
1022
1282
|
|
|
1023
1283
|
// Animate spinner at ~80ms (smooth rotation through 10 braille frames)
|
|
@@ -1028,17 +1288,33 @@ Guidelines:
|
|
|
1028
1288
|
|
|
1029
1289
|
streamUpdate();
|
|
1030
1290
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1291
|
+
let record: AgentRecord;
|
|
1292
|
+
try {
|
|
1293
|
+
const fgResult = await manager.spawnAndWait(pi, ctx, subagentType, params.prompt, {
|
|
1294
|
+
description: params.description,
|
|
1295
|
+
model,
|
|
1296
|
+
maxTurns: effectiveMaxTurns,
|
|
1297
|
+
isolated,
|
|
1298
|
+
inheritContext,
|
|
1299
|
+
thinkingLevel: thinking,
|
|
1300
|
+
isolation,
|
|
1301
|
+
invocation: agentInvocation,
|
|
1302
|
+
signal,
|
|
1303
|
+
...fgCallbacks,
|
|
1304
|
+
}, (fgAgentId) => {
|
|
1305
|
+
// onSpawned: called synchronously after spawn, before onSessionCreated fires.
|
|
1306
|
+
// Set up the output file so streamToOutputFile can pick it up.
|
|
1307
|
+
const fgRec = manager.getRecord(fgAgentId);
|
|
1308
|
+
if (fgRec) {
|
|
1309
|
+
fgRec.outputFile = createOutputFilePath(ctx.cwd, fgAgentId, ctx.sessionManager.getSessionId());
|
|
1310
|
+
writeInitialEntry(fgRec.outputFile, fgAgentId, params.prompt, ctx.cwd);
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
record = fgResult.record;
|
|
1314
|
+
} catch (err) {
|
|
1315
|
+
clearInterval(spinnerInterval);
|
|
1316
|
+
return textResult(err instanceof Error ? err.message : String(err));
|
|
1317
|
+
}
|
|
1042
1318
|
|
|
1043
1319
|
clearInterval(spinnerInterval);
|
|
1044
1320
|
|
|
@@ -1049,12 +1325,14 @@ Guidelines:
|
|
|
1049
1325
|
}
|
|
1050
1326
|
|
|
1051
1327
|
// Get final token count
|
|
1052
|
-
const tokenText =
|
|
1328
|
+
const tokenText = formatLifetimeTokens(fgState);
|
|
1053
1329
|
|
|
1054
1330
|
const details = buildDetails(detailBase, record, fgState, { tokens: tokenText });
|
|
1055
1331
|
|
|
1332
|
+
// "general-purpose" may itself be unregistered (defaults disabled, no
|
|
1333
|
+
// user override) — getConfig then uses the hardcoded fallback config.
|
|
1056
1334
|
const fallbackNote = fellBack
|
|
1057
|
-
? `Note: Unknown agent type "${rawType}" — using general-purpose.\n\n`
|
|
1335
|
+
? `Note: Unknown agent type "${rawType}" — using ${resolveType("general-purpose") ? "general-purpose" : "the fallback agent config"}.\n\n`
|
|
1058
1336
|
: "";
|
|
1059
1337
|
|
|
1060
1338
|
if (record.status === "error") {
|
|
@@ -1070,15 +1348,16 @@ Guidelines:
|
|
|
1070
1348
|
details,
|
|
1071
1349
|
);
|
|
1072
1350
|
},
|
|
1073
|
-
});
|
|
1351
|
+
}));
|
|
1074
1352
|
|
|
1075
1353
|
// ---- get_subagent_result tool ----
|
|
1076
1354
|
|
|
1077
|
-
pi.registerTool({
|
|
1078
|
-
name:
|
|
1355
|
+
pi.registerTool(defineTool({
|
|
1356
|
+
name: SUBAGENT_TOOL_NAMES.GET_RESULT,
|
|
1079
1357
|
label: "Get Agent Result",
|
|
1080
1358
|
description:
|
|
1081
1359
|
"Check status and retrieve results from a background agent. Use the agent ID returned by Agent with run_in_background.",
|
|
1360
|
+
promptSnippet: "Check status and retrieve results from a background agent",
|
|
1082
1361
|
parameters: Type.Object({
|
|
1083
1362
|
agent_id: Type.String({
|
|
1084
1363
|
description: "The agent ID to check.",
|
|
@@ -1112,12 +1391,17 @@ Guidelines:
|
|
|
1112
1391
|
|
|
1113
1392
|
const displayName = getDisplayName(record.type);
|
|
1114
1393
|
const duration = formatDuration(record.startedAt, record.completedAt);
|
|
1115
|
-
const tokens =
|
|
1116
|
-
const
|
|
1394
|
+
const tokens = formatLifetimeTokens(record);
|
|
1395
|
+
const contextPercent = getSessionContextPercent(record.session);
|
|
1396
|
+
const statsParts = [`Tool uses: ${record.toolUses}`];
|
|
1397
|
+
if (tokens) statsParts.push(tokens);
|
|
1398
|
+
if (contextPercent !== null) statsParts.push(`Context: ${Math.round(contextPercent)}%`);
|
|
1399
|
+
if (record.compactionCount) statsParts.push(`Compactions: ${record.compactionCount}`);
|
|
1400
|
+
statsParts.push(`Duration: ${duration}`);
|
|
1117
1401
|
|
|
1118
1402
|
let output =
|
|
1119
1403
|
`Agent: ${record.id}\n` +
|
|
1120
|
-
`Type: ${displayName} | Status: ${record.status} | ${
|
|
1404
|
+
`Type: ${displayName} | Status: ${record.status}${getStatusNote(record.status)} | ${statsParts.join(" | ")}\n` +
|
|
1121
1405
|
`Description: ${record.description}\n\n`;
|
|
1122
1406
|
|
|
1123
1407
|
if (record.status === "running") {
|
|
@@ -1144,16 +1428,17 @@ Guidelines:
|
|
|
1144
1428
|
|
|
1145
1429
|
return textResult(output);
|
|
1146
1430
|
},
|
|
1147
|
-
});
|
|
1431
|
+
}));
|
|
1148
1432
|
|
|
1149
1433
|
// ---- steer_subagent tool ----
|
|
1150
1434
|
|
|
1151
|
-
pi.registerTool({
|
|
1152
|
-
name:
|
|
1435
|
+
pi.registerTool(defineTool({
|
|
1436
|
+
name: SUBAGENT_TOOL_NAMES.STEER,
|
|
1153
1437
|
label: "Steer Agent",
|
|
1154
1438
|
description:
|
|
1155
1439
|
"Send a steering message to a running agent. The message will interrupt the agent after its current tool execution " +
|
|
1156
1440
|
"and be injected into its conversation, allowing you to redirect its work mid-run. Only works on running agents.",
|
|
1441
|
+
promptSnippet: "Send a steering message to redirect a running background agent",
|
|
1157
1442
|
parameters: Type.Object({
|
|
1158
1443
|
agent_id: Type.String({
|
|
1159
1444
|
description: "The agent ID to steer (must be currently running).",
|
|
@@ -1181,149 +1466,29 @@ Guidelines:
|
|
|
1181
1466
|
try {
|
|
1182
1467
|
await steerAgent(record.session, params.message);
|
|
1183
1468
|
pi.events.emit("subagents:steered", { id: record.id, message: params.message });
|
|
1184
|
-
|
|
1469
|
+
const tokens = formatLifetimeTokens(record);
|
|
1470
|
+
const contextPercent = getSessionContextPercent(record.session);
|
|
1471
|
+
const stateParts: string[] = [];
|
|
1472
|
+
if (tokens) stateParts.push(tokens);
|
|
1473
|
+
stateParts.push(`${record.toolUses} tool ${record.toolUses === 1 ? "use" : "uses"}`);
|
|
1474
|
+
if (contextPercent !== null) stateParts.push(`context ${Math.round(contextPercent)}% full`);
|
|
1475
|
+
if (record.compactionCount) stateParts.push(`${record.compactionCount} compaction${record.compactionCount === 1 ? "" : "s"}`);
|
|
1476
|
+
return textResult(
|
|
1477
|
+
`Steering message sent to agent ${record.id}. The agent will process it after its current tool execution.\n` +
|
|
1478
|
+
`Current state: ${stateParts.join(" · ")}`,
|
|
1479
|
+
);
|
|
1185
1480
|
} catch (err) {
|
|
1186
1481
|
return textResult(`Failed to steer agent: ${err instanceof Error ? err.message : String(err)}`);
|
|
1187
1482
|
}
|
|
1188
1483
|
},
|
|
1189
|
-
});
|
|
1484
|
+
}));
|
|
1190
1485
|
|
|
1191
|
-
// ----
|
|
1192
|
-
|
|
1193
|
-
const projectAgentsDir = () => {
|
|
1194
|
-
const cwd = getProjectCwd();
|
|
1195
|
-
return cwd ? join(cwd, ".pi", "agents") : undefined;
|
|
1196
|
-
};
|
|
1197
|
-
const personalAgentsDir = () => join(homedir(), ".pi", "agent", "agents");
|
|
1198
|
-
|
|
1199
|
-
/** Find the file path of a custom agent by name (project first, then global). */
|
|
1200
|
-
function findAgentFile(name: string): { path: string; location: "project" | "personal" } | undefined {
|
|
1201
|
-
const projectDir = projectAgentsDir();
|
|
1202
|
-
if (projectDir) {
|
|
1203
|
-
const projectPath = join(projectDir, `${name}.md`);
|
|
1204
|
-
if (existsSync(projectPath)) return { path: projectPath, location: "project" };
|
|
1205
|
-
}
|
|
1206
|
-
const personalPath = join(personalAgentsDir(), `${name}.md`);
|
|
1207
|
-
if (existsSync(personalPath)) return { path: personalPath, location: "personal" };
|
|
1208
|
-
return undefined;
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
function getModelLabel(type: string, registry?: ModelRegistry): string {
|
|
1212
|
-
const cfg = getAgentConfig(type);
|
|
1213
|
-
if (!cfg?.model) return "inherit";
|
|
1214
|
-
// If registry provided, check if the model actually resolves
|
|
1215
|
-
if (registry) {
|
|
1216
|
-
const resolved = resolveModel(cfg.model, registry);
|
|
1217
|
-
if (typeof resolved === "string") return "inherit"; // model not available
|
|
1218
|
-
}
|
|
1219
|
-
return getModelLabelFromConfig(cfg.model);
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
async function showAgentsMenu(ctx: ExtensionCommandContext) {
|
|
1223
|
-
reloadCustomAgents();
|
|
1224
|
-
const allNames = getAllTypes();
|
|
1225
|
-
|
|
1226
|
-
// Build select options
|
|
1227
|
-
const options: string[] = [];
|
|
1228
|
-
|
|
1229
|
-
// Running agents entry (only if there are active agents)
|
|
1230
|
-
const agents = manager.listAgents();
|
|
1231
|
-
if (agents.length > 0) {
|
|
1232
|
-
const running = agents.filter(a => a.status === "running" || a.status === "queued").length;
|
|
1233
|
-
const done = agents.filter(a => a.status === "completed" || a.status === "steered").length;
|
|
1234
|
-
options.push(`Running agents (${agents.length}) — ${running} running, ${done} done`);
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
// Agent types list
|
|
1238
|
-
if (allNames.length > 0) {
|
|
1239
|
-
options.push(`Agent types (${allNames.length})`);
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
// Actions
|
|
1243
|
-
options.push("Create new agent");
|
|
1244
|
-
options.push("Settings");
|
|
1245
|
-
|
|
1246
|
-
const noAgentsMsg = allNames.length === 0 && agents.length === 0
|
|
1247
|
-
? "No agents found. Create specialized subagents that can be delegated to.\n\n" +
|
|
1248
|
-
"Each subagent has its own context window, custom system prompt, and specific tools.\n\n" +
|
|
1249
|
-
"Try creating: Code Reviewer, Security Auditor, Test Writer, or Documentation Writer.\n\n"
|
|
1250
|
-
: "";
|
|
1251
|
-
|
|
1252
|
-
if (noAgentsMsg) {
|
|
1253
|
-
ctx.ui.notify(noAgentsMsg, "info");
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
const choice = await ctx.ui.select("Agents", options);
|
|
1257
|
-
if (!choice) return;
|
|
1258
|
-
|
|
1259
|
-
if (choice.startsWith("Running agents (")) {
|
|
1260
|
-
await showRunningAgents(ctx);
|
|
1261
|
-
await showAgentsMenu(ctx);
|
|
1262
|
-
} else if (choice.startsWith("Agent types (")) {
|
|
1263
|
-
await showAllAgentsList(ctx);
|
|
1264
|
-
await showAgentsMenu(ctx);
|
|
1265
|
-
} else if (choice === "Create new agent") {
|
|
1266
|
-
await showCreateWizard(ctx);
|
|
1267
|
-
} else if (choice === "Settings") {
|
|
1268
|
-
await showSettings(ctx);
|
|
1269
|
-
await showAgentsMenu(ctx);
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
async function showAllAgentsList(ctx: ExtensionCommandContext) {
|
|
1274
|
-
const allNames = getAllTypes();
|
|
1275
|
-
if (allNames.length === 0) {
|
|
1276
|
-
ctx.ui.notify("No agents.", "info");
|
|
1277
|
-
return;
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
// Source indicators: defaults unmarked, custom agents get • (project) or ◦ (global)
|
|
1281
|
-
// Disabled agents get ✕ prefix
|
|
1282
|
-
const sourceIndicator = (cfg: AgentConfig | undefined) => {
|
|
1283
|
-
const disabled = cfg?.enabled === false;
|
|
1284
|
-
if (cfg?.source === "project") return disabled ? "✕• " : "• ";
|
|
1285
|
-
if (cfg?.source === "global") return disabled ? "✕◦ " : "◦ ";
|
|
1286
|
-
if (disabled) return "✕ ";
|
|
1287
|
-
return " ";
|
|
1288
|
-
};
|
|
1289
|
-
|
|
1290
|
-
const entries = allNames.map(name => {
|
|
1291
|
-
const cfg = getAgentConfig(name);
|
|
1292
|
-
const disabled = cfg?.enabled === false;
|
|
1293
|
-
const model = getModelLabel(name, ctx.modelRegistry);
|
|
1294
|
-
const indicator = sourceIndicator(cfg);
|
|
1295
|
-
const prefix = `${indicator}${name} · ${model}`;
|
|
1296
|
-
const desc = disabled ? "(disabled)" : (cfg?.description ?? name);
|
|
1297
|
-
return { name, prefix, desc };
|
|
1298
|
-
});
|
|
1299
|
-
const maxPrefix = Math.max(...entries.map(e => e.prefix.length));
|
|
1300
|
-
|
|
1301
|
-
const hasCustom = allNames.some(n => { const c = getAgentConfig(n); return c && !c.isDefault && c.enabled !== false; });
|
|
1302
|
-
const hasDisabled = allNames.some(n => getAgentConfig(n)?.enabled === false);
|
|
1303
|
-
const legendParts: string[] = [];
|
|
1304
|
-
if (hasCustom) legendParts.push("• = project ◦ = global");
|
|
1305
|
-
if (hasDisabled) legendParts.push("✕ = disabled");
|
|
1306
|
-
const legend = legendParts.length ? "\n" + legendParts.join(" ") : "";
|
|
1307
|
-
|
|
1308
|
-
const options = entries.map(({ prefix, desc }) =>
|
|
1309
|
-
`${prefix.padEnd(maxPrefix)} — ${desc}`,
|
|
1310
|
-
);
|
|
1311
|
-
if (legend) options.push(legend);
|
|
1312
|
-
|
|
1313
|
-
const choice = await ctx.ui.select("Agent types", options);
|
|
1314
|
-
if (!choice) return;
|
|
1315
|
-
|
|
1316
|
-
const agentName = choice.split(" · ")[0].replace(/^[•◦✕\s]+/, "").trim();
|
|
1317
|
-
if (getAgentConfig(agentName)) {
|
|
1318
|
-
await showAgentDetail(ctx, agentName);
|
|
1319
|
-
await showAllAgentsList(ctx);
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1486
|
+
// ---- Running-agents list (the /pp > Subagents entry) ----
|
|
1322
1487
|
|
|
1323
1488
|
async function showRunningAgents(ctx: ExtensionCommandContext) {
|
|
1324
1489
|
const agents = manager.listAgents();
|
|
1325
1490
|
if (agents.length === 0) {
|
|
1326
|
-
ctx.ui.
|
|
1491
|
+
await ctx.ui.select("Running agents", ["Back"]);
|
|
1327
1492
|
return;
|
|
1328
1493
|
}
|
|
1329
1494
|
|
|
@@ -1339,7 +1504,15 @@ Guidelines:
|
|
|
1339
1504
|
// Find the selected agent by matching the option index
|
|
1340
1505
|
const idx = options.indexOf(choice);
|
|
1341
1506
|
if (idx < 0) return;
|
|
1342
|
-
|
|
1507
|
+
// The list is a snapshot, so re-resolve by id at selection time: the agent
|
|
1508
|
+
// may have finished and been reaped while the menu was open. If it's gone,
|
|
1509
|
+
// just refresh the list instead of opening a dead viewer.
|
|
1510
|
+
const record = manager.getRecord(agents[idx].id);
|
|
1511
|
+
if (!record) {
|
|
1512
|
+
ctx.ui.notify("That agent is no longer available.", "info");
|
|
1513
|
+
await showRunningAgents(ctx);
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1343
1516
|
|
|
1344
1517
|
await viewAgentConversation(ctx, record);
|
|
1345
1518
|
// Back-navigation: re-show the list
|
|
@@ -1352,436 +1525,23 @@ Guidelines:
|
|
|
1352
1525
|
return;
|
|
1353
1526
|
}
|
|
1354
1527
|
|
|
1355
|
-
const { ConversationViewer } = await import("./ui/conversation-viewer.js");
|
|
1528
|
+
const { ConversationViewer, VIEWPORT_HEIGHT_PCT } = await import("./ui/conversation-viewer.js");
|
|
1356
1529
|
const session = record.session;
|
|
1357
1530
|
const activity = agentActivity.get(record.id);
|
|
1358
1531
|
|
|
1359
1532
|
await ctx.ui.custom<undefined>(
|
|
1360
|
-
(tui, theme,
|
|
1361
|
-
return new ConversationViewer(tui, session, record, activity, theme, done)
|
|
1533
|
+
(tui, theme, keybindings, done) => {
|
|
1534
|
+
return new ConversationViewer(tui, session, record, activity, theme, done, () => {
|
|
1535
|
+
if (manager.abort(record.id)) {
|
|
1536
|
+
ctx.ui.notify(`Stopped "${record.description}".`, "info");
|
|
1537
|
+
}
|
|
1538
|
+
}, keybindings, (message: string) => manager.steer(record.id, message));
|
|
1362
1539
|
},
|
|
1363
1540
|
{
|
|
1364
1541
|
overlay: true,
|
|
1365
|
-
overlayOptions: { anchor: "center", width: "90%" },
|
|
1542
|
+
overlayOptions: { anchor: "center", width: "90%", maxHeight: `${VIEWPORT_HEIGHT_PCT}%` },
|
|
1366
1543
|
},
|
|
1367
1544
|
);
|
|
1368
1545
|
}
|
|
1369
1546
|
|
|
1370
|
-
async function showAgentDetail(ctx: ExtensionCommandContext, name: string) {
|
|
1371
|
-
const cfg = getAgentConfig(name);
|
|
1372
|
-
if (!cfg) {
|
|
1373
|
-
ctx.ui.notify(`Agent config not found for "${name}".`, "warning");
|
|
1374
|
-
return;
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
const file = findAgentFile(name);
|
|
1378
|
-
const isDefault = cfg.isDefault === true;
|
|
1379
|
-
const disabled = cfg.enabled === false;
|
|
1380
|
-
|
|
1381
|
-
let menuOptions: string[];
|
|
1382
|
-
if (disabled && file) {
|
|
1383
|
-
// Disabled agent with a file — offer Enable
|
|
1384
|
-
menuOptions = isDefault
|
|
1385
|
-
? ["Enable", "Edit", "Reset to default", "Delete", "Back"]
|
|
1386
|
-
: ["Enable", "Edit", "Delete", "Back"];
|
|
1387
|
-
} else if (isDefault && !file) {
|
|
1388
|
-
// Default agent with no .md override
|
|
1389
|
-
menuOptions = ["Eject (export as .md)", "Disable", "Back"];
|
|
1390
|
-
} else if (isDefault && file) {
|
|
1391
|
-
// Default agent with .md override (ejected)
|
|
1392
|
-
menuOptions = ["Edit", "Disable", "Reset to default", "Delete", "Back"];
|
|
1393
|
-
} else {
|
|
1394
|
-
// User-defined agent
|
|
1395
|
-
menuOptions = ["Edit", "Disable", "Delete", "Back"];
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
const choice = await ctx.ui.select(name, menuOptions);
|
|
1399
|
-
if (!choice || choice === "Back") return;
|
|
1400
|
-
|
|
1401
|
-
if (choice === "Edit" && file) {
|
|
1402
|
-
const content = readFileSync(file.path, "utf-8");
|
|
1403
|
-
const edited = await ctx.ui.editor(`Edit ${name}`, content);
|
|
1404
|
-
if (edited !== undefined && edited !== content) {
|
|
1405
|
-
const { writeFileSync } = await import("node:fs");
|
|
1406
|
-
writeFileSync(file.path, edited, "utf-8");
|
|
1407
|
-
reloadCustomAgents();
|
|
1408
|
-
ctx.ui.notify(`Updated ${file.path}`, "info");
|
|
1409
|
-
}
|
|
1410
|
-
} else if (choice === "Delete") {
|
|
1411
|
-
if (file) {
|
|
1412
|
-
const confirmed = await ctx.ui.confirm("Delete agent", `Delete ${name} from ${file.location} (${file.path})?`);
|
|
1413
|
-
if (confirmed) {
|
|
1414
|
-
unlinkSync(file.path);
|
|
1415
|
-
reloadCustomAgents();
|
|
1416
|
-
ctx.ui.notify(`Deleted ${file.path}`, "info");
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
} else if (choice === "Reset to default" && file) {
|
|
1420
|
-
const confirmed = await ctx.ui.confirm("Reset to default", `Delete override ${file.path} and restore embedded default?`);
|
|
1421
|
-
if (confirmed) {
|
|
1422
|
-
unlinkSync(file.path);
|
|
1423
|
-
reloadCustomAgents();
|
|
1424
|
-
ctx.ui.notify(`Restored default ${name}`, "info");
|
|
1425
|
-
}
|
|
1426
|
-
} else if (choice.startsWith("Eject")) {
|
|
1427
|
-
await ejectAgent(ctx, name, cfg);
|
|
1428
|
-
} else if (choice === "Disable") {
|
|
1429
|
-
await disableAgent(ctx, name);
|
|
1430
|
-
} else if (choice === "Enable") {
|
|
1431
|
-
await enableAgent(ctx, name);
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
/** Eject a default agent: write its embedded config as a .md file. */
|
|
1436
|
-
async function ejectAgent(ctx: ExtensionCommandContext, name: string, cfg: AgentConfig) {
|
|
1437
|
-
const location = await ctx.ui.select("Choose location", [
|
|
1438
|
-
"Project (.pi/agents/)",
|
|
1439
|
-
"Personal (~/.pi/agent/agents/)",
|
|
1440
|
-
]);
|
|
1441
|
-
if (!location) return;
|
|
1442
|
-
|
|
1443
|
-
const targetDir = location.startsWith("Project") ? projectAgentsDir() : personalAgentsDir();
|
|
1444
|
-
mkdirSync(targetDir, { recursive: true });
|
|
1445
|
-
|
|
1446
|
-
const targetPath = join(targetDir, `${name}.md`);
|
|
1447
|
-
if (existsSync(targetPath)) {
|
|
1448
|
-
const overwrite = await ctx.ui.confirm("Overwrite", `${targetPath} already exists. Overwrite?`);
|
|
1449
|
-
if (!overwrite) return;
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
// Build the .md file content
|
|
1453
|
-
const fmFields: string[] = [];
|
|
1454
|
-
fmFields.push(`description: ${cfg.description}`);
|
|
1455
|
-
if (cfg.displayName) fmFields.push(`display_name: ${cfg.displayName}`);
|
|
1456
|
-
fmFields.push(`tools: ${cfg.builtinToolNames?.join(", ") || "all"}`);
|
|
1457
|
-
if (cfg.model) fmFields.push(`model: ${cfg.model}`);
|
|
1458
|
-
if (cfg.thinking) fmFields.push(`thinking: ${cfg.thinking}`);
|
|
1459
|
-
if (cfg.maxTurns) fmFields.push(`max_turns: ${cfg.maxTurns}`);
|
|
1460
|
-
fmFields.push(`prompt_mode: ${cfg.promptMode}`);
|
|
1461
|
-
if (cfg.extensions === false) fmFields.push("extensions: false");
|
|
1462
|
-
else if (Array.isArray(cfg.extensions)) fmFields.push(`extensions: ${cfg.extensions.join(", ")}`);
|
|
1463
|
-
if (cfg.skills === false) fmFields.push("skills: false");
|
|
1464
|
-
else if (Array.isArray(cfg.skills)) fmFields.push(`skills: ${cfg.skills.join(", ")}`);
|
|
1465
|
-
if (cfg.disallowedTools?.length) fmFields.push(`disallowed_tools: ${cfg.disallowedTools.join(", ")}`);
|
|
1466
|
-
if (cfg.inheritContext) fmFields.push("inherit_context: true");
|
|
1467
|
-
if (cfg.runInBackground) fmFields.push("run_in_background: true");
|
|
1468
|
-
if (cfg.isolated) fmFields.push("isolated: true");
|
|
1469
|
-
if (cfg.memory) fmFields.push(`memory: ${cfg.memory}`);
|
|
1470
|
-
if (cfg.isolation) fmFields.push(`isolation: ${cfg.isolation}`);
|
|
1471
|
-
|
|
1472
|
-
const content = `---\n${fmFields.join("\n")}\n---\n\n${cfg.systemPrompt}\n`;
|
|
1473
|
-
|
|
1474
|
-
const { writeFileSync } = await import("node:fs");
|
|
1475
|
-
writeFileSync(targetPath, content, "utf-8");
|
|
1476
|
-
reloadCustomAgents();
|
|
1477
|
-
ctx.ui.notify(`Ejected ${name} to ${targetPath}`, "info");
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
/** Disable an agent: set enabled: false in its .md file, or create a stub for built-in defaults. */
|
|
1481
|
-
async function disableAgent(ctx: ExtensionCommandContext, name: string) {
|
|
1482
|
-
const file = findAgentFile(name);
|
|
1483
|
-
if (file) {
|
|
1484
|
-
// Existing file — set enabled: false in frontmatter (idempotent)
|
|
1485
|
-
const content = readFileSync(file.path, "utf-8");
|
|
1486
|
-
if (content.includes("\nenabled: false\n")) {
|
|
1487
|
-
ctx.ui.notify(`${name} is already disabled.`, "info");
|
|
1488
|
-
return;
|
|
1489
|
-
}
|
|
1490
|
-
const updated = content.replace(/^---\n/, "---\nenabled: false\n");
|
|
1491
|
-
const { writeFileSync } = await import("node:fs");
|
|
1492
|
-
writeFileSync(file.path, updated, "utf-8");
|
|
1493
|
-
reloadCustomAgents();
|
|
1494
|
-
ctx.ui.notify(`Disabled ${name} (${file.path})`, "info");
|
|
1495
|
-
return;
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
// No file (built-in default) — create a stub
|
|
1499
|
-
const location = await ctx.ui.select("Choose location", [
|
|
1500
|
-
"Project (.pi/agents/)",
|
|
1501
|
-
"Personal (~/.pi/agent/agents/)",
|
|
1502
|
-
]);
|
|
1503
|
-
if (!location) return;
|
|
1504
|
-
|
|
1505
|
-
const targetDir = location.startsWith("Project") ? projectAgentsDir() : personalAgentsDir();
|
|
1506
|
-
mkdirSync(targetDir, { recursive: true });
|
|
1507
|
-
|
|
1508
|
-
const targetPath = join(targetDir, `${name}.md`);
|
|
1509
|
-
const { writeFileSync } = await import("node:fs");
|
|
1510
|
-
writeFileSync(targetPath, "---\nenabled: false\n---\n", "utf-8");
|
|
1511
|
-
reloadCustomAgents();
|
|
1512
|
-
ctx.ui.notify(`Disabled ${name} (${targetPath})`, "info");
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
/** Enable a disabled agent by removing enabled: false from its frontmatter. */
|
|
1516
|
-
async function enableAgent(ctx: ExtensionCommandContext, name: string) {
|
|
1517
|
-
const file = findAgentFile(name);
|
|
1518
|
-
if (!file) return;
|
|
1519
|
-
|
|
1520
|
-
const content = readFileSync(file.path, "utf-8");
|
|
1521
|
-
const updated = content.replace(/^(---\n)enabled: false\n/, "$1");
|
|
1522
|
-
const { writeFileSync } = await import("node:fs");
|
|
1523
|
-
|
|
1524
|
-
// If the file was just a stub ("---\n---\n"), delete it to restore the built-in default
|
|
1525
|
-
if (updated.trim() === "---\n---" || updated.trim() === "---\n---\n") {
|
|
1526
|
-
unlinkSync(file.path);
|
|
1527
|
-
reloadCustomAgents();
|
|
1528
|
-
ctx.ui.notify(`Enabled ${name} (removed ${file.path})`, "info");
|
|
1529
|
-
} else {
|
|
1530
|
-
writeFileSync(file.path, updated, "utf-8");
|
|
1531
|
-
reloadCustomAgents();
|
|
1532
|
-
ctx.ui.notify(`Enabled ${name} (${file.path})`, "info");
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
async function showCreateWizard(ctx: ExtensionCommandContext) {
|
|
1537
|
-
const location = await ctx.ui.select("Choose location", [
|
|
1538
|
-
"Project (.pi/agents/)",
|
|
1539
|
-
"Personal (~/.pi/agent/agents/)",
|
|
1540
|
-
]);
|
|
1541
|
-
if (!location) return;
|
|
1542
|
-
|
|
1543
|
-
const targetDir = location.startsWith("Project") ? projectAgentsDir() : personalAgentsDir();
|
|
1544
|
-
|
|
1545
|
-
const method = await ctx.ui.select("Creation method", [
|
|
1546
|
-
"Generate with Claude (recommended)",
|
|
1547
|
-
"Manual configuration",
|
|
1548
|
-
]);
|
|
1549
|
-
if (!method) return;
|
|
1550
|
-
|
|
1551
|
-
if (method.startsWith("Generate")) {
|
|
1552
|
-
await showGenerateWizard(ctx, targetDir);
|
|
1553
|
-
} else {
|
|
1554
|
-
await showManualWizard(ctx, targetDir);
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
async function showGenerateWizard(ctx: ExtensionCommandContext, targetDir: string) {
|
|
1559
|
-
const description = await ctx.ui.input("Describe what this agent should do");
|
|
1560
|
-
if (!description) return;
|
|
1561
|
-
|
|
1562
|
-
const name = await ctx.ui.input("Agent name (filename, no spaces)");
|
|
1563
|
-
if (!name) return;
|
|
1564
|
-
|
|
1565
|
-
mkdirSync(targetDir, { recursive: true });
|
|
1566
|
-
|
|
1567
|
-
const targetPath = join(targetDir, `${name}.md`);
|
|
1568
|
-
if (existsSync(targetPath)) {
|
|
1569
|
-
const overwrite = await ctx.ui.confirm("Overwrite", `${targetPath} already exists. Overwrite?`);
|
|
1570
|
-
if (!overwrite) return;
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
ctx.ui.notify("Generating agent definition...", "info");
|
|
1574
|
-
|
|
1575
|
-
const generatePrompt = `Create a custom pi sub-agent definition file based on this description: "${description}"
|
|
1576
|
-
|
|
1577
|
-
Write a markdown file to: ${targetPath}
|
|
1578
|
-
|
|
1579
|
-
The file format is a markdown file with YAML frontmatter and a system prompt body:
|
|
1580
|
-
|
|
1581
|
-
\`\`\`markdown
|
|
1582
|
-
---
|
|
1583
|
-
description: <one-line description shown in UI>
|
|
1584
|
-
tools: <comma-separated built-in tools: read, bash, edit, write, grep, find, ls. Use "none" for no tools. Omit for all tools>
|
|
1585
|
-
model: <optional model as "provider/modelId", e.g. "anthropic/claude-haiku-4-5-20251001". Omit to inherit parent model>
|
|
1586
|
-
thinking: <optional thinking level: off, minimal, low, medium, high, xhigh. Omit to inherit>
|
|
1587
|
-
max_turns: <optional max agentic turns. 0 or omit for unlimited (default)>
|
|
1588
|
-
prompt_mode: <"replace" (body IS the full system prompt) or "append" (body is appended to default prompt). Default: replace>
|
|
1589
|
-
extensions: <true (inherit all MCP/extension tools), false (none), or comma-separated names. Default: true>
|
|
1590
|
-
skills: <true (inherit all), false (none), or comma-separated skill names to preload into prompt. Default: true>
|
|
1591
|
-
disallowed_tools: <comma-separated tool names to block, even if otherwise available. Omit for none>
|
|
1592
|
-
inherit_context: <true to fork parent conversation into agent so it sees chat history. Default: false>
|
|
1593
|
-
run_in_background: <true to run in background by default. Default: false>
|
|
1594
|
-
isolated: <true for no extension/MCP tools, only built-in tools. Default: false>
|
|
1595
|
-
memory: <"user" (global), "project" (per-project), or "local" (gitignored per-project) for persistent memory. Omit for none>
|
|
1596
|
-
isolation: <"worktree" to run in isolated git worktree. Omit for normal>
|
|
1597
|
-
---
|
|
1598
|
-
|
|
1599
|
-
<system prompt body — instructions for the agent>
|
|
1600
|
-
\`\`\`
|
|
1601
|
-
|
|
1602
|
-
Guidelines for choosing settings:
|
|
1603
|
-
- For read-only tasks (review, analysis): tools: read, bash, grep, find, ls
|
|
1604
|
-
- For code modification tasks: include edit, write
|
|
1605
|
-
- Use prompt_mode: append if the agent should keep the default system prompt and add specialization on top
|
|
1606
|
-
- Use prompt_mode: replace for fully custom agents with their own personality/instructions
|
|
1607
|
-
- Set inherit_context: true if the agent needs to know what was discussed in the parent conversation
|
|
1608
|
-
- Set isolated: true if the agent should NOT have access to MCP servers or other extensions
|
|
1609
|
-
- Only include frontmatter fields that differ from defaults — omit fields where the default is fine
|
|
1610
|
-
|
|
1611
|
-
Write the file using the write tool. Only write the file, nothing else.`;
|
|
1612
|
-
|
|
1613
|
-
const record = await manager.spawnAndWait(pi, ctx, "general-purpose", generatePrompt, {
|
|
1614
|
-
description: `Generate ${name} agent`,
|
|
1615
|
-
maxTurns: 5,
|
|
1616
|
-
});
|
|
1617
|
-
|
|
1618
|
-
if (record.status === "error") {
|
|
1619
|
-
ctx.ui.notify(`Generation failed: ${record.error}`, "warning");
|
|
1620
|
-
return;
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
reloadCustomAgents();
|
|
1624
|
-
|
|
1625
|
-
if (existsSync(targetPath)) {
|
|
1626
|
-
ctx.ui.notify(`Created ${targetPath}`, "info");
|
|
1627
|
-
} else {
|
|
1628
|
-
ctx.ui.notify("Agent generation completed but file was not created. Check the agent output.", "warning");
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
async function showManualWizard(ctx: ExtensionCommandContext, targetDir: string) {
|
|
1633
|
-
// 1. Name
|
|
1634
|
-
const name = await ctx.ui.input("Agent name (filename, no spaces)");
|
|
1635
|
-
if (!name) return;
|
|
1636
|
-
|
|
1637
|
-
// 2. Description
|
|
1638
|
-
const description = await ctx.ui.input("Description (one line)");
|
|
1639
|
-
if (!description) return;
|
|
1640
|
-
|
|
1641
|
-
// 3. Tools
|
|
1642
|
-
const toolChoice = await ctx.ui.select("Tools", ["all", "none", "read-only (read, bash, grep, find, ls)", "custom..."]);
|
|
1643
|
-
if (!toolChoice) return;
|
|
1644
|
-
|
|
1645
|
-
let tools: string;
|
|
1646
|
-
if (toolChoice === "all") {
|
|
1647
|
-
tools = BUILTIN_TOOL_NAMES.join(", ");
|
|
1648
|
-
} else if (toolChoice === "none") {
|
|
1649
|
-
tools = "none";
|
|
1650
|
-
} else if (toolChoice.startsWith("read-only")) {
|
|
1651
|
-
tools = "read, bash, grep, find, ls";
|
|
1652
|
-
} else {
|
|
1653
|
-
const customTools = await ctx.ui.input("Tools (comma-separated)", BUILTIN_TOOL_NAMES.join(", "));
|
|
1654
|
-
if (!customTools) return;
|
|
1655
|
-
tools = customTools;
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
// 4. Model
|
|
1659
|
-
const modelChoice = await ctx.ui.select("Model", [
|
|
1660
|
-
"inherit (parent model)",
|
|
1661
|
-
"haiku",
|
|
1662
|
-
"sonnet",
|
|
1663
|
-
"opus",
|
|
1664
|
-
"custom...",
|
|
1665
|
-
]);
|
|
1666
|
-
if (!modelChoice) return;
|
|
1667
|
-
|
|
1668
|
-
let modelLine = "";
|
|
1669
|
-
if (modelChoice === "haiku") modelLine = "\nmodel: anthropic/claude-haiku-4-5-20251001";
|
|
1670
|
-
else if (modelChoice === "sonnet") modelLine = "\nmodel: anthropic/claude-sonnet-4-6";
|
|
1671
|
-
else if (modelChoice === "opus") modelLine = "\nmodel: anthropic/claude-opus-4-6";
|
|
1672
|
-
else if (modelChoice === "custom...") {
|
|
1673
|
-
const customModel = await ctx.ui.input("Model (provider/modelId)");
|
|
1674
|
-
if (customModel) modelLine = `\nmodel: ${customModel}`;
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
// 5. Thinking
|
|
1678
|
-
const thinkingChoice = await ctx.ui.select("Thinking level", [
|
|
1679
|
-
"inherit",
|
|
1680
|
-
"off",
|
|
1681
|
-
"minimal",
|
|
1682
|
-
"low",
|
|
1683
|
-
"medium",
|
|
1684
|
-
"high",
|
|
1685
|
-
"xhigh",
|
|
1686
|
-
]);
|
|
1687
|
-
if (!thinkingChoice) return;
|
|
1688
|
-
|
|
1689
|
-
let thinkingLine = "";
|
|
1690
|
-
if (thinkingChoice !== "inherit") thinkingLine = `\nthinking: ${thinkingChoice}`;
|
|
1691
|
-
|
|
1692
|
-
// 6. System prompt
|
|
1693
|
-
const systemPrompt = await ctx.ui.editor("System prompt", "");
|
|
1694
|
-
if (systemPrompt === undefined) return;
|
|
1695
|
-
|
|
1696
|
-
// Build the file
|
|
1697
|
-
const content = `---
|
|
1698
|
-
description: ${description}
|
|
1699
|
-
tools: ${tools}${modelLine}${thinkingLine}
|
|
1700
|
-
prompt_mode: replace
|
|
1701
|
-
---
|
|
1702
|
-
|
|
1703
|
-
${systemPrompt}
|
|
1704
|
-
`;
|
|
1705
|
-
|
|
1706
|
-
mkdirSync(targetDir, { recursive: true });
|
|
1707
|
-
const targetPath = join(targetDir, `${name}.md`);
|
|
1708
|
-
|
|
1709
|
-
if (existsSync(targetPath)) {
|
|
1710
|
-
const overwrite = await ctx.ui.confirm("Overwrite", `${targetPath} already exists. Overwrite?`);
|
|
1711
|
-
if (!overwrite) return;
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
const { writeFileSync } = await import("node:fs");
|
|
1715
|
-
writeFileSync(targetPath, content, "utf-8");
|
|
1716
|
-
reloadCustomAgents();
|
|
1717
|
-
ctx.ui.notify(`Created ${targetPath}`, "info");
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
async function showSettings(ctx: ExtensionCommandContext) {
|
|
1721
|
-
const choice = await ctx.ui.select("Settings", [
|
|
1722
|
-
`Max concurrency (current: ${manager.getMaxConcurrent()})`,
|
|
1723
|
-
`Default max turns (current: ${getDefaultMaxTurns() ?? "unlimited"})`,
|
|
1724
|
-
`Grace turns (current: ${getGraceTurns()})`,
|
|
1725
|
-
`Join mode (current: ${getDefaultJoinMode()})`,
|
|
1726
|
-
]);
|
|
1727
|
-
if (!choice) return;
|
|
1728
|
-
|
|
1729
|
-
if (choice.startsWith("Max concurrency")) {
|
|
1730
|
-
const val = await ctx.ui.input("Max concurrent background agents", String(manager.getMaxConcurrent()));
|
|
1731
|
-
if (val) {
|
|
1732
|
-
const n = parseInt(val, 10);
|
|
1733
|
-
if (n >= 1) {
|
|
1734
|
-
manager.setMaxConcurrent(n);
|
|
1735
|
-
ctx.ui.notify(`Max concurrency set to ${n}`, "info");
|
|
1736
|
-
} else {
|
|
1737
|
-
ctx.ui.notify("Must be a positive integer.", "warning");
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
} else if (choice.startsWith("Default max turns")) {
|
|
1741
|
-
const val = await ctx.ui.input("Default max turns before wrap-up (0 = unlimited)", String(getDefaultMaxTurns() ?? 0));
|
|
1742
|
-
if (val) {
|
|
1743
|
-
const n = parseInt(val, 10);
|
|
1744
|
-
if (n === 0) {
|
|
1745
|
-
setDefaultMaxTurns(undefined);
|
|
1746
|
-
ctx.ui.notify("Default max turns set to unlimited", "info");
|
|
1747
|
-
} else if (n >= 1) {
|
|
1748
|
-
setDefaultMaxTurns(n);
|
|
1749
|
-
ctx.ui.notify(`Default max turns set to ${n}`, "info");
|
|
1750
|
-
} else {
|
|
1751
|
-
ctx.ui.notify("Must be 0 (unlimited) or a positive integer.", "warning");
|
|
1752
|
-
}
|
|
1753
|
-
}
|
|
1754
|
-
} else if (choice.startsWith("Grace turns")) {
|
|
1755
|
-
const val = await ctx.ui.input("Grace turns after wrap-up steer", String(getGraceTurns()));
|
|
1756
|
-
if (val) {
|
|
1757
|
-
const n = parseInt(val, 10);
|
|
1758
|
-
if (n >= 1) {
|
|
1759
|
-
setGraceTurns(n);
|
|
1760
|
-
ctx.ui.notify(`Grace turns set to ${n}`, "info");
|
|
1761
|
-
} else {
|
|
1762
|
-
ctx.ui.notify("Must be a positive integer.", "warning");
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
} else if (choice.startsWith("Join mode")) {
|
|
1766
|
-
const val = await ctx.ui.select("Default join mode for background agents", [
|
|
1767
|
-
"smart — auto-group 2+ agents in same turn (default)",
|
|
1768
|
-
"async — always notify individually",
|
|
1769
|
-
"group — always group background agents",
|
|
1770
|
-
]);
|
|
1771
|
-
if (val) {
|
|
1772
|
-
const mode = val.split(" ")[0] as JoinMode;
|
|
1773
|
-
setDefaultJoinMode(mode);
|
|
1774
|
-
ctx.ui.notify(`Default join mode set to ${mode}`, "info");
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
pi.registerCommand("agents", {
|
|
1780
|
-
description: "Manage agents",
|
|
1781
|
-
handler: async (_args, ctx) => { await showAgentsMenu(ctx); },
|
|
1782
|
-
});
|
|
1783
|
-
|
|
1784
|
-
(globalThis as any)[Symbol.for("pi-subagents:menu")] = {
|
|
1785
|
-
showMenu: async (ctx: ExtensionCommandContext) => showAgentsMenu(ctx),
|
|
1786
|
-
};
|
|
1787
1547
|
}
|