@d3ara1n/pi-subagent 1.6.0 → 1.7.1
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/package.json +1 -1
- package/src/index.ts +4 -0
- package/src/spawn.ts +11 -7
- package/src/utils.test.ts +23 -0
- package/src/utils.ts +21 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@d3ara1n/pi-subagent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Role-based subagent orchestration for pi — delegates tasks to specialized pi child processes with configurable model roles",
|
|
6
6
|
"main": "src/index.ts",
|
package/src/index.ts
CHANGED
|
@@ -162,6 +162,10 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
162
162
|
"- Parallel: multiple subagent_delegate calls at the same time run concurrently — foreground and background alike, no special flag.",
|
|
163
163
|
"- Background (background: true): non-blocking — returns an id immediately so you can do your own work while the run executes.",
|
|
164
164
|
"",
|
|
165
|
+
"RUN HISTORY:",
|
|
166
|
+
"",
|
|
167
|
+
"- Every spawned run is audited to ~/.pi/subagent/history/{sessionId}/{toolCallId}.json (toolCallId = the delegate call's id). The file holds the FULL raw output even when you received a compressed/truncated version, plus the task, activity log, and usage.",
|
|
168
|
+
"",
|
|
165
169
|
"BACKGROUND DELEGATION:",
|
|
166
170
|
"",
|
|
167
171
|
"- Use it only when you have your own work this turn (including an ongoing discussion with the user) while the run executes; otherwise let the call block and return the result directly.",
|
package/src/spawn.ts
CHANGED
|
@@ -367,11 +367,12 @@ export async function spawnSubagent(
|
|
|
367
367
|
// RPC command acknowledgements ("response" lines) carry no agent state.
|
|
368
368
|
if (event.type === "response") return;
|
|
369
369
|
|
|
370
|
-
// RPC mode is a resident server
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
|
|
370
|
+
// RPC mode is a resident server. agent_end only marks one low-level run
|
|
371
|
+
// and may be followed by an automatic retry, compaction, or queued
|
|
372
|
+
// continuation. agent_settled is the authoritative terminal event. We run
|
|
373
|
+
// one prompt per child, so ending stdin there triggers graceful shutdown
|
|
374
|
+
// (onInputEnd → runtime dispose → exit).
|
|
375
|
+
if (event.type === "agent_settled") {
|
|
375
376
|
try {
|
|
376
377
|
proc?.stdin?.end();
|
|
377
378
|
} catch {
|
|
@@ -398,8 +399,11 @@ export async function spawnSubagent(
|
|
|
398
399
|
);
|
|
399
400
|
}
|
|
400
401
|
if (!result.model && msg.model) result.model = msg.model;
|
|
401
|
-
|
|
402
|
-
|
|
402
|
+
// message_end is authoritative for the latest assistant attempt. A
|
|
403
|
+
// successful native retry must clear the transient error left by the
|
|
404
|
+
// failed attempt instead of triggering a redundant whole-run fallback.
|
|
405
|
+
result.stopReason = msg.stopReason;
|
|
406
|
+
result.errorMessage = msg.errorMessage;
|
|
403
407
|
|
|
404
408
|
// Track last assistant text
|
|
405
409
|
for (const part of msg.content) {
|
package/src/utils.test.ts
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
createThrottler,
|
|
42
42
|
terminalResultLine,
|
|
43
43
|
buildDisplayItems,
|
|
44
|
+
formatToolCall,
|
|
44
45
|
} from "./utils.ts";
|
|
45
46
|
import type { ActivityEntry, SubagentResult, SubagentRole } from "./types.ts";
|
|
46
47
|
|
|
@@ -427,6 +428,28 @@ describe("terminalResultLine", () => {
|
|
|
427
428
|
test("finishedText replaces the success chain (wait's status-only line)", () => {
|
|
428
429
|
assert.equal(terminalResultLine(baseResult(), id, "finished"), "\u2713 finished");
|
|
429
430
|
});
|
|
431
|
+
test("error message newlines are flattened to one line", () => {
|
|
432
|
+
assert.equal(
|
|
433
|
+
terminalResultLine(baseResult({ exitCode: 1, errorMessage: "boom\n at frame 2\nat frame 3" }), id),
|
|
434
|
+
"\u2717 boom at frame 2 at frame 3",
|
|
435
|
+
);
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// ── formatToolCall: single-line guarantee for TUI rows ──
|
|
440
|
+
describe("formatToolCall newline sanitization", () => {
|
|
441
|
+
const id = (_color: string, text: string) => text;
|
|
442
|
+
|
|
443
|
+
test("multi-line bash command renders as one line", () => {
|
|
444
|
+
const out = formatToolCall("bash", { command: "echo a\necho b\n echo c" }, id);
|
|
445
|
+
assert.ok(!out.includes("\n"));
|
|
446
|
+
assert.equal(out, "$ echo a echo b echo c");
|
|
447
|
+
});
|
|
448
|
+
test("default branch preview flattens embedded newlines", () => {
|
|
449
|
+
const out = formatToolCall("fetch", { url: "https://x.test/a\n/b" }, id);
|
|
450
|
+
assert.ok(!out.includes("\n"));
|
|
451
|
+
assert.ok(out.includes("https://x.test/a /b"));
|
|
452
|
+
});
|
|
430
453
|
});
|
|
431
454
|
|
|
432
455
|
// ── createThrottler: burst coalescing for onUpdate ──
|
package/src/utils.ts
CHANGED
|
@@ -136,10 +136,30 @@ export function shortenPath(p: string): string {
|
|
|
136
136
|
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
/** Flatten embedded newlines (multi-line bash commands, patterns, error
|
|
140
|
+
* messages) into single spaces so a row never spans multiple terminal lines.
|
|
141
|
+
* Every caller renders the result as ONE TUI row — inline rows join it with
|
|
142
|
+
* "\n" separators and the :view overlay wraps each row in a border frame. */
|
|
143
|
+
function oneLine(s: string): string {
|
|
144
|
+
return s.replace(/\s*\r?\n\s*/g, " ");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* One-line tool-call row for TUI display. Newline sanitization happens here
|
|
149
|
+
* at the single choke point so every tool branch is covered.
|
|
150
|
+
*/
|
|
139
151
|
export function formatToolCall(
|
|
140
152
|
toolName: string,
|
|
141
153
|
args: Record<string, unknown>,
|
|
142
154
|
fg: (color: string, text: string) => string,
|
|
155
|
+
): string {
|
|
156
|
+
return oneLine(renderToolCall(toolName, args, fg));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function renderToolCall(
|
|
160
|
+
toolName: string,
|
|
161
|
+
args: Record<string, unknown>,
|
|
162
|
+
fg: (color: string, text: string) => string,
|
|
143
163
|
): string {
|
|
144
164
|
switch (toolName) {
|
|
145
165
|
case "subagent_delegate": {
|
|
@@ -306,7 +326,7 @@ function failureResultText(r: {
|
|
|
306
326
|
const isCancelled = r.stopReason === "cancelled";
|
|
307
327
|
return {
|
|
308
328
|
content:
|
|
309
|
-
r.errorMessage ||
|
|
329
|
+
oneLine(r.errorMessage || "") ||
|
|
310
330
|
(isTimeout ? "Timed out" : isBudget ? "Budget exceeded" : isCancelled ? "Cancelled" : "failed"),
|
|
311
331
|
// Timeout/budget/cancel are intentional stops with partial output —
|
|
312
332
|
// warning, not the error red reserved for real failures.
|