@nmzpy/pi-ember-stack 0.1.6 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -83
- package/package.json +62 -48
- package/plugins/devin-auth/extensions/index.ts +68 -7
- package/plugins/devin-auth/src/cloud-direct/auth.ts +246 -246
- package/plugins/devin-auth/src/cloud-direct/catalog.ts +246 -246
- package/plugins/devin-auth/src/cloud-direct/chat.ts +1096 -1091
- package/plugins/devin-auth/src/cloud-direct/index.ts +41 -41
- package/plugins/devin-auth/src/cloud-direct/metadata.ts +78 -78
- package/plugins/devin-auth/src/cloud-direct/wire.ts +202 -202
- package/plugins/devin-auth/src/models.ts +42 -196
- package/plugins/devin-auth/src/oauth/register-user.ts +174 -174
- package/plugins/devin-auth/src/oauth/types.ts +71 -71
- package/plugins/devin-auth/src/stream.ts +1 -1
- package/plugins/index.ts +29 -6
- package/plugins/pi-compact-tools/index.ts +35 -192
- package/plugins/pi-compact-tools/renderer.ts +589 -0
- package/plugins/pi-custom-agents/index.ts +727 -373
- package/plugins/pi-custom-agents/questionnaire-tool.ts +14 -5
- package/plugins/pi-custom-agents/subagent/agents/coder.md +1 -0
- package/plugins/pi-custom-agents/subagent/agents/scout.md +16 -37
- package/plugins/pi-custom-agents/subagent/extensions/agents.ts +18 -1
- package/plugins/pi-custom-agents/subagent/extensions/index.ts +118 -226
- package/plugins/pi-custom-agents/subagent/extensions/model.ts +96 -96
- package/plugins/pi-custom-agents/subagent/extensions/render.ts +205 -1
- package/plugins/pi-custom-agents/subagent/extensions/runner.ts +260 -170
- package/plugins/pi-custom-agents/subagent/extensions/test/render.test.ts +145 -0
- package/plugins/pi-ember-fff/index.ts +975 -0
- package/plugins/pi-ember-fff/query.ts +247 -0
- package/plugins/pi-ember-fff/test/query.test.ts +222 -0
- package/plugins/pi-ember-fff/test/renderer.test.ts +727 -0
- package/plugins/pi-ember-tps/index.ts +144 -0
- package/plugins/pi-ember-ui/ember.json +99 -0
- package/plugins/pi-ember-ui/index.ts +805 -0
- package/plugins/pi-ember-ui/mode-colors.ts +196 -0
- package/tsconfig.json +2 -1
- package/plugins/pi-custom-agents/subagent/agents/architect.md +0 -56
|
@@ -1,31 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SDK-based sub-agent runner for pi-subagent.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* - No AGENTS.md, no extensions, no skills, no prompt templates loaded.
|
|
10
|
-
* - Thinking disabled, compaction disabled, retry disabled.
|
|
11
|
-
* - In-memory session (no disk I/O).
|
|
12
|
-
* - Shared auth/model infrastructure (no re-connection).
|
|
13
|
-
*
|
|
14
|
-
* Estimated token savings vs process-spawn: ~4-11K tokens per invocation.
|
|
4
|
+
* Compared to process-spawn, this saves ~4-11K tokens per invocation by using
|
|
5
|
+
* the pi SDK directly with a minimal system prompt, no extensions, no skills,
|
|
6
|
+
* no prompt templates, no thinking, and no compaction. To avoid blocking the
|
|
7
|
+
* parent TUI thread while the subagent runs, each agent is executed in a
|
|
8
|
+
* Node worker_thread and messages its progress back.
|
|
15
9
|
*/
|
|
16
10
|
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import { Worker } from "node:worker_threads";
|
|
17
13
|
import type { Message, Model } from "@earendil-works/pi-ai";
|
|
18
|
-
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
19
14
|
import {
|
|
20
|
-
AuthStorage,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
ModelRegistry,
|
|
24
|
-
type ResourceLoader,
|
|
25
|
-
SessionManager,
|
|
26
|
-
SettingsManager,
|
|
15
|
+
type AuthStorage,
|
|
16
|
+
getAgentDir,
|
|
17
|
+
type ModelRegistry,
|
|
27
18
|
} from "@earendil-works/pi-coding-agent";
|
|
28
19
|
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Constants
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
const PARALLEL_TOOL_CALL_GUIDANCE = `
|
|
25
|
+
|
|
26
|
+
## Tool Call Efficiency
|
|
27
|
+
|
|
28
|
+
When multiple independent tool calls are needed (e.g. reading several files,
|
|
29
|
+
searching for different patterns), emit them all in a single response rather
|
|
30
|
+
than one at a time. The runtime executes independent tool calls in parallel,
|
|
31
|
+
so batching saves round-trips and reduces latency.
|
|
32
|
+
`;
|
|
33
|
+
|
|
29
34
|
// ---------------------------------------------------------------------------
|
|
30
35
|
// Types
|
|
31
36
|
// ---------------------------------------------------------------------------
|
|
@@ -52,6 +57,123 @@ export interface SubAgentResult {
|
|
|
52
57
|
errorMessage?: string;
|
|
53
58
|
}
|
|
54
59
|
|
|
60
|
+
interface WorkerInput {
|
|
61
|
+
cwd: string;
|
|
62
|
+
agentDir: string;
|
|
63
|
+
authPath: string;
|
|
64
|
+
modelsPath: string | undefined;
|
|
65
|
+
systemPrompt: string;
|
|
66
|
+
fullSystemPrompt: string;
|
|
67
|
+
task: string;
|
|
68
|
+
tools: string[];
|
|
69
|
+
model: Model<any>;
|
|
70
|
+
agentName: string;
|
|
71
|
+
thinkingLevel: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Worker script — kept inline so the package stays self-contained. Worker
|
|
76
|
+
// threads cannot load .ts via jiti, so the code is written as plain JS and
|
|
77
|
+
// executed from a data: URL.
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
const WORKER_SCRIPT = `
|
|
81
|
+
const { parentPort, workerData } = require("node:worker_threads");
|
|
82
|
+
|
|
83
|
+
const { createAgentSession, getAgentDir, loadProjectContextFiles, AuthStorage, ModelRegistry, SessionManager, SettingsManager } = require("@earendil-works/pi-coding-agent");
|
|
84
|
+
|
|
85
|
+
function post(type, payload) {
|
|
86
|
+
parentPort?.postMessage({ type, payload });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const result = {
|
|
90
|
+
agent: workerData.agentName || "subagent",
|
|
91
|
+
task: workerData.task,
|
|
92
|
+
exitCode: 0,
|
|
93
|
+
messages: [],
|
|
94
|
+
stderr: "",
|
|
95
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
96
|
+
model: workerData.model ? \`\${workerData.model.provider}/\${workerData.model.id}\` : undefined,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
async function main() {
|
|
100
|
+
const authStorage = AuthStorage.create(workerData.authPath);
|
|
101
|
+
const modelRegistry = ModelRegistry.create(authStorage, workerData.modelsPath);
|
|
102
|
+
const settingsManager = SettingsManager.inMemory({ compaction: { enabled: false }, retry: { enabled: false } });
|
|
103
|
+
|
|
104
|
+
const resourceLoader = {
|
|
105
|
+
getExtensions: () => ({ extensions: [], errors: [], runtime: {} }),
|
|
106
|
+
getSkills: () => ({ skills: [], diagnostics: [] }),
|
|
107
|
+
getPrompts: () => ({ prompts: [], diagnostics: [] }),
|
|
108
|
+
getThemes: () => ({ themes: [], diagnostics: [] }),
|
|
109
|
+
getAgentsFiles: () => ({ agentsFiles: [] }),
|
|
110
|
+
getSystemPrompt: () => workerData.fullSystemPrompt,
|
|
111
|
+
getAppendSystemPrompt: () => [],
|
|
112
|
+
extendResources: () => {},
|
|
113
|
+
reload: async () => {},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const { session } = await createAgentSession({
|
|
117
|
+
cwd: workerData.cwd,
|
|
118
|
+
model: workerData.model,
|
|
119
|
+
thinkingLevel: workerData.thinkingLevel || "off",
|
|
120
|
+
authStorage,
|
|
121
|
+
modelRegistry,
|
|
122
|
+
resourceLoader,
|
|
123
|
+
tools: workerData.tools,
|
|
124
|
+
sessionManager: SessionManager.inMemory(workerData.cwd),
|
|
125
|
+
settingsManager,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const unsubscribe = session.subscribe((event) => {
|
|
129
|
+
if (event.type === "message_end") {
|
|
130
|
+
const msg = event.message;
|
|
131
|
+
if (msg.role === "assistant") {
|
|
132
|
+
result.usage.turns++;
|
|
133
|
+
if (msg.usage) {
|
|
134
|
+
result.usage.input += msg.usage.input || 0;
|
|
135
|
+
result.usage.output += msg.usage.output || 0;
|
|
136
|
+
result.usage.cacheRead += msg.usage.cacheRead || 0;
|
|
137
|
+
result.usage.cacheWrite += msg.usage.cacheWrite || 0;
|
|
138
|
+
result.usage.cost += msg.usage.cost?.total || 0;
|
|
139
|
+
result.usage.contextTokens = msg.usage.totalTokens || 0;
|
|
140
|
+
}
|
|
141
|
+
if (!result.model && msg.model) {
|
|
142
|
+
result.model = \`\${msg.provider || "?"}/\${msg.model}\`;
|
|
143
|
+
}
|
|
144
|
+
if (msg.stopReason) result.stopReason = msg.stopReason;
|
|
145
|
+
if (msg.errorMessage) result.errorMessage = msg.errorMessage;
|
|
146
|
+
}
|
|
147
|
+
result.messages.push(msg);
|
|
148
|
+
post("message", { ...result, messages: [...result.messages] });
|
|
149
|
+
}
|
|
150
|
+
if (event.type === "agent_end" && result.messages.length === 0 && event.messages) {
|
|
151
|
+
result.messages = event.messages;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await session.prompt(workerData.task);
|
|
157
|
+
result.exitCode = result.stopReason === "aborted" ? 1 : 0;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
result.exitCode = 1;
|
|
160
|
+
result.stopReason = result.stopReason || "error";
|
|
161
|
+
result.errorMessage = result.errorMessage || (error instanceof Error ? error.message : String(error));
|
|
162
|
+
} finally {
|
|
163
|
+
unsubscribe?.();
|
|
164
|
+
try { session.dispose(); } catch {}
|
|
165
|
+
}
|
|
166
|
+
post("done", result);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
main().catch((err) => {
|
|
170
|
+
result.exitCode = 1;
|
|
171
|
+
result.stopReason = result.stopReason || "error";
|
|
172
|
+
result.errorMessage = result.errorMessage || (err instanceof Error ? err.message : String(err));
|
|
173
|
+
post("done", result);
|
|
174
|
+
});
|
|
175
|
+
`;
|
|
176
|
+
|
|
55
177
|
// ---------------------------------------------------------------------------
|
|
56
178
|
// Public API
|
|
57
179
|
// ---------------------------------------------------------------------------
|
|
@@ -81,179 +203,147 @@ export async function runSubAgent(options: {
|
|
|
81
203
|
signal,
|
|
82
204
|
agentName = "subagent",
|
|
83
205
|
thinkingLevel = "off",
|
|
84
|
-
onUpdate,
|
|
85
206
|
onMessage,
|
|
86
207
|
} = options;
|
|
87
208
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
209
|
+
if (signal?.aborted) {
|
|
210
|
+
return {
|
|
211
|
+
agent: agentName,
|
|
212
|
+
task,
|
|
213
|
+
exitCode: 1,
|
|
214
|
+
messages: [],
|
|
215
|
+
stderr: "",
|
|
216
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
217
|
+
stopReason: "aborted",
|
|
218
|
+
errorMessage: "Sub-agent aborted before start",
|
|
219
|
+
};
|
|
220
|
+
}
|
|
97
221
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
222
|
+
const agentDir = getAgentDir();
|
|
223
|
+
const authPath = getAuthStoragePath(authStorage);
|
|
224
|
+
const modelsPath = getModelsPath(modelRegistry);
|
|
225
|
+
|
|
226
|
+
const contextFiles = loadProjectContextFilesCompat({ cwd, agentDir });
|
|
227
|
+
const contextPrefix = contextFiles.length > 0
|
|
228
|
+
? contextFiles.map((f) => f.content).join("\n\n---\n\n") + "\n\n---\n\n"
|
|
229
|
+
: "";
|
|
230
|
+
const fullSystemPrompt = contextPrefix + systemPrompt + PARALLEL_TOOL_CALL_GUIDANCE;
|
|
231
|
+
|
|
232
|
+
const input: WorkerInput = {
|
|
233
|
+
cwd,
|
|
234
|
+
agentDir,
|
|
235
|
+
authPath,
|
|
236
|
+
modelsPath,
|
|
237
|
+
systemPrompt,
|
|
238
|
+
fullSystemPrompt,
|
|
239
|
+
task,
|
|
240
|
+
tools,
|
|
241
|
+
model,
|
|
242
|
+
agentName,
|
|
243
|
+
thinkingLevel,
|
|
110
244
|
};
|
|
111
245
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
});
|
|
246
|
+
return new Promise<SubAgentResult>((resolve, reject) => {
|
|
247
|
+
let settled = false;
|
|
248
|
+
const worker = new Worker(WORKER_SCRIPT, { eval: true, workerData: input as any });
|
|
116
249
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
result
|
|
122
|
-
return result;
|
|
250
|
+
function finish(result: SubAgentResult) {
|
|
251
|
+
if (settled) return;
|
|
252
|
+
settled = true;
|
|
253
|
+
worker.terminate().catch(() => {});
|
|
254
|
+
resolve(result);
|
|
123
255
|
}
|
|
124
256
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
modelRegistry,
|
|
131
|
-
resourceLoader,
|
|
132
|
-
tools,
|
|
133
|
-
sessionManager: SessionManager.inMemory(cwd),
|
|
134
|
-
settingsManager,
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
let cleanupAbort: (() => void) | undefined;
|
|
138
|
-
let cleanupEventAbort: (() => void) | undefined;
|
|
139
|
-
try {
|
|
140
|
-
// Wire abort signal
|
|
141
|
-
if (signal) {
|
|
142
|
-
const onAbort = () => session.abort();
|
|
143
|
-
if (signal.aborted) {
|
|
144
|
-
// Already aborted — shortcut
|
|
145
|
-
result.exitCode = 1;
|
|
146
|
-
result.stopReason = "aborted";
|
|
147
|
-
result.errorMessage = "Sub-agent aborted before start";
|
|
148
|
-
onAbort();
|
|
149
|
-
return result;
|
|
150
|
-
}
|
|
151
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
152
|
-
cleanupAbort = () => signal.removeEventListener("abort", onAbort);
|
|
257
|
+
worker.on("message", (message: { type: "message" | "done"; payload: SubAgentResult }) => {
|
|
258
|
+
if (message.type === "message") {
|
|
259
|
+
onMessage?.(message.payload);
|
|
260
|
+
} else if (message.type === "done") {
|
|
261
|
+
finish(message.payload);
|
|
153
262
|
}
|
|
263
|
+
});
|
|
154
264
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
try {
|
|
166
|
-
switch (event.type) {
|
|
167
|
-
case "message_end": {
|
|
168
|
-
const msg = event.message as AgentMessage;
|
|
169
|
-
if (msg.role === "assistant") {
|
|
170
|
-
result.usage.turns++;
|
|
171
|
-
if (msg.usage) {
|
|
172
|
-
result.usage.input += msg.usage.input || 0;
|
|
173
|
-
result.usage.output += msg.usage.output || 0;
|
|
174
|
-
result.usage.cacheRead += msg.usage.cacheRead || 0;
|
|
175
|
-
result.usage.cacheWrite += msg.usage.cacheWrite || 0;
|
|
176
|
-
result.usage.cost += msg.usage.cost?.total || 0;
|
|
177
|
-
result.usage.contextTokens = msg.usage.totalTokens || 0;
|
|
178
|
-
}
|
|
179
|
-
if (!result.model && msg.model) {
|
|
180
|
-
result.model = `${msg.provider || "?"}/${msg.model}`;
|
|
181
|
-
}
|
|
182
|
-
if (msg.stopReason) result.stopReason = msg.stopReason;
|
|
183
|
-
if (msg.errorMessage) result.errorMessage = msg.errorMessage;
|
|
184
|
-
}
|
|
185
|
-
// Collect all messages for extraction
|
|
186
|
-
result.messages.push(msg as unknown as Message);
|
|
187
|
-
if (onMessage) onMessage({ ...result, messages: [...result.messages] });
|
|
188
|
-
break;
|
|
189
|
-
}
|
|
190
|
-
case "agent_end": {
|
|
191
|
-
// agent_end carries all messages; use them if we haven't collected
|
|
192
|
-
if (result.messages.length === 0 && event.messages) {
|
|
193
|
-
result.messages = event.messages as unknown as Message[];
|
|
194
|
-
}
|
|
195
|
-
finish(() => {
|
|
196
|
-
unsubscribe();
|
|
197
|
-
resolve();
|
|
198
|
-
});
|
|
199
|
-
break;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
} catch (err) {
|
|
203
|
-
finish(() => {
|
|
204
|
-
unsubscribe();
|
|
205
|
-
reject(err);
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
// Resolve on abort so the eventPromise doesn't hang
|
|
211
|
-
if (signal) {
|
|
212
|
-
const onAbortResolve = () => {
|
|
213
|
-
finish(() => {
|
|
214
|
-
result.exitCode = 1;
|
|
215
|
-
result.stopReason = "aborted";
|
|
216
|
-
if (!result.errorMessage) result.errorMessage = "Sub-agent aborted";
|
|
217
|
-
unsubscribe();
|
|
218
|
-
resolve();
|
|
219
|
-
});
|
|
220
|
-
};
|
|
221
|
-
signal.addEventListener("abort", onAbortResolve, { once: true });
|
|
222
|
-
cleanupEventAbort = () => signal.removeEventListener("abort", onAbortResolve);
|
|
223
|
-
}
|
|
265
|
+
worker.on("error", (err) => {
|
|
266
|
+
finish({
|
|
267
|
+
agent: agentName,
|
|
268
|
+
task,
|
|
269
|
+
exitCode: 1,
|
|
270
|
+
messages: [],
|
|
271
|
+
stderr: "",
|
|
272
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
273
|
+
stopReason: "error",
|
|
274
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
224
275
|
});
|
|
276
|
+
});
|
|
225
277
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
278
|
+
worker.on("exit", (code) => {
|
|
279
|
+
if (settled) return;
|
|
280
|
+
finish({
|
|
281
|
+
agent: agentName,
|
|
282
|
+
task,
|
|
283
|
+
exitCode: 1,
|
|
284
|
+
messages: [],
|
|
285
|
+
stderr: "",
|
|
286
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
287
|
+
stopReason: "error",
|
|
288
|
+
errorMessage: `Sub-agent worker exited unexpectedly (code ${code})`,
|
|
289
|
+
});
|
|
290
|
+
});
|
|
230
291
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
292
|
+
if (signal) {
|
|
293
|
+
const onAbort = () => {
|
|
294
|
+
if (settled) return;
|
|
295
|
+
worker.terminate().catch(() => {});
|
|
296
|
+
finish({
|
|
297
|
+
agent: agentName,
|
|
298
|
+
task,
|
|
299
|
+
exitCode: 1,
|
|
300
|
+
messages: [],
|
|
301
|
+
stderr: "",
|
|
302
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
303
|
+
stopReason: "aborted",
|
|
304
|
+
errorMessage: "Sub-agent aborted",
|
|
305
|
+
});
|
|
306
|
+
};
|
|
307
|
+
if (signal.aborted) {
|
|
308
|
+
onAbort();
|
|
309
|
+
return;
|
|
242
310
|
}
|
|
311
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
243
312
|
}
|
|
244
|
-
}
|
|
245
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
246
|
-
result.exitCode = 1;
|
|
247
|
-
result.errorMessage = message;
|
|
248
|
-
if (!result.stopReason) result.stopReason = "error";
|
|
249
|
-
return result;
|
|
250
|
-
}
|
|
313
|
+
});
|
|
251
314
|
}
|
|
252
315
|
|
|
253
316
|
// ---------------------------------------------------------------------------
|
|
254
317
|
// Helpers
|
|
255
318
|
// ---------------------------------------------------------------------------
|
|
256
319
|
|
|
320
|
+
function getAuthStoragePath(authStorage: AuthStorage): string {
|
|
321
|
+
try {
|
|
322
|
+
const storage = authStorage as unknown as { authPath?: string; path?: string };
|
|
323
|
+
if (storage.authPath) return storage.authPath;
|
|
324
|
+
if (storage.path) return storage.path;
|
|
325
|
+
} catch {}
|
|
326
|
+
return path.join(getAgentDir(), "auth.json");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function getModelsPath(modelRegistry: ModelRegistry): string | undefined {
|
|
330
|
+
try {
|
|
331
|
+
const registry = modelRegistry as unknown as { modelsPath?: string; path?: string };
|
|
332
|
+
if (registry.modelsPath) return registry.modelsPath;
|
|
333
|
+
if (registry.path) return registry.path;
|
|
334
|
+
} catch {}
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function loadProjectContextFilesCompat({ cwd, agentDir }: { cwd: string; agentDir: string }): { content: string }[] {
|
|
339
|
+
try {
|
|
340
|
+
const { loadProjectContextFiles } = require("@earendil-works/pi-coding-agent");
|
|
341
|
+
return loadProjectContextFiles({ cwd, agentDir });
|
|
342
|
+
} catch {
|
|
343
|
+
return [];
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
257
347
|
export function getFinalOutput(messages: Message[]): string {
|
|
258
348
|
// Prefer the last assistant message with non-empty text and NO tool calls (pure final answer).
|
|
259
349
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import { renderSubagentLayout, anySubagentRunning, SubagentCapLine } from "../render.ts";
|
|
3
|
+
|
|
4
|
+
function makeTheme() {
|
|
5
|
+
const fg = mock((tag: string, text: string) => `[${tag}:${text}]`);
|
|
6
|
+
return {
|
|
7
|
+
fg,
|
|
8
|
+
bold: mock((s: string) => `*${s}*`),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function stripAnsi(text: string): string {
|
|
13
|
+
return text.replace(/\u001b\[[0-9;]*m/g, "");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function makeResult(agent: string, exitCode: number, failed = false) {
|
|
17
|
+
return {
|
|
18
|
+
agent,
|
|
19
|
+
task: "test",
|
|
20
|
+
exitCode,
|
|
21
|
+
messages: [] as any[],
|
|
22
|
+
stderr: "",
|
|
23
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
24
|
+
...(failed ? { stopReason: "error", errorMessage: "fail" } : {}),
|
|
25
|
+
} as any;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("SubagentCapLine", () => {
|
|
29
|
+
test("renders exactly the current width and disappears when hidden", () => {
|
|
30
|
+
let visible = true;
|
|
31
|
+
const cap = new SubagentCapLine(() => visible, (_color, text) => text);
|
|
32
|
+
|
|
33
|
+
expect(cap.render(37)).toEqual(["\u2500".repeat(37)]);
|
|
34
|
+
expect(cap.render(241)).toEqual(["\u2500".repeat(241)]);
|
|
35
|
+
|
|
36
|
+
visible = false;
|
|
37
|
+
expect(cap.render(80)).toEqual([]);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("renderSubagentLayout", () => {
|
|
42
|
+
test("running single mode uses the Thinking gradient without a bullet", () => {
|
|
43
|
+
const theme = makeTheme() as any;
|
|
44
|
+
const out = renderSubagentLayout({ agent: "coder", task: "do stuff" }, [], theme);
|
|
45
|
+
expect(stripAnsi(out)).toContain("coder");
|
|
46
|
+
expect(out).toContain("\u001b[38;2;");
|
|
47
|
+
expect(stripAnsi(out)).not.toContain("\u2022");
|
|
48
|
+
expect(out).not.toContain("\u2713");
|
|
49
|
+
expect(out).not.toContain("\u2717");
|
|
50
|
+
expect(out).not.toContain("subagent");
|
|
51
|
+
expect(out).not.toContain("[user]");
|
|
52
|
+
expect(out).not.toContain("\u23f3");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("completed single mode shows checkmark", () => {
|
|
56
|
+
const theme = makeTheme() as any;
|
|
57
|
+
const out = renderSubagentLayout({ agent: "coder", task: "do stuff" }, [makeResult("coder", 0)], theme);
|
|
58
|
+
expect(stripAnsi(out)).toContain("coder");
|
|
59
|
+
expect(stripAnsi(out)).toContain("\u2713");
|
|
60
|
+
expect(stripAnsi(out)).not.toContain("\u2717");
|
|
61
|
+
expect(out).not.toContain("\u001b[38;2;");
|
|
62
|
+
expect(stripAnsi(out)).not.toContain("\u2022");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("failed single mode shows X mark", () => {
|
|
66
|
+
const theme = makeTheme() as any;
|
|
67
|
+
const out = renderSubagentLayout({ agent: "coder", task: "do stuff" }, [makeResult("coder", 1, true)], theme);
|
|
68
|
+
expect(stripAnsi(out)).toContain("coder");
|
|
69
|
+
expect(stripAnsi(out)).toContain("\u2717");
|
|
70
|
+
expect(stripAnsi(out)).not.toContain("\u2713");
|
|
71
|
+
expect(out).not.toContain("\u001b[38;2;");
|
|
72
|
+
expect(stripAnsi(out)).not.toContain("\u2022");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("failed single mode includes inline error text", () => {
|
|
76
|
+
const theme = makeTheme() as any;
|
|
77
|
+
const result = makeResult("coder", 1, true);
|
|
78
|
+
result.errorMessage = "timeout during read";
|
|
79
|
+
const out = renderSubagentLayout({ agent: "coder", task: "do stuff" }, [result], theme);
|
|
80
|
+
expect(stripAnsi(out)).toContain("timeout during read");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("completed single mode does not include error text", () => {
|
|
84
|
+
const theme = makeTheme() as any;
|
|
85
|
+
const out = renderSubagentLayout({ agent: "coder", task: "do stuff" }, [makeResult("coder", 0)], theme);
|
|
86
|
+
expect(stripAnsi(out)).not.toContain("error");
|
|
87
|
+
expect(stripAnsi(out)).not.toContain("Error");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("failed parallel mode shows inline error only on failed child", () => {
|
|
91
|
+
const theme = makeTheme() as any;
|
|
92
|
+
const args = { tasks: [{ agent: "coder", task: "a" }, { agent: "scout", task: "b" }] };
|
|
93
|
+
const failed = makeResult("coder", 1, true);
|
|
94
|
+
failed.errorMessage = "could not read file";
|
|
95
|
+
const out = renderSubagentLayout(args, [failed, makeResult("scout", 0)], theme);
|
|
96
|
+
const output = stripAnsi(out);
|
|
97
|
+
expect(output).toContain("coder");
|
|
98
|
+
expect(output).toContain("scout");
|
|
99
|
+
expect(output).toContain("could not read file");
|
|
100
|
+
const matches = output.split("could not read file").length - 1;
|
|
101
|
+
expect(matches).toBe(1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("parallel mode shows plain Subagents header with children", () => {
|
|
105
|
+
const theme = makeTheme() as any;
|
|
106
|
+
const args = { tasks: [{ agent: "coder", task: "a" }, { agent: "scout", task: "b" }] };
|
|
107
|
+
const out = renderSubagentLayout(args, [], theme);
|
|
108
|
+
const lines = out.split("\n");
|
|
109
|
+
const header = lines[0];
|
|
110
|
+
expect(stripAnsi(header)).toContain("Subagents");
|
|
111
|
+
expect(header).not.toContain("\u001b[38;2;");
|
|
112
|
+
expect(stripAnsi(out)).toContain("coder");
|
|
113
|
+
expect(stripAnsi(out)).toContain("scout");
|
|
114
|
+
expect(out).not.toContain("\u23f3");
|
|
115
|
+
expect(out).not.toContain("parallel");
|
|
116
|
+
expect(out).not.toContain("[user]");
|
|
117
|
+
expect(stripAnsi(out)).not.toContain("\u2022");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("chain mode only shows started steps", () => {
|
|
121
|
+
const theme = makeTheme() as any;
|
|
122
|
+
const args = { chain: [{ agent: "scout", task: "a" }, { agent: "coder", task: "b" }] };
|
|
123
|
+
const out = renderSubagentLayout(args, [makeResult("scout", 0)], theme);
|
|
124
|
+
expect(out).toContain("scout");
|
|
125
|
+
expect(out).not.toContain("coder");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("no hourglass glyphs anywhere", () => {
|
|
129
|
+
const theme = makeTheme() as any;
|
|
130
|
+
const args = { tasks: [{ agent: "coder", task: "a" }] };
|
|
131
|
+
const out = renderSubagentLayout(args, [makeResult("coder", -1)], theme);
|
|
132
|
+
expect(out).not.toContain("\u23f3");
|
|
133
|
+
expect(out).not.toContain("\u25d0");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("anySubagentRunning true when exitCode is -1", () => {
|
|
137
|
+
const args = { tasks: [{ agent: "coder", task: "a" }] };
|
|
138
|
+
expect(anySubagentRunning(args, [makeResult("coder", -1)])).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("anySubagentRunning false when all done", () => {
|
|
142
|
+
const args = { tasks: [{ agent: "coder", task: "a" }] };
|
|
143
|
+
expect(anySubagentRunning(args, [makeResult("coder", 0)])).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
});
|