@maxiaochao/pi-toolkit 0.9.6 → 0.10.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/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/package.json +5 -5
- package/packages/herdr-subagents/CHANGELOG.md +41 -0
- package/packages/herdr-subagents/README.md +155 -0
- package/packages/herdr-subagents/TESTING.md +24 -0
- package/packages/herdr-subagents/agents/explorer.md +8 -0
- package/packages/herdr-subagents/agents/reviewer.md +8 -0
- package/packages/{tiny-subagent → herdr-subagents}/agents/worker.md +1 -0
- package/packages/herdr-subagents/docs/demo.gif +0 -0
- package/packages/herdr-subagents/docs/screenshot.png +0 -0
- package/packages/herdr-subagents/extensions/herdr-subagents.ts +258 -0
- package/packages/herdr-subagents/extensions/subagent-report.ts +65 -0
- package/packages/{tiny-subagent → herdr-subagents}/package.json +7 -6
- package/packages/herdr-subagents/src/batch-runner.ts +219 -0
- package/packages/herdr-subagents/src/blocking.ts +175 -0
- package/packages/herdr-subagents/src/config.ts +125 -0
- package/packages/herdr-subagents/src/continue-run.ts +87 -0
- package/packages/herdr-subagents/src/dispatcher.ts +73 -0
- package/packages/herdr-subagents/src/errors.ts +3 -0
- package/packages/herdr-subagents/src/fleet-controller.ts +82 -0
- package/packages/herdr-subagents/src/fleet.ts +56 -0
- package/packages/herdr-subagents/src/herdr.ts +224 -0
- package/packages/herdr-subagents/src/history.ts +210 -0
- package/packages/herdr-subagents/src/live-agent.ts +87 -0
- package/packages/herdr-subagents/src/monitor.ts +18 -0
- package/packages/herdr-subagents/src/ownership.ts +17 -0
- package/packages/herdr-subagents/src/parameters.ts +21 -0
- package/packages/{tiny-subagent → herdr-subagents}/src/personas.ts +13 -2
- package/packages/herdr-subagents/src/protocol.ts +23 -0
- package/packages/herdr-subagents/src/queued-runs.ts +70 -0
- package/packages/herdr-subagents/src/reconcile.ts +167 -0
- package/packages/herdr-subagents/src/records.ts +213 -0
- package/packages/herdr-subagents/src/result-path.ts +7 -0
- package/packages/herdr-subagents/src/run-locator.ts +19 -0
- package/packages/herdr-subagents/src/run-pane-allocator.ts +78 -0
- package/packages/herdr-subagents/src/run-status.ts +23 -0
- package/packages/herdr-subagents/src/runner.ts +250 -0
- package/packages/herdr-subagents/src/state.ts +11 -0
- package/packages/herdr-subagents/src/tab-cleanup.ts +68 -0
- package/packages/herdr-subagents/src/tool.ts +217 -0
- package/packages/herdr-subagents/src/validation.ts +38 -0
- package/packages/herdr-subagents/tests/extension-contract.ts +225 -0
- package/packages/herdr-subagents/tests/run-tests.mjs +889 -0
- package/packages/todo/extensions/todo.ts +1 -1
- package/packages/tiny-subagent/CHANGELOG.md +0 -12
- package/packages/tiny-subagent/README.md +0 -128
- package/packages/tiny-subagent/TESTING.md +0 -20
- package/packages/tiny-subagent/docs/demo.gif +0 -0
- package/packages/tiny-subagent/docs/screenshot.png +0 -0
- package/packages/tiny-subagent/extensions/tiny-subagent.ts +0 -228
- package/packages/tiny-subagent/src/runner.ts +0 -319
- package/packages/tiny-subagent/tests/run-tests.mjs +0 -111
|
@@ -1,319 +0,0 @@
|
|
|
1
|
-
import { createWriteStream, existsSync, realpathSync } from "node:fs";
|
|
2
|
-
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { basename, join } from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
6
|
-
|
|
7
|
-
export const MAX_INLINE_BYTES = 16 * 1024;
|
|
8
|
-
const CHILD_ENV = "PI_SUBAGENT_CHILD";
|
|
9
|
-
const BINARY_ENV = "PI_TINY_SUBAGENT_PI_BINARY";
|
|
10
|
-
|
|
11
|
-
export interface TinySubagentOptions {
|
|
12
|
-
model?: string;
|
|
13
|
-
thinking?: string;
|
|
14
|
-
tools?: string[];
|
|
15
|
-
systemPrompt?: string;
|
|
16
|
-
cwd?: string;
|
|
17
|
-
signal?: AbortSignal;
|
|
18
|
-
onUpdate?: (result: TinySubagentResult) => void;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface TinySubagentArtifacts {
|
|
22
|
-
directory: string;
|
|
23
|
-
stdout: string;
|
|
24
|
-
stderr: string;
|
|
25
|
-
finalOutput?: string;
|
|
26
|
-
cleanup: "remove directory when no longer needed";
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface TinySubagentContent {
|
|
30
|
-
type?: string;
|
|
31
|
-
text?: string;
|
|
32
|
-
name?: string;
|
|
33
|
-
arguments?: Record<string, unknown>;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface TinySubagentMessage {
|
|
37
|
-
role?: string;
|
|
38
|
-
content?: string | TinySubagentContent[];
|
|
39
|
-
toolCallId?: string;
|
|
40
|
-
toolName?: string;
|
|
41
|
-
isError?: boolean;
|
|
42
|
-
stopReason?: string;
|
|
43
|
-
errorMessage?: string;
|
|
44
|
-
model?: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface TinySubagentResult {
|
|
48
|
-
ok: boolean;
|
|
49
|
-
output: string;
|
|
50
|
-
outputBytes: number;
|
|
51
|
-
stderr: string;
|
|
52
|
-
stderrBytes: number;
|
|
53
|
-
stdout: string;
|
|
54
|
-
stdoutBytes: number;
|
|
55
|
-
protocolErrors: string[];
|
|
56
|
-
errorMessage?: string;
|
|
57
|
-
stopReason?: string;
|
|
58
|
-
exitCode: number | null;
|
|
59
|
-
signal: NodeJS.Signals | null;
|
|
60
|
-
model?: string;
|
|
61
|
-
cwd: string;
|
|
62
|
-
durationMs: number;
|
|
63
|
-
messages: TinySubagentMessage[];
|
|
64
|
-
artifacts?: TinySubagentArtifacts;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
interface JsonEvent {
|
|
68
|
-
type?: string;
|
|
69
|
-
message?: TinySubagentMessage;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
class Capture {
|
|
73
|
-
private readonly stream;
|
|
74
|
-
private inline = "";
|
|
75
|
-
private truncated = false;
|
|
76
|
-
bytes = 0;
|
|
77
|
-
|
|
78
|
-
readonly path: string;
|
|
79
|
-
constructor(path: string) {
|
|
80
|
-
this.path = path;
|
|
81
|
-
this.stream = createWriteStream(path, { encoding: "utf8", mode: 0o600 });
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
write(text: string): void {
|
|
86
|
-
this.bytes += Buffer.byteLength(text, "utf8");
|
|
87
|
-
this.stream.write(text);
|
|
88
|
-
if (this.truncated) return;
|
|
89
|
-
const next = this.inline + text;
|
|
90
|
-
if (Buffer.byteLength(next, "utf8") <= MAX_INLINE_BYTES) {
|
|
91
|
-
this.inline = next;
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
this.inline = truncateUtf8(next, MAX_INLINE_BYTES);
|
|
95
|
-
this.truncated = true;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async close(): Promise<void> {
|
|
99
|
-
await new Promise<void>((resolve, reject) => {
|
|
100
|
-
this.stream.once("error", reject);
|
|
101
|
-
this.stream.once("finish", resolve);
|
|
102
|
-
this.stream.end();
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
preview(): string {
|
|
107
|
-
return this.truncated ? `${this.inline}\n[truncated after ${MAX_INLINE_BYTES} bytes; see artifact file]` : this.inline;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function truncateUtf8(text: string, maxBytes: number): string {
|
|
112
|
-
let result = text.slice(0, maxBytes);
|
|
113
|
-
while (Buffer.byteLength(result, "utf8") > maxBytes) result = result.slice(0, -1);
|
|
114
|
-
return result;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function getFinalText(messages: TinySubagentMessage[]): string {
|
|
118
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
119
|
-
const message = messages[i];
|
|
120
|
-
if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
121
|
-
return message.content
|
|
122
|
-
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
123
|
-
.map((part) => part.text as string)
|
|
124
|
-
.join("");
|
|
125
|
-
}
|
|
126
|
-
return "";
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function parseLine(line: string, messages: TinySubagentMessage[], protocolErrors: string[]): boolean {
|
|
130
|
-
if (!line.trim()) return false;
|
|
131
|
-
try {
|
|
132
|
-
const event = JSON.parse(line) as JsonEvent;
|
|
133
|
-
if ((event.type === "message_end" || event.type === "tool_result_end") && event.message) {
|
|
134
|
-
messages.push(event.message);
|
|
135
|
-
return true;
|
|
136
|
-
}
|
|
137
|
-
} catch {
|
|
138
|
-
if (protocolErrors.length < 20) protocolErrors.push(truncateUtf8(line, 4096));
|
|
139
|
-
}
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function getPiInvocation(args: string[]): { command: string; args: string[] } {
|
|
144
|
-
const configuredBinary = process.env[BINARY_ENV]?.trim();
|
|
145
|
-
if (configuredBinary) return { command: configuredBinary, args };
|
|
146
|
-
|
|
147
|
-
const entry = process.argv[1];
|
|
148
|
-
if (entry && existsSync(entry) && /\.(?:mjs|cjs|js)$/i.test(entry)) {
|
|
149
|
-
try {
|
|
150
|
-
const resolved = realpathSync(entry);
|
|
151
|
-
if (resolved === entry || existsSync(resolved)) return { command: process.execPath, args: [entry, ...args] };
|
|
152
|
-
} catch {
|
|
153
|
-
// Fall through to the Pi binary on PATH.
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (/^pi(?:\.exe)?$/i.test(basename(process.execPath))) return { command: process.execPath, args };
|
|
158
|
-
return { command: "pi", args };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function stopReasonIsFailure(stopReason: string | undefined): boolean {
|
|
162
|
-
return stopReason === "error" || stopReason === "aborted";
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export async function runTinySubagent(prompt: string, options: TinySubagentOptions = {}): Promise<TinySubagentResult> {
|
|
166
|
-
const cwd = options.cwd ?? process.cwd();
|
|
167
|
-
const artifactDirectory = await mkdtemp(join(tmpdir(), "pi-tiny-subagent-"));
|
|
168
|
-
const stdoutCapture = new Capture(join(artifactDirectory, "stdout.jsonl"));
|
|
169
|
-
const stderrCapture = new Capture(join(artifactDirectory, "stderr.log"));
|
|
170
|
-
const args = ["--mode", "json", "--print", "--no-session", "--no-extensions", "--no-skills"];
|
|
171
|
-
if (options.model?.trim()) args.push("--model", options.model.trim());
|
|
172
|
-
if (options.thinking?.trim()) args.push("--thinking", options.thinking.trim());
|
|
173
|
-
if (options.tools?.length) args.push("--tools", options.tools.join(","));
|
|
174
|
-
if (options.systemPrompt?.trim()) {
|
|
175
|
-
const systemPromptPath = join(artifactDirectory, "system-prompt.md");
|
|
176
|
-
await writeFile(systemPromptPath, options.systemPrompt, { encoding: "utf8", mode: 0o600 });
|
|
177
|
-
args.push("--append-system-prompt", systemPromptPath);
|
|
178
|
-
}
|
|
179
|
-
args.push("--", prompt);
|
|
180
|
-
|
|
181
|
-
const startedAt = Date.now();
|
|
182
|
-
const messages: TinySubagentMessage[] = [];
|
|
183
|
-
const protocolErrors: string[] = [];
|
|
184
|
-
let stdoutBuffer = "";
|
|
185
|
-
let errorMessage: string | undefined;
|
|
186
|
-
let aborted = false;
|
|
187
|
-
let exitCode: number | null = null;
|
|
188
|
-
let childSignal: NodeJS.Signals | null = null;
|
|
189
|
-
let spawnError: Error | undefined;
|
|
190
|
-
let child: ReturnType<typeof spawn> | undefined;
|
|
191
|
-
const invocation = getPiInvocation(args);
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const emitUpdate = () => {
|
|
195
|
-
if (!options.onUpdate) return;
|
|
196
|
-
const output = getFinalText(messages);
|
|
197
|
-
const lastAssistant = [...messages].reverse().find((message) => message.role === "assistant");
|
|
198
|
-
options.onUpdate({
|
|
199
|
-
ok: false,
|
|
200
|
-
output,
|
|
201
|
-
outputBytes: Buffer.byteLength(output, "utf8"),
|
|
202
|
-
stderr: stderrCapture.preview(),
|
|
203
|
-
stderrBytes: stderrCapture.bytes,
|
|
204
|
-
stdout: stdoutCapture.preview(),
|
|
205
|
-
stdoutBytes: stdoutCapture.bytes,
|
|
206
|
-
protocolErrors: [...protocolErrors],
|
|
207
|
-
...(lastAssistant?.stopReason ? { stopReason: lastAssistant.stopReason } : {}),
|
|
208
|
-
exitCode,
|
|
209
|
-
signal: childSignal,
|
|
210
|
-
...(options.model ?? lastAssistant?.model ? { model: options.model ?? lastAssistant?.model } : {}),
|
|
211
|
-
cwd,
|
|
212
|
-
durationMs: Date.now() - startedAt,
|
|
213
|
-
messages: messages.slice(),
|
|
214
|
-
});
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
emitUpdate();
|
|
218
|
-
await new Promise<void>((resolve) => {
|
|
219
|
-
let settled = false;
|
|
220
|
-
const finish = () => {
|
|
221
|
-
if (settled) return;
|
|
222
|
-
settled = true;
|
|
223
|
-
resolve();
|
|
224
|
-
};
|
|
225
|
-
try {
|
|
226
|
-
child = spawn(invocation.command, invocation.args, {
|
|
227
|
-
cwd,
|
|
228
|
-
shell: false,
|
|
229
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
230
|
-
env: { ...process.env, [CHILD_ENV]: "1" },
|
|
231
|
-
});
|
|
232
|
-
} catch (error) {
|
|
233
|
-
spawnError = error instanceof Error ? error : new Error(String(error));
|
|
234
|
-
finish();
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
child.stdout?.on("data", (chunk: Buffer | string) => {
|
|
238
|
-
const text = chunk.toString();
|
|
239
|
-
stdoutCapture.write(text);
|
|
240
|
-
stdoutBuffer += text;
|
|
241
|
-
const lines = stdoutBuffer.split("\n");
|
|
242
|
-
stdoutBuffer = lines.pop() ?? "";
|
|
243
|
-
for (const line of lines) {
|
|
244
|
-
if (parseLine(line, messages, protocolErrors)) emitUpdate();
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
child.stderr?.on("data", (chunk: Buffer | string) => stderrCapture.write(chunk.toString()));
|
|
248
|
-
child.once("error", (error) => {
|
|
249
|
-
spawnError = error;
|
|
250
|
-
});
|
|
251
|
-
child.once("close", (code, signal) => {
|
|
252
|
-
if (stdoutBuffer && parseLine(stdoutBuffer, messages, protocolErrors)) emitUpdate();
|
|
253
|
-
// Node may report a negative pseudo-exit code for ENOENT; expose a stable process failure code and keep the real error in errorMessage.
|
|
254
|
-
exitCode = spawnError ? 1 : code;
|
|
255
|
-
childSignal = signal;
|
|
256
|
-
finish();
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
const terminate = () => {
|
|
260
|
-
if (aborted) return;
|
|
261
|
-
aborted = true;
|
|
262
|
-
child?.kill("SIGTERM");
|
|
263
|
-
setTimeout(() => {
|
|
264
|
-
if (child && !child.killed) child.kill("SIGKILL");
|
|
265
|
-
}, 5000).unref();
|
|
266
|
-
};
|
|
267
|
-
if (options.signal) {
|
|
268
|
-
if (options.signal.aborted) terminate();
|
|
269
|
-
else options.signal.addEventListener("abort", terminate, { once: true });
|
|
270
|
-
}
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
await Promise.all([stdoutCapture.close(), stderrCapture.close()]);
|
|
274
|
-
const finalOutput = getFinalText(messages);
|
|
275
|
-
const lastAssistant = [...messages].reverse().find((message) => message.role === "assistant");
|
|
276
|
-
const stopReason = lastAssistant?.stopReason;
|
|
277
|
-
errorMessage = spawnError?.message ?? lastAssistant?.errorMessage;
|
|
278
|
-
if (aborted) errorMessage ||= "Tiny subagent was aborted";
|
|
279
|
-
if (!finalOutput && exitCode === 0 && !stopReason) errorMessage ||= "No assistant output received";
|
|
280
|
-
if (protocolErrors.length > 0 && !finalOutput && !errorMessage) errorMessage = "Pi emitted invalid JSON output";
|
|
281
|
-
if (stopReasonIsFailure(stopReason)) errorMessage ||= `Subagent stopped with reason: ${stopReason}`;
|
|
282
|
-
|
|
283
|
-
const outputBytes = Buffer.byteLength(finalOutput, "utf8");
|
|
284
|
-
const keepArtifacts = stdoutCapture.bytes > MAX_INLINE_BYTES || stderrCapture.bytes > MAX_INLINE_BYTES || outputBytes > MAX_INLINE_BYTES;
|
|
285
|
-
let artifacts: TinySubagentArtifacts | undefined;
|
|
286
|
-
if (keepArtifacts) {
|
|
287
|
-
const finalOutputPath = join(artifactDirectory, "final-output.txt");
|
|
288
|
-
if (outputBytes > 0) await writeFile(finalOutputPath, finalOutput, { encoding: "utf8", mode: 0o600 });
|
|
289
|
-
artifacts = {
|
|
290
|
-
directory: artifactDirectory,
|
|
291
|
-
stdout: stdoutCapture.path,
|
|
292
|
-
stderr: stderrCapture.path,
|
|
293
|
-
cleanup: "remove directory when no longer needed",
|
|
294
|
-
...(outputBytes > MAX_INLINE_BYTES ? { finalOutput: finalOutputPath } : {}),
|
|
295
|
-
};
|
|
296
|
-
} else {
|
|
297
|
-
await rm(artifactDirectory, { recursive: true, force: true });
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
return {
|
|
301
|
-
ok: !errorMessage && exitCode === 0 && childSignal === null && !stopReasonIsFailure(stopReason),
|
|
302
|
-
output: outputBytes > MAX_INLINE_BYTES ? `${truncateUtf8(finalOutput, MAX_INLINE_BYTES)}\n[truncated after ${MAX_INLINE_BYTES} bytes; see artifact file]` : finalOutput,
|
|
303
|
-
outputBytes,
|
|
304
|
-
stderr: stderrCapture.preview(),
|
|
305
|
-
stderrBytes: stderrCapture.bytes,
|
|
306
|
-
stdout: stdoutCapture.preview(),
|
|
307
|
-
stdoutBytes: stdoutCapture.bytes,
|
|
308
|
-
protocolErrors,
|
|
309
|
-
...(errorMessage ? { errorMessage } : {}),
|
|
310
|
-
...(stopReason ? { stopReason } : {}),
|
|
311
|
-
exitCode,
|
|
312
|
-
signal: childSignal,
|
|
313
|
-
...(options.model ?? lastAssistant?.model ? { model: options.model ?? lastAssistant?.model } : {}),
|
|
314
|
-
cwd,
|
|
315
|
-
durationMs: Date.now() - startedAt,
|
|
316
|
-
messages: messages.slice(),
|
|
317
|
-
...(artifacts ? { artifacts } : {}),
|
|
318
|
-
};
|
|
319
|
-
}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { discoverPackageAgents, parseAgentMarkdown } from "../src/personas.ts";
|
|
5
|
-
import { MAX_INLINE_BYTES, runTinySubagent } from "../src/runner.ts";
|
|
6
|
-
|
|
7
|
-
let passed = 0;
|
|
8
|
-
let failed = 0;
|
|
9
|
-
function check(name, condition, detail = "") {
|
|
10
|
-
if (condition) {
|
|
11
|
-
passed += 1;
|
|
12
|
-
console.log(`PASS ${name}`);
|
|
13
|
-
} else {
|
|
14
|
-
failed += 1;
|
|
15
|
-
console.log(`FAIL ${name} ${detail}`);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const root = mkdtempSync(join(tmpdir(), "pi-tiny-subagent-test-"));
|
|
20
|
-
const fakePi = join(root, "fake-pi.mjs");
|
|
21
|
-
writeFileSync(fakePi, `#!/usr/bin/env node
|
|
22
|
-
import { appendFileSync, readFileSync } from "node:fs";
|
|
23
|
-
const args = process.argv.slice(2);
|
|
24
|
-
const capture = process.env.TINY_CAPTURE;
|
|
25
|
-
if (capture) appendFileSync(capture, JSON.stringify({ args, cwd: process.cwd(), child: process.env.PI_SUBAGENT_CHILD, promptFile: args[args.indexOf("--append-system-prompt") + 1] ? readFileSync(args[args.indexOf("--append-system-prompt") + 1], "utf8") : null }) + "\\n");
|
|
26
|
-
const mode = process.env.TINY_MODE || "success";
|
|
27
|
-
if (mode === "abort") { setTimeout(() => {}, 30000); }
|
|
28
|
-
if (mode === "success") {
|
|
29
|
-
process.stdout.write(JSON.stringify({ type: "message_end", message: { role: "assistant", model: "fake/model", stopReason: "stop", content: [{ type: "text", text: "worker result" }] } }) + "\\n");
|
|
30
|
-
} else if (mode === "events") {
|
|
31
|
-
process.stdout.write(JSON.stringify({ type: "message_end", message: { role: "assistant", content: [{ type: "toolCall", name: "bash", arguments: { command: "printf live" } }] } }) + "\\n");
|
|
32
|
-
process.stdout.write(JSON.stringify({ type: "tool_result_end", message: { role: "toolResult", toolName: "bash", toolCallId: "call-1", isError: false, content: [{ type: "text", text: "live result" }] } }) + "\\n");
|
|
33
|
-
process.stdout.write(JSON.stringify({ type: "message_end", message: { role: "assistant", model: "fake/model", stopReason: "stop", content: [{ type: "text", text: "event result" }] } }) + "\\n");
|
|
34
|
-
} else if (mode === "error") {
|
|
35
|
-
process.stderr.write("provider failed\\n");
|
|
36
|
-
process.stdout.write(JSON.stringify({ type: "message_end", message: { role: "assistant", stopReason: "error", errorMessage: "rate limited", content: [] } }) + "\\n");
|
|
37
|
-
process.exitCode = 7;
|
|
38
|
-
} else if (mode === "protocol") {
|
|
39
|
-
process.stdout.write("not-json\\n");
|
|
40
|
-
process.exitCode = 2;
|
|
41
|
-
} else if (mode === "large") {
|
|
42
|
-
const text = "x".repeat(${MAX_INLINE_BYTES + 1000});
|
|
43
|
-
process.stderr.write("e".repeat(${MAX_INLINE_BYTES + 1000}));
|
|
44
|
-
process.stdout.write(JSON.stringify({ type: "message_end", message: { role: "assistant", stopReason: "stop", content: [{ type: "text", text }] } }) + "\\n");
|
|
45
|
-
}
|
|
46
|
-
`, { encoding: "utf8", mode: 0o700 });
|
|
47
|
-
chmodSync(fakePi, 0o700);
|
|
48
|
-
|
|
49
|
-
const capture = join(root, "capture.jsonl");
|
|
50
|
-
const oldEnv = { ...process.env };
|
|
51
|
-
function setEnv(values) {
|
|
52
|
-
for (const key of Object.keys(process.env)) delete process.env[key];
|
|
53
|
-
Object.assign(process.env, oldEnv, values);
|
|
54
|
-
}
|
|
55
|
-
function restoreEnv() {
|
|
56
|
-
for (const key of Object.keys(process.env)) delete process.env[key];
|
|
57
|
-
Object.assign(process.env, oldEnv);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
setEnv({ PI_TINY_SUBAGENT_PI_BINARY: fakePi, TINY_CAPTURE: capture, TINY_MODE: "success" });
|
|
62
|
-
const success = await runTinySubagent("hello", { cwd: root, model: "fake/model", tools: ["read"], systemPrompt: "You are a worker." });
|
|
63
|
-
const launch = JSON.parse(readFileSync(capture, "utf8").trim());
|
|
64
|
-
check("success output", success.ok && success.output === "worker result");
|
|
65
|
-
check("model, tools, and fresh-process flags", launch.args.includes("--model") && launch.args.includes("fake/model") && launch.args.includes("--tools") && launch.args.includes("read") && launch.args.includes("--no-session") && launch.args.includes("--no-extensions") && launch.args.includes("--no-skills"));
|
|
66
|
-
check("cwd and child marker", launch.cwd === root && launch.child === "1");
|
|
67
|
-
check("persona system prompt file", launch.promptFile === "You are a worker.");
|
|
68
|
-
|
|
69
|
-
setEnv({ PI_TINY_SUBAGENT_PI_BINARY: fakePi, TINY_MODE: "events" });
|
|
70
|
-
const updates = [];
|
|
71
|
-
const events = await runTinySubagent("events", { cwd: root, onUpdate: (partial) => updates.push(partial) });
|
|
72
|
-
check("event transcript and live updates", events.ok && events.output === "event result" && events.messages.length === 3 && events.messages[0].content?.[0]?.type === "toolCall" && events.messages[1].role === "toolResult" && updates.length >= 4 && updates.at(-1).messages.length === 3);
|
|
73
|
-
|
|
74
|
-
setEnv({ PI_TINY_SUBAGENT_PI_BINARY: fakePi, TINY_MODE: "error" });
|
|
75
|
-
const error = await runTinySubagent("fail", { cwd: root });
|
|
76
|
-
check("Pi error details", !error.ok && error.exitCode === 7 && error.stopReason === "error" && error.errorMessage === "rate limited" && error.stderr.includes("provider failed"));
|
|
77
|
-
setEnv({ PI_TINY_SUBAGENT_PI_BINARY: fakePi, TINY_MODE: "protocol" });
|
|
78
|
-
const protocol = await runTinySubagent("bad", { cwd: root });
|
|
79
|
-
check("protocol error details", !protocol.ok && protocol.exitCode === 2 && protocol.protocolErrors.includes("not-json"));
|
|
80
|
-
|
|
81
|
-
setEnv({ PI_TINY_SUBAGENT_PI_BINARY: "/definitely/missing/pi", TINY_MODE: "success" });
|
|
82
|
-
const missing = await runTinySubagent("missing", { cwd: root });
|
|
83
|
-
check("spawn error details", !missing.ok && missing.exitCode === 1 && missing.errorMessage?.includes("ENOENT"));
|
|
84
|
-
|
|
85
|
-
setEnv({ PI_TINY_SUBAGENT_PI_BINARY: fakePi, TINY_MODE: "large" });
|
|
86
|
-
const large = await runTinySubagent("large", { cwd: root });
|
|
87
|
-
check("large output spills to artifacts", large.ok && large.artifacts && large.artifacts.cleanup.includes("remove directory") && large.output.includes("see artifact file") && large.outputBytes > MAX_INLINE_BYTES && large.artifacts.finalOutput && readFileSync(large.artifacts.finalOutput, "utf8").length > MAX_INLINE_BYTES);
|
|
88
|
-
if (large.artifacts) rmSync(large.artifacts.directory, { recursive: true, force: true });
|
|
89
|
-
|
|
90
|
-
setEnv({ PI_TINY_SUBAGENT_PI_BINARY: fakePi, TINY_MODE: "abort" });
|
|
91
|
-
const controller = new AbortController();
|
|
92
|
-
const pending = runTinySubagent("abort", { cwd: root, signal: controller.signal });
|
|
93
|
-
setTimeout(() => controller.abort(), 50);
|
|
94
|
-
const aborted = await pending;
|
|
95
|
-
check("abort details", !aborted.ok && aborted.errorMessage === "Tiny subagent was aborted");
|
|
96
|
-
|
|
97
|
-
const definition = parseAgentMarkdown(`---\nname: reviewer\ndescription: Review code\nmodel: fake/model\ntools: read, grep\n---\nReview carefully.`, "reviewer.md");
|
|
98
|
-
check("persona frontmatter parsing", definition.name === "reviewer" && definition.model === "fake/model" && definition.tools?.join(",") === "read,grep" && definition.systemPrompt === "Review carefully.");
|
|
99
|
-
const agentsDir = join(root, "agents");
|
|
100
|
-
await import("node:fs/promises").then(({ mkdir }) => mkdir(agentsDir));
|
|
101
|
-
writeFileSync(join(agentsDir, "worker.md"), `---\nname: worker\ndescription: Worker\n---\nDo work.`);
|
|
102
|
-
writeFileSync(join(agentsDir, "broken.md"), "not frontmatter");
|
|
103
|
-
const discovery = discoverPackageAgents(agentsDir);
|
|
104
|
-
check("persona discovery and diagnostics", discovery.agents.length === 1 && discovery.agents[0].name === "worker" && discovery.diagnostics.length === 1);
|
|
105
|
-
} finally {
|
|
106
|
-
restoreEnv();
|
|
107
|
-
rmSync(root, { recursive: true, force: true });
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
console.log(`${passed} passed, ${failed} failed`);
|
|
111
|
-
if (failed > 0) process.exitCode = 1;
|