@ferris1225/pi-subagents 4.2.5 → 4.2.8
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/README.md +375 -346
- package/agents/executor.md +53 -53
- package/package.json +3 -5
- package/src/agents.ts +237 -237
- package/src/announcements.ts +81 -78
- package/src/dispatch.ts +615 -541
- package/src/durable.ts +517 -510
- package/src/format.ts +181 -165
- package/src/index.ts +102 -100
- package/src/monitor.ts +1 -1
- package/src/prompt.ts +69 -69
- package/src/rpc-run.ts +987 -993
- package/src/runtime.ts +348 -312
- package/src/status.ts +66 -0
- package/src/thread-lifecycle.ts +1341 -1324
- package/src/tools.ts +384 -384
- package/src/widget.ts +268 -266
- package/src/worktree.ts +974 -943
package/src/format.ts
CHANGED
|
@@ -1,165 +1,181 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure formatting/result helpers shared by the subagent tool and completion
|
|
3
|
-
* delivery: usage rendering, completion blocks, synthetic result constructors,
|
|
4
|
-
* and run-id matching.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { AgentConfig } from "./agents.ts";
|
|
8
|
-
import {
|
|
9
|
-
import { emptyUsage } from "./rpc-run.ts";
|
|
10
|
-
import {
|
|
11
|
-
getResultOutput,
|
|
12
|
-
isFailedResult,
|
|
13
|
-
truncateResultOutput,
|
|
14
|
-
writeResultArtifact,
|
|
15
|
-
type SingleResult,
|
|
16
|
-
type UsageStats,
|
|
17
|
-
} from "./spawn.ts";
|
|
18
|
-
|
|
19
|
-
export function queuedResult(agent: AgentConfig, task: string, thinking?: string): SingleResult {
|
|
20
|
-
return {
|
|
21
|
-
agent: agent.name,
|
|
22
|
-
task,
|
|
23
|
-
exitCode: -1,
|
|
24
|
-
messages: [],
|
|
25
|
-
stderr: "",
|
|
26
|
-
usage: emptyUsage(),
|
|
27
|
-
model: agent.model,
|
|
28
|
-
...(thinking ? { thinking } : {}),
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function failedStartResult(agentName: string, task: string, errorMessage: string): SingleResult {
|
|
33
|
-
return {
|
|
34
|
-
agent: agentName,
|
|
35
|
-
task,
|
|
36
|
-
exitCode: 1,
|
|
37
|
-
messages: [],
|
|
38
|
-
stderr: errorMessage,
|
|
39
|
-
usage: emptyUsage(),
|
|
40
|
-
errorMessage,
|
|
41
|
-
dispatchFailed: true,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Failed result for a background task that crashed with an exception (spawn
|
|
46
|
-
* infra, delivery API, ...) instead of returning a normal result. */
|
|
47
|
-
export function dispatchFailedResult(agent: AgentConfig, task: string, error: unknown, thinking?: string): SingleResult {
|
|
48
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
49
|
-
return {
|
|
50
|
-
...queuedResult(agent, task, thinking),
|
|
51
|
-
exitCode: 1,
|
|
52
|
-
stderr: errorMessage,
|
|
53
|
-
stopReason: "error",
|
|
54
|
-
errorMessage,
|
|
55
|
-
dispatchFailed: true,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function formatTokens(count: number): string {
|
|
60
|
-
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
61
|
-
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k`;
|
|
62
|
-
return String(count);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function formatUsage(usage: UsageStats): string {
|
|
66
|
-
const parts: string[] = [];
|
|
67
|
-
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
68
|
-
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
|
|
69
|
-
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
|
70
|
-
if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
|
|
71
|
-
if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
|
|
72
|
-
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
73
|
-
return parts.join(" ");
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface CompletionFormatOptions {
|
|
77
|
-
/** Project-scoped directory the full result is written to when the output
|
|
78
|
-
* is truncated: <projectRoot>/results. */
|
|
79
|
-
resultRoot?: string;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Pure formatting/result helpers shared by the subagent tool and completion
|
|
3
|
+
* delivery: usage rendering, completion blocks, synthetic result constructors,
|
|
4
|
+
* and run-id matching.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AgentConfig } from "./agents.ts";
|
|
8
|
+
import { runLabel, shrinkRunLabel } from "./monitor.ts";
|
|
9
|
+
import { emptyUsage } from "./rpc-run.ts";
|
|
10
|
+
import {
|
|
11
|
+
getResultOutput,
|
|
12
|
+
isFailedResult,
|
|
13
|
+
truncateResultOutput,
|
|
14
|
+
writeResultArtifact,
|
|
15
|
+
type SingleResult,
|
|
16
|
+
type UsageStats,
|
|
17
|
+
} from "./spawn.ts";
|
|
18
|
+
|
|
19
|
+
export function queuedResult(agent: AgentConfig, task: string, thinking?: string): SingleResult {
|
|
20
|
+
return {
|
|
21
|
+
agent: agent.name,
|
|
22
|
+
task,
|
|
23
|
+
exitCode: -1,
|
|
24
|
+
messages: [],
|
|
25
|
+
stderr: "",
|
|
26
|
+
usage: emptyUsage(),
|
|
27
|
+
model: agent.model,
|
|
28
|
+
...(thinking ? { thinking } : {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function failedStartResult(agentName: string, task: string, errorMessage: string): SingleResult {
|
|
33
|
+
return {
|
|
34
|
+
agent: agentName,
|
|
35
|
+
task,
|
|
36
|
+
exitCode: 1,
|
|
37
|
+
messages: [],
|
|
38
|
+
stderr: errorMessage,
|
|
39
|
+
usage: emptyUsage(),
|
|
40
|
+
errorMessage,
|
|
41
|
+
dispatchFailed: true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Failed result for a background task that crashed with an exception (spawn
|
|
46
|
+
* infra, delivery API, ...) instead of returning a normal result. */
|
|
47
|
+
export function dispatchFailedResult(agent: AgentConfig, task: string, error: unknown, thinking?: string): SingleResult {
|
|
48
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
49
|
+
return {
|
|
50
|
+
...queuedResult(agent, task, thinking),
|
|
51
|
+
exitCode: 1,
|
|
52
|
+
stderr: errorMessage,
|
|
53
|
+
stopReason: "error",
|
|
54
|
+
errorMessage,
|
|
55
|
+
dispatchFailed: true,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function formatTokens(count: number): string {
|
|
60
|
+
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
61
|
+
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k`;
|
|
62
|
+
return String(count);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function formatUsage(usage: UsageStats): string {
|
|
66
|
+
const parts: string[] = [];
|
|
67
|
+
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
68
|
+
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
|
|
69
|
+
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
|
70
|
+
if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
|
|
71
|
+
if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
|
|
72
|
+
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
73
|
+
return parts.join(" ");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface CompletionFormatOptions {
|
|
77
|
+
/** Project-scoped directory the full result is written to when the output
|
|
78
|
+
* is truncated: <projectRoot>/results. */
|
|
79
|
+
resultRoot?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Display width of the task-derived label folded into the completion heading:
|
|
83
|
+
* tight enough to stay a hint rather than a second copy of the task. */
|
|
84
|
+
const COMPLETION_LABEL_MAX = 24;
|
|
85
|
+
|
|
86
|
+
export function formatCompletionBlock(
|
|
87
|
+
result: SingleResult,
|
|
88
|
+
maxResultLines: number,
|
|
89
|
+
options: CompletionFormatOptions = {},
|
|
90
|
+
): string {
|
|
91
|
+
const failed = isFailedResult(result);
|
|
92
|
+
const failedTools = result.failedTools ?? [];
|
|
93
|
+
const status = failed ? "failed" : "completed";
|
|
94
|
+
const usage = formatUsage(result.usage);
|
|
95
|
+
const output = getResultOutput(result);
|
|
96
|
+
const { text, truncated } = truncateResultOutput(output, maxResultLines);
|
|
97
|
+
const fallbackNote = result.modelFallbackFrom
|
|
98
|
+
? ` (selected model ${result.modelFallbackFrom} failed → main ${result.model ?? "dynamic default"})`
|
|
99
|
+
: "";
|
|
100
|
+
const startupRetryNote = result.startupRetries
|
|
101
|
+
? ` (recovered after ${result.startupRetries} startup retr${result.startupRetries === 1 ? "y" : "ies"} — concurrent pi startup race)`
|
|
102
|
+
: "";
|
|
103
|
+
const runNote = result.runId !== undefined ? ` · run #${result.runId}` : "";
|
|
104
|
+
// A short task-derived label, not the task itself: the parent authored the
|
|
105
|
+
// task and still has it in context, but a wide fan-out of same-agent runs
|
|
106
|
+
// needs more than a run id to tell completions apart.
|
|
107
|
+
const label = shrinkRunLabel(runLabel(result.task), COMPLETION_LABEL_MAX);
|
|
108
|
+
const lines = [
|
|
109
|
+
`### [${result.agent}${label ? `·${label}` : ""}] ${status}${usage ? ` (${usage})` : ""}${fallbackNote}${startupRetryNote}${runNote}`,
|
|
110
|
+
"",
|
|
111
|
+
];
|
|
112
|
+
if (result.isolation === "worktree") {
|
|
113
|
+
const isolation =
|
|
114
|
+
result.integrationStatus === "integrated"
|
|
115
|
+
? "worktree · changes integrated into the original working tree"
|
|
116
|
+
: result.integrationStatus === "no_changes"
|
|
117
|
+
? "worktree · no changes; temporary worktree removed"
|
|
118
|
+
: result.integrationStatus === "retained"
|
|
119
|
+
? result.integrationApplied
|
|
120
|
+
? "worktree · changes applied, but cleanup failed; recovery artifacts retained"
|
|
121
|
+
: "worktree · integration failed; recovery artifacts retained"
|
|
122
|
+
: "worktree · isolated";
|
|
123
|
+
lines.push(`Isolation: ${isolation}`);
|
|
124
|
+
if (result.integrationWorktreePath) lines.push(`Retained worktree: ${result.integrationWorktreePath}`);
|
|
125
|
+
if (result.integrationPatchPath) lines.push(`Retained patch: ${result.integrationPatchPath}`);
|
|
126
|
+
if (result.integrationError) lines.push(`Integration error: ${result.integrationError}`);
|
|
127
|
+
lines.push("");
|
|
128
|
+
}
|
|
129
|
+
lines.push(text);
|
|
130
|
+
// Failed-tool diagnostics ride along only when the run itself failed: they
|
|
131
|
+
// explain the failure, while on a successful run a transient failed call
|
|
132
|
+
// (no-match grep, rejected edit) is noise the agent already worked around.
|
|
133
|
+
if (failed && failedTools.length > 0) {
|
|
134
|
+
lines.push(
|
|
135
|
+
"",
|
|
136
|
+
`⚠ ${failedTools.length} failed tool call${failedTools.length === 1 ? "" : "s"}:`,
|
|
137
|
+
...failedTools.map((tool) => `- ${tool.toolName}: ${tool.error.trim() || "(no output)"}`),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (truncated) {
|
|
141
|
+
// The full text lives on disk so the main agent can read it on demand.
|
|
142
|
+
const artifact = options.resultRoot
|
|
143
|
+
? writeResultArtifact(output, result.agent, options.resultRoot)
|
|
144
|
+
: "(result root unavailable)";
|
|
145
|
+
// State the loss (shown vs total) and condition the read: handing the
|
|
146
|
+
// parent both a summary and a full-text entrance invites the same content
|
|
147
|
+
// into its context twice.
|
|
148
|
+
const totalLines = output.split("\n").length;
|
|
149
|
+
lines.push("", `(${maxResultLines} of ${totalLines} lines shown; full result ${artifact} — read only if these are insufficient)`);
|
|
150
|
+
}
|
|
151
|
+
return lines.join("\n");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Instruction appended to a model-level failure: the sub-agent's provider never
|
|
155
|
+
* produced usable output (or the run stalled), so the task is handed back to the
|
|
156
|
+
* main window instead of being left as a dead failure. When the run preserved a
|
|
157
|
+
* session with earlier work (and the run id is known), steer the main agent to
|
|
158
|
+
* RESUME it in-context once a model is available, instead of re-dispatching
|
|
159
|
+
* fresh (which would re-scan everything). */
|
|
160
|
+
export function modelLevelTakeoverNote(result: SingleResult, opts?: { runId?: number }): string {
|
|
161
|
+
const retry = result.modelFallbackFrom ? ", and the current main model also failed" : "";
|
|
162
|
+
const detail = result.errorMessage?.trim();
|
|
163
|
+
const cause = detail
|
|
164
|
+
? `its model/provider call failed (${detail})`
|
|
165
|
+
: "its model was unavailable or failed (or the run stalled)";
|
|
166
|
+
const sessionPreserved = Boolean(result.sessionDir && result.sessionId) && opts?.runId !== undefined;
|
|
167
|
+
const recovery = sessionPreserved
|
|
168
|
+
? ` The sub-agent's earlier work in this run is preserved. Once a model is available again, call subagent_control with { action: "resume", id: ${opts!.runId} } to CONTINUE it in-context (it keeps the same run id and does not re-scan), or execute the task in the main window with your own tools.`
|
|
169
|
+
: ` Please execute this task in the main window with your own tools; do not re-dispatch it as a sub-agent.`;
|
|
170
|
+
return `The sub-agent could not complete this task: ${cause}${retry}.${recovery}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Resolve a run-id request to actual ids: an exact numeric match always wins
|
|
174
|
+
* (so "1" never fans out to 10, 11, …); only when no exact match exists does a
|
|
175
|
+
* prefix match run, as a convenience for partial ids. Keeps single-digit lookups
|
|
176
|
+
* from returning — or, for subagent_stop, acting on — a whole prefix family. */
|
|
177
|
+
export function matchRunIds(ids: number[], requested: string): number[] {
|
|
178
|
+
const exact = ids.filter((id) => String(id) === requested);
|
|
179
|
+
if (exact.length > 0) return exact;
|
|
180
|
+
return ids.filter((id) => String(id).startsWith(requested));
|
|
181
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,100 +1,102 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pi-subagents — focused sub-agent delegation for pi.
|
|
3
|
-
*
|
|
4
|
-
* Assembly point: builds the shared runtime and registers everything.
|
|
5
|
-
* The heavy lifting lives in focused modules:
|
|
6
|
-
* - dispatch.ts — tool contract, managed role policy, internal steps
|
|
7
|
-
* - thread-lifecycle.ts — stable generations, controls, final integration/delivery
|
|
8
|
-
* - tools.ts — subagent_control / subagent_stop
|
|
9
|
-
* - announcements.ts — session-start recovery, notices, and widget install
|
|
10
|
-
* - widget.ts — active-only TUI run status
|
|
11
|
-
* - runtime.ts — shared per-session state
|
|
12
|
-
*
|
|
13
|
-
* Also registers the `/subagents-setup` command and a `before_agent_start` hook
|
|
14
|
-
* that injects a delegation directive into the parent system prompt so the main
|
|
15
|
-
* model uses the tool proactively.
|
|
16
|
-
*
|
|
17
|
-
* The tool is not registered inside child sub-agent processes, which prevents
|
|
18
|
-
* runaway recursion and keeps child context windows clean.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
22
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
23
|
-
import { discoverAgents } from "./agents.ts";
|
|
24
|
-
import { registerAnnouncements } from "./announcements.ts";
|
|
25
|
-
import { getConfigPath, loadConfig } from "./config.ts";
|
|
26
|
-
import { registerSubagentTool } from "./dispatch.ts";
|
|
27
|
-
import { matchRunIds } from "./format.ts";
|
|
28
|
-
import { buildDelegationDirective } from "./prompt.ts";
|
|
29
|
-
import { createRuntime } from "./runtime.ts";
|
|
30
|
-
import { runSetup } from "./setup.ts";
|
|
31
|
-
import { currentSubagentDepth } from "./spawn.ts";
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
0,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
pi
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* pi-subagents — focused sub-agent delegation for pi.
|
|
3
|
+
*
|
|
4
|
+
* Assembly point: builds the shared runtime and registers everything.
|
|
5
|
+
* The heavy lifting lives in focused modules:
|
|
6
|
+
* - dispatch.ts — tool contract, managed role policy, internal steps
|
|
7
|
+
* - thread-lifecycle.ts — stable generations, controls, final integration/delivery
|
|
8
|
+
* - tools.ts — subagent_control / subagent_stop
|
|
9
|
+
* - announcements.ts — session-start recovery, notices, and widget install
|
|
10
|
+
* - widget.ts — active-only TUI run status
|
|
11
|
+
* - runtime.ts — shared per-session state
|
|
12
|
+
*
|
|
13
|
+
* Also registers the `/subagents-setup` command and a `before_agent_start` hook
|
|
14
|
+
* that injects a delegation directive into the parent system prompt so the main
|
|
15
|
+
* model uses the tool proactively.
|
|
16
|
+
*
|
|
17
|
+
* The tool is not registered inside child sub-agent processes, which prevents
|
|
18
|
+
* runaway recursion and keeps child context windows clean.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
22
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
23
|
+
import { discoverAgents } from "./agents.ts";
|
|
24
|
+
import { registerAnnouncements } from "./announcements.ts";
|
|
25
|
+
import { getConfigPath, loadConfig } from "./config.ts";
|
|
26
|
+
import { registerSubagentTool } from "./dispatch.ts";
|
|
27
|
+
import { matchRunIds } from "./format.ts";
|
|
28
|
+
import { buildDelegationDirective } from "./prompt.ts";
|
|
29
|
+
import { createRuntime } from "./runtime.ts";
|
|
30
|
+
import { runSetup } from "./setup.ts";
|
|
31
|
+
import { currentSubagentDepth } from "./spawn.ts";
|
|
32
|
+
import { clearActiveRunsStatus } from "./status.ts";
|
|
33
|
+
import { bootstrapDurableState } from "./thread-lifecycle.ts";
|
|
34
|
+
import { registerLookupTools } from "./tools.ts";
|
|
35
|
+
import { clearActiveRunsWidget } from "./widget.ts";
|
|
36
|
+
|
|
37
|
+
export { matchRunIds };
|
|
38
|
+
|
|
39
|
+
export default function (pi: ExtensionAPI): void {
|
|
40
|
+
const configPath = getConfigPath(getAgentDir());
|
|
41
|
+
const runtime = createRuntime(pi, configPath);
|
|
42
|
+
|
|
43
|
+
// Recursion guard: sub-agent children are leaf processes. The `subagent` tool is
|
|
44
|
+
// excluded from their toolset at spawn (--exclude-tools); this check is defense
|
|
45
|
+
// in depth so a child can never expose the tool back to its model, even if
|
|
46
|
+
// another extension ignores the depth marker.
|
|
47
|
+
if (currentSubagentDepth() >= 1) {
|
|
48
|
+
pi.registerCommand("subagents-setup", {
|
|
49
|
+
description: "Configure pi-subagents (unavailable in nested sub-agent processes)",
|
|
50
|
+
handler: async (_args, ctx) => {
|
|
51
|
+
ctx.ui.notify("pi-subagents setup is unavailable in nested sub-agent processes.", "warning");
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
pi.registerMessageRenderer("subagent-result", (message, _options, theme) =>
|
|
58
|
+
new Text(
|
|
59
|
+
`${theme.fg("toolTitle", theme.bold("subagent result"))}\n${message.content}`,
|
|
60
|
+
0,
|
|
61
|
+
0,
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
66
|
+
clearActiveRunsStatus(ctx);
|
|
67
|
+
clearActiveRunsWidget(ctx);
|
|
68
|
+
await runtime.shutdown();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
registerSubagentTool(pi, runtime);
|
|
72
|
+
registerLookupTools(pi, runtime);
|
|
73
|
+
|
|
74
|
+
pi.registerCommand("subagents-setup", {
|
|
75
|
+
description: "Configure pi-subagents: agents, selected models, capability-aware thinking, and runtime settings",
|
|
76
|
+
handler: async (_args, ctx) => {
|
|
77
|
+
await runSetup(ctx, configPath);
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
registerAnnouncements(pi, runtime);
|
|
82
|
+
|
|
83
|
+
// Durable bootstrap: restore parked threads from the manifest so a reload or
|
|
84
|
+
// restart keeps status and resume working, then age out old records and sweep
|
|
85
|
+
// leaked temp/state directories. Registration never blocks on it and every
|
|
86
|
+
// stage is best-effort; the restore pass is published as runtime.durableRestore
|
|
87
|
+
// so the tools and the session-start notice wait for it instead of racing it.
|
|
88
|
+
void bootstrapDurableState(runtime);
|
|
89
|
+
|
|
90
|
+
// Proactive dispatch: inject the delegation directive into the parent system prompt.
|
|
91
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
92
|
+
const config = await loadConfig(configPath);
|
|
93
|
+
const { agents } = discoverAgents(ctx.cwd, {
|
|
94
|
+
scope: config.agentScope,
|
|
95
|
+
enabledNames: config.enabledAgents,
|
|
96
|
+
projectTrusted: ctx.isProjectTrusted?.() === true,
|
|
97
|
+
});
|
|
98
|
+
const directive = buildDelegationDirective(agents);
|
|
99
|
+
if (!directive) return undefined;
|
|
100
|
+
return { systemPrompt: `${event.systemPrompt}\n${directive}` };
|
|
101
|
+
});
|
|
102
|
+
}
|
package/src/monitor.ts
CHANGED
|
@@ -242,7 +242,7 @@ export const RUN_LABEL_MAX = 32;
|
|
|
242
242
|
* distinguishing fragment so concurrent same-agent runs are told apart by WHAT
|
|
243
243
|
* they do, not just their run id. A file/directory path outranks other
|
|
244
244
|
* fragment kinds (a kebab-case word like "edge-case" never beats
|
|
245
|
-
* "
|
|
245
|
+
* "src/config.ts"); a long path keeps its tail (the filename is the
|
|
246
246
|
* recognisable part); a task with no recognizable fragment falls back to a
|
|
247
247
|
* head slice of its prose. Grapheme-safe.
|
|
248
248
|
*/
|