@ferris1225/pi-subagents 0.28.0 → 0.31.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/README.md +270 -597
- package/agents/explore.md +1 -0
- package/agents/reviewer.md +1 -0
- package/agents/worker.md +1 -1
- package/package.json +1 -1
- package/src/completion.ts +28 -0
- package/src/config.ts +25 -0
- package/src/dispatch.ts +845 -0
- package/src/format.ts +149 -0
- package/src/index.ts +90 -1386
- package/src/models.ts +13 -0
- package/src/monitor.ts +27 -0
- package/src/prompt.ts +10 -0
- package/src/runtime.ts +145 -0
- package/src/setup.ts +50 -0
- package/src/spawn.ts +189 -68
- package/src/tools.ts +409 -0
- package/src/ui.ts +8 -3
- package/src/widget.ts +182 -0
package/src/format.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure formatting/result helpers shared by the subagent tool and the lookup
|
|
3
|
+
* tools (subagent_wait/status): usage rendering, completion blocks, synthetic
|
|
4
|
+
* result constructors, and run-id matching.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AgentConfig } from "./agents.ts";
|
|
8
|
+
import { formatTaskSummary } from "./monitor.ts";
|
|
9
|
+
import {
|
|
10
|
+
getResultOutput,
|
|
11
|
+
isFailedResult,
|
|
12
|
+
truncateResultOutput,
|
|
13
|
+
writeResultArtifact,
|
|
14
|
+
type SingleResult,
|
|
15
|
+
type UsageStats,
|
|
16
|
+
} from "./spawn.ts";
|
|
17
|
+
|
|
18
|
+
export function emptyUsage(): UsageStats {
|
|
19
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function queuedResult(agent: AgentConfig, task: string, thinking?: string): SingleResult {
|
|
23
|
+
return {
|
|
24
|
+
agent: agent.name,
|
|
25
|
+
agentSource: agent.source,
|
|
26
|
+
task,
|
|
27
|
+
exitCode: -1,
|
|
28
|
+
messages: [],
|
|
29
|
+
stderr: "",
|
|
30
|
+
usage: emptyUsage(),
|
|
31
|
+
model: agent.model,
|
|
32
|
+
...(thinking ? { thinking } : {}),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function failedStartResult(agentName: string, task: string, errorMessage: string): SingleResult {
|
|
37
|
+
return {
|
|
38
|
+
agent: agentName,
|
|
39
|
+
agentSource: "unknown",
|
|
40
|
+
task,
|
|
41
|
+
exitCode: 1,
|
|
42
|
+
messages: [],
|
|
43
|
+
stderr: errorMessage,
|
|
44
|
+
usage: emptyUsage(),
|
|
45
|
+
errorMessage,
|
|
46
|
+
dispatchFailed: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Failed result for a background task that crashed with an exception (spawn
|
|
51
|
+
* infra, delivery API, ...) instead of returning a normal result. */
|
|
52
|
+
export function dispatchFailedResult(agent: AgentConfig, task: string, error: unknown, thinking?: string): SingleResult {
|
|
53
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
54
|
+
return {
|
|
55
|
+
...queuedResult(agent, task, thinking),
|
|
56
|
+
exitCode: 1,
|
|
57
|
+
stderr: errorMessage,
|
|
58
|
+
stopReason: "error",
|
|
59
|
+
errorMessage,
|
|
60
|
+
dispatchFailed: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function formatTokens(count: number): string {
|
|
65
|
+
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
66
|
+
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k`;
|
|
67
|
+
return String(count);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function formatUsage(usage: UsageStats): string {
|
|
71
|
+
const parts: string[] = [];
|
|
72
|
+
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
73
|
+
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
|
|
74
|
+
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
|
75
|
+
if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
|
|
76
|
+
if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
|
|
77
|
+
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
78
|
+
return parts.join(" ");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function formatCompletionBlock(result: SingleResult, maxResultLines: number, cwd?: string): string {
|
|
82
|
+
const failed = isFailedResult(result);
|
|
83
|
+
const failedTools = result.failedTools ?? [];
|
|
84
|
+
const status = failed
|
|
85
|
+
? "failed"
|
|
86
|
+
: failedTools.length > 0
|
|
87
|
+
? `completed with ${failedTools.length} failed tool call${failedTools.length === 1 ? "" : "s"}`
|
|
88
|
+
: "completed";
|
|
89
|
+
const usage = formatUsage(result.usage);
|
|
90
|
+
const output = getResultOutput(result);
|
|
91
|
+
const { text, truncated } = truncateResultOutput(output, maxResultLines);
|
|
92
|
+
const fallbackNote = result.modelFallbackFrom
|
|
93
|
+
? ` (model fell back from ${result.modelFallbackFrom} to ${result.model ?? "main-window model"})`
|
|
94
|
+
: "";
|
|
95
|
+
const startupRetryNote = result.startupRetries
|
|
96
|
+
? ` (recovered after ${result.startupRetries} startup retr${result.startupRetries === 1 ? "y" : "ies"} — concurrent pi startup race)`
|
|
97
|
+
: "";
|
|
98
|
+
const modelRetryNote = result.modelRetries
|
|
99
|
+
? ` (recovered after ${result.modelRetries} same-model retr${result.modelRetries === 1 ? "y" : "ies"} on a transient provider error)`
|
|
100
|
+
: "";
|
|
101
|
+
const lines = [`### [${result.agent}] ${status}${usage ? ` (${usage})` : ""}${fallbackNote}${startupRetryNote}${modelRetryNote}`, "", `Task: ${formatTaskSummary(result.task, 80, false)}`, "", text];
|
|
102
|
+
// A run can exit cleanly while its last tools failed (e.g. a build that broke):
|
|
103
|
+
// the final text alone may claim more than the tools achieved, so surface the
|
|
104
|
+
// failures explicitly and tell the main agent to verify before relying on it.
|
|
105
|
+
if (!failed && failedTools.length > 0) {
|
|
106
|
+
const shown = failedTools.slice(0, 3);
|
|
107
|
+
const more = failedTools.length - shown.length;
|
|
108
|
+
lines.push(
|
|
109
|
+
"",
|
|
110
|
+
`⚠ ${failedTools.length} tool call${failedTools.length === 1 ? "" : "s"} failed during this run — the final text above may not reflect a working state:`,
|
|
111
|
+
...shown.map((tool) => `- ${tool.toolName}: ${tool.error.trim() || "(no output)"}`),
|
|
112
|
+
);
|
|
113
|
+
if (more > 0) lines.push(`- … and ${more} more`);
|
|
114
|
+
lines.push("Verify the actual artifacts before relying on this report.");
|
|
115
|
+
}
|
|
116
|
+
if (truncated) {
|
|
117
|
+
// The full text lives on disk so the main agent can read it on demand.
|
|
118
|
+
lines.push("", `(output truncated to ${maxResultLines} lines; full result: ${writeResultArtifact(output, result.agent, cwd)})`);
|
|
119
|
+
}
|
|
120
|
+
return lines.join("\n");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Instruction appended to a model-level failure: the sub-agent's provider never
|
|
124
|
+
* produced usable output (or the run stalled), so the task is handed back to the
|
|
125
|
+
* main window instead of being left as a dead failure. When the run preserved a
|
|
126
|
+
* session with earlier work (and the run id is known), steer the main agent to
|
|
127
|
+
* RESUME it in-context once a model is available, instead of re-dispatching
|
|
128
|
+
* fresh (which would re-scan everything). */
|
|
129
|
+
export function modelLevelTakeoverNote(result: SingleResult, opts?: { runId?: number }): string {
|
|
130
|
+
const sameModel = result.modelRetries
|
|
131
|
+
? `, after ${result.modelRetries} same-model retr${result.modelRetries === 1 ? "y" : "ies"} on transient errors`
|
|
132
|
+
: "";
|
|
133
|
+
const retry = result.modelFallbackFrom ? ", and the resume on the main-window model also failed" : "";
|
|
134
|
+
const sessionPreserved = Boolean(result.sessionDir && result.sessionId) && opts?.runId !== undefined;
|
|
135
|
+
const recovery = sessionPreserved
|
|
136
|
+
? ` The sub-agent's earlier work in this run is preserved. Once a model is available again, call subagent with { resume: ${opts!.runId} } to CONTINUE it in-context (it picks up where it stopped — no re-scan), or execute the task in the main window with your own tools.`
|
|
137
|
+
: ` Please execute this task in the main window with your own tools; do not re-dispatch it as a sub-agent.`;
|
|
138
|
+
return `The sub-agent could not complete this task: its model was unavailable or failed (or the run stalled)${sameModel}${retry}.${recovery}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Resolve a run-id request to actual ids: an exact numeric match always wins
|
|
142
|
+
* (so "1" never fans out to 10, 11, …); only when no exact match exists does a
|
|
143
|
+
* prefix match run, as a convenience for partial ids. Keeps single-digit lookups
|
|
144
|
+
* from returning — or, for subagent_stop, acting on — a whole prefix family. */
|
|
145
|
+
export function matchRunIds(ids: number[], requested: string): number[] {
|
|
146
|
+
const exact = ids.filter((id) => String(id) === requested);
|
|
147
|
+
if (exact.length > 0) return exact;
|
|
148
|
+
return ids.filter((id) => String(id).startsWith(requested));
|
|
149
|
+
}
|