@fitchmultz/pi-subagents 0.46.0 → 0.46.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/CHANGELOG.md +22 -0
- package/dist/agents/agent-management.js +8 -2
- package/dist/agents/agent-selection.js +5 -17
- package/dist/agents/agents.js +10 -10
- package/dist/extension/build-info.js +1 -1
- package/dist/extension/index.js +11 -5
- package/dist/extension/result-snapshot.js +0 -18
- package/dist/history/ingest.js +40 -31
- package/dist/history/prune.js +1 -1
- package/dist/history/run-projection.js +1 -2
- package/dist/history/search-queries.js +8 -2
- package/dist/history/store.js +20 -4
- package/dist/history/text-chunks.js +1 -2
- package/dist/history/transcript-queries.js +12 -6
- package/dist/intercom/result-intercom.js +0 -74
- package/dist/pi-intercom/broker/broker.js +33 -16
- package/dist/pi-intercom/broker/framing.js +67 -17
- package/dist/pi-intercom/broker/snapshot-reader.js +29 -8
- package/dist/pi-intercom/broker/snapshots.js +144 -41
- package/dist/pi-intercom/session-targets.js +0 -1
- package/dist/runs/background/async-run-discovery.js +4 -6
- package/dist/runs/background/completion-dedupe.js +4 -55
- package/dist/runs/background/notify.js +1 -9
- package/dist/runs/background/result-watcher.js +1 -1
- package/dist/runs/background/runner-child-observer.js +12 -7
- package/dist/runs/background/runner-monitor.js +2 -0
- package/dist/runs/background/runner-parallel.js +2 -3
- package/dist/runs/background/runner-step-output.js +1 -3
- package/dist/runs/shared/acceptance-finalization.js +2 -2
- package/dist/runs/shared/acceptance.js +1 -1
- package/dist/runs/shared/child-observation-spool.js +8 -3
- package/dist/runs/shared/model-fallback.js +3 -0
- package/dist/runs/shared/parallel-utils.js +1 -21
- package/dist/runs/shared/run-restoration.js +4 -5
- package/dist/runs/shared/single-output.js +1 -4
- package/dist/runs/shared/subagent-prompt-runtime.js +2 -2
- package/dist/runs/shared/supervisor-questions.js +7 -4
- package/dist/runs/shared/workflow-policy.js +2 -1
- package/dist/shared/journal-frames.js +1 -1
- package/dist/shared/settings.js +1 -4
- package/dist/slash/command-discovery.js +13 -12
- package/dist/tui/agent-history.js +0 -14
- package/dist/tui/agent-view.js +0 -1
- package/dist/tui/conversation-history.js +10 -1
- package/dist/tui/history-cards.js +8 -22
- package/docs/intercom.md +5 -1
- package/docs/performance.md +72 -3
- package/docs/reference.md +1 -1
- package/package.json +1 -1
- package/src/agents/agent-management.ts +8 -4
- package/src/agents/agent-selection.ts +8 -19
- package/src/agents/agents.ts +19 -14
- package/src/extension/index.ts +12 -5
- package/src/extension/result-snapshot.ts +1 -27
- package/src/history/ingest.ts +57 -49
- package/src/history/prune.ts +1 -1
- package/src/history/run-projection.ts +2 -4
- package/src/history/search-queries.ts +9 -2
- package/src/history/store.ts +21 -5
- package/src/history/text-chunks.ts +7 -2
- package/src/history/transcript-queries.ts +16 -10
- package/src/intercom/result-intercom.ts +0 -103
- package/src/pi-intercom/broker/broker.ts +34 -20
- package/src/pi-intercom/broker/framing.ts +71 -20
- package/src/pi-intercom/broker/snapshot-reader.ts +47 -9
- package/src/pi-intercom/broker/snapshots.ts +184 -51
- package/src/pi-intercom/session-targets.ts +0 -2
- package/src/runs/background/async-run-discovery.ts +4 -10
- package/src/runs/background/completion-dedupe.ts +6 -69
- package/src/runs/background/notify.ts +2 -12
- package/src/runs/background/result-watcher.ts +1 -1
- package/src/runs/background/runner-child-observer.ts +19 -11
- package/src/runs/background/runner-monitor.ts +2 -0
- package/src/runs/background/runner-parallel.ts +2 -3
- package/src/runs/background/runner-step-output.ts +1 -3
- package/src/runs/shared/acceptance-finalization.ts +2 -2
- package/src/runs/shared/acceptance.ts +0 -2
- package/src/runs/shared/child-observation-spool.ts +8 -3
- package/src/runs/shared/model-fallback.ts +3 -0
- package/src/runs/shared/parallel-utils.ts +1 -25
- package/src/runs/shared/run-restoration.ts +4 -4
- package/src/runs/shared/single-output.ts +0 -4
- package/src/runs/shared/subagent-prompt-runtime.ts +2 -2
- package/src/runs/shared/supervisor-questions.ts +10 -10
- package/src/runs/shared/workflow-policy.ts +1 -1
- package/src/shared/journal-frames.ts +1 -1
- package/src/shared/settings.ts +1 -4
- package/src/slash/command-discovery.ts +13 -12
- package/src/tui/agent-history.ts +1 -18
- package/src/tui/agent-view.ts +0 -1
- package/src/tui/conversation-history.ts +15 -1
- package/src/tui/history-cards.ts +8 -26
|
@@ -2,29 +2,8 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { isRecord, hasText } from "./async-value.ts";
|
|
3
3
|
|
|
4
4
|
interface CompletionDataLike {
|
|
5
|
-
completionId?:
|
|
6
|
-
id
|
|
7
|
-
agent?: unknown;
|
|
8
|
-
timestamp?: unknown;
|
|
9
|
-
sessionId?: unknown;
|
|
10
|
-
taskIndex?: unknown;
|
|
11
|
-
totalTasks?: unknown;
|
|
12
|
-
success?: unknown;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function asNonEmptyString(value: unknown): string | undefined {
|
|
16
|
-
if (typeof value !== "string") {
|
|
17
|
-
return undefined;
|
|
18
|
-
}
|
|
19
|
-
const trimmed = value.trim();
|
|
20
|
-
return trimmed.length > 0 ? trimmed : undefined;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function asFiniteNumber(value: unknown): number | undefined {
|
|
24
|
-
if (typeof value !== "number") {
|
|
25
|
-
return undefined;
|
|
26
|
-
}
|
|
27
|
-
return Number.isFinite(value) ? value : undefined;
|
|
5
|
+
completionId?: string;
|
|
6
|
+
id: string;
|
|
28
7
|
}
|
|
29
8
|
|
|
30
9
|
function stableStringify(value: unknown): string {
|
|
@@ -48,32 +27,11 @@ function payloadDigest(value: unknown): string {
|
|
|
48
27
|
return createHash("sha256").update(stableStringify(value)).digest("hex").slice(0, 16);
|
|
49
28
|
}
|
|
50
29
|
|
|
51
|
-
export function buildCompletionKey(data: Readonly<CompletionDataLike
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return `completion:${completionId}`;
|
|
55
|
-
}
|
|
56
|
-
const id = asNonEmptyString(data.id);
|
|
57
|
-
if (hasText(id)) {
|
|
58
|
-
return `id:${id}:${payloadDigest(data)}`;
|
|
30
|
+
export function buildCompletionKey(data: Readonly<CompletionDataLike>): string {
|
|
31
|
+
if (hasText(data.completionId)) {
|
|
32
|
+
return `completion:${data.completionId.trim()}`;
|
|
59
33
|
}
|
|
60
|
-
|
|
61
|
-
const agent = asNonEmptyString(data.agent) ?? "unknown";
|
|
62
|
-
const timestamp = asFiniteNumber(data.timestamp);
|
|
63
|
-
const taskIndex = asFiniteNumber(data.taskIndex);
|
|
64
|
-
const totalTasks = asFiniteNumber(data.totalTasks);
|
|
65
|
-
const success = data.success === true ? "1" : "?";
|
|
66
|
-
const successIdentity = data.success === false ? "0" : success;
|
|
67
|
-
return [
|
|
68
|
-
"meta",
|
|
69
|
-
sessionId,
|
|
70
|
-
agent,
|
|
71
|
-
timestamp !== undefined ? String(timestamp) : "no-ts",
|
|
72
|
-
taskIndex !== undefined ? String(taskIndex) : "-",
|
|
73
|
-
totalTasks !== undefined ? String(totalTasks) : "-",
|
|
74
|
-
successIdentity,
|
|
75
|
-
fallback,
|
|
76
|
-
].join(":");
|
|
34
|
+
return `id:${data.id.trim()}:${payloadDigest(data)}`;
|
|
77
35
|
}
|
|
78
36
|
|
|
79
37
|
/** Mutable identity store owned by the completion-admission lifecycle. */
|
|
@@ -105,24 +63,3 @@ export function markSeenWithTtl(
|
|
|
105
63
|
seen.set(key, now);
|
|
106
64
|
return false;
|
|
107
65
|
}
|
|
108
|
-
|
|
109
|
-
function isSeenMap(value: unknown): value is Map<string, number> {
|
|
110
|
-
if (!(value instanceof Map)) {
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
const entries: Map<unknown, unknown> = value;
|
|
114
|
-
return [...entries].every(
|
|
115
|
-
([key, timestamp]) => typeof key === "string" && typeof timestamp === "number",
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function getGlobalSeenMap(storeKey: string): Map<string, number> {
|
|
120
|
-
const globalStore = globalThis as Record<string, unknown>;
|
|
121
|
-
const existing = globalStore[storeKey];
|
|
122
|
-
if (isSeenMap(existing)) {
|
|
123
|
-
return existing;
|
|
124
|
-
}
|
|
125
|
-
const map = new Map<string, number>();
|
|
126
|
-
globalStore[storeKey] = map;
|
|
127
|
-
return map;
|
|
128
|
-
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/** Subagent completion notifications. */
|
|
2
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { buildCompletionKey, getGlobalSeenMap, markSeenWithTtl } from "./completion-dedupe.ts";
|
|
4
3
|
import { SUBAGENT_ASYNC_COMPLETE_EVENT } from "../../shared/types.ts";
|
|
5
4
|
import { hasText, isRecord } from "./async-value.ts";
|
|
6
5
|
import type { ReadonlyInput } from "../../shared/types/inputs.ts";
|
|
@@ -144,7 +143,7 @@ function isUnsubscribe(value: unknown): value is () => void {
|
|
|
144
143
|
|
|
145
144
|
export default function registerSubagentNotify(
|
|
146
145
|
pi: ReadonlyInput<ExtensionAPI>,
|
|
147
|
-
onQueued
|
|
146
|
+
onQueued: (completionKey: string, runId?: string) => void,
|
|
148
147
|
): () => void {
|
|
149
148
|
const unsubscribeStoreKey = "__pi_subagents_notify_unsubscribe__";
|
|
150
149
|
const globalStore = globalThis as Record<string, unknown>;
|
|
@@ -156,7 +155,6 @@ export default function registerSubagentNotify(
|
|
|
156
155
|
// Best effort cleanup for stale handlers from an older reload.
|
|
157
156
|
}
|
|
158
157
|
}
|
|
159
|
-
const seen = getGlobalSeenMap("__pi_subagents_notify_seen__");
|
|
160
158
|
const handleComplete = (data: unknown): void => {
|
|
161
159
|
if (
|
|
162
160
|
!isResult(data) ||
|
|
@@ -165,17 +163,9 @@ export default function registerSubagentNotify(
|
|
|
165
163
|
) {
|
|
166
164
|
return;
|
|
167
165
|
}
|
|
168
|
-
const key = buildCompletionKey(data, "notify");
|
|
169
|
-
// Owned completion keys are governed by the shared queue/journal lifecycle, not a TTL.
|
|
170
|
-
if (
|
|
171
|
-
(onQueued === undefined || !hasText(data.completionKey)) &&
|
|
172
|
-
markSeenWithTtl(seen, key, Date.now(), 10 * 60 * 1000)
|
|
173
|
-
) {
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
166
|
pi.sendMessage(buildNotification(data), { triggerTurn: true });
|
|
177
167
|
if (hasText(data.completionKey)) {
|
|
178
|
-
onQueued
|
|
168
|
+
onQueued(data.completionKey, data.runId);
|
|
179
169
|
}
|
|
180
170
|
};
|
|
181
171
|
const unsubscribe = pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, handleComplete);
|
|
@@ -292,7 +292,7 @@ class ResultWatcher {
|
|
|
292
292
|
if (this.foreign(envelope)) {
|
|
293
293
|
return;
|
|
294
294
|
}
|
|
295
|
-
const key = buildCompletionKey({ ...envelope.data, id: envelope.runId }
|
|
295
|
+
const key = buildCompletionKey({ ...envelope.data, id: envelope.runId });
|
|
296
296
|
if (this.alreadyHandled(envelope, key)) {
|
|
297
297
|
return;
|
|
298
298
|
}
|
|
@@ -59,6 +59,7 @@ export class RunnerChildObserver {
|
|
|
59
59
|
this.id = config.id;
|
|
60
60
|
this.hooks = hooks;
|
|
61
61
|
this.mutatingFailureStates = statusPayload.steps.map(() => createMutatingFailureState());
|
|
62
|
+
this.syncTopLevelCurrentTool();
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
expandChildren(flatIndex: number, count: number): void {
|
|
@@ -69,16 +70,21 @@ export class RunnerChildObserver {
|
|
|
69
70
|
);
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
const active =
|
|
74
|
-
.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
73
|
+
syncTopLevelCurrentTool(): void {
|
|
74
|
+
const active =
|
|
75
|
+
this.statusPayload.state === "running" || this.statusPayload.state === "queued"
|
|
76
|
+
? this.statusPayload.steps
|
|
77
|
+
.filter(
|
|
78
|
+
(step) =>
|
|
79
|
+
step.status === "running" &&
|
|
80
|
+
typeof step.currentTool === "string" &&
|
|
81
|
+
step.currentTool.length > 0,
|
|
82
|
+
)
|
|
83
|
+
.sort(
|
|
84
|
+
(left, right) => (right.currentToolStartedAt ?? 0) - (left.currentToolStartedAt ?? 0),
|
|
85
|
+
)
|
|
86
|
+
.at(0)
|
|
87
|
+
: undefined;
|
|
82
88
|
this.statusPayload.currentTool = active?.currentTool;
|
|
83
89
|
this.statusPayload.currentToolStartedAt = active?.currentToolStartedAt;
|
|
84
90
|
this.statusPayload.currentPath = active?.currentPath;
|
|
@@ -106,7 +112,9 @@ export class RunnerChildObserver {
|
|
|
106
112
|
}
|
|
107
113
|
step.streamingText = updateStreamingText(step.streamingText, event);
|
|
108
114
|
this.recordChildActivity(flatIndex, event, toolSnapshot, now);
|
|
109
|
-
|
|
115
|
+
if (event.type === "tool_execution_start" || event.type === "tool_execution_end") {
|
|
116
|
+
this.syncTopLevelCurrentTool();
|
|
117
|
+
}
|
|
110
118
|
step.lastActivityAt = now;
|
|
111
119
|
this.statusPayload.lastActivityAt = now;
|
|
112
120
|
this.statusPayload.lastUpdate = now;
|
|
@@ -166,6 +166,8 @@ export class RunnerMonitor {
|
|
|
166
166
|
clearTimeout(this.statusWriteTimer);
|
|
167
167
|
this.statusWriteTimer = undefined;
|
|
168
168
|
}
|
|
169
|
+
// Step completion, interruption and expansion can change activity without a tool-end event.
|
|
170
|
+
this.childObserver.syncTopLevelCurrentTool();
|
|
169
171
|
this.refreshWorkflowGraph();
|
|
170
172
|
writeAtomicJson(this.statusPath, this.statusPayload);
|
|
171
173
|
if (this.config.runtimeVersion !== 2) {
|
|
@@ -2,7 +2,6 @@ import * as path from "node:path";
|
|
|
2
2
|
import { inspect } from "node:util";
|
|
3
3
|
import type { ReadonlyDeep } from "type-fest";
|
|
4
4
|
import { appendJsonl } from "../../shared/artifacts.ts";
|
|
5
|
-
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
6
5
|
import { writeInitialProgressFile } from "../../shared/settings.ts";
|
|
7
6
|
import { completeWorkflowStep, runParallelTasks } from "../shared/workflow-policy.ts";
|
|
8
7
|
import {
|
|
@@ -194,7 +193,7 @@ export class RunnerParallelGroup {
|
|
|
194
193
|
status.currentStep = position.flatIndex;
|
|
195
194
|
status.lastUpdate = failedAt;
|
|
196
195
|
status.outputFile = path.join(this.config.asyncDir, `output-${position.flatIndex}.log`);
|
|
197
|
-
|
|
196
|
+
this.monitor.writeStatusPayload();
|
|
198
197
|
this.append({
|
|
199
198
|
type: "subagent.parallel.completed",
|
|
200
199
|
ts: failedAt,
|
|
@@ -224,7 +223,7 @@ export class RunnerParallelGroup {
|
|
|
224
223
|
status.lastActivityAt = startedAt;
|
|
225
224
|
status.lastUpdate = startedAt;
|
|
226
225
|
status.outputFile = path.join(this.config.asyncDir, `output-${position.flatIndex}.log`);
|
|
227
|
-
|
|
226
|
+
this.monitor.writeStatusPayload();
|
|
228
227
|
this.append({
|
|
229
228
|
type: "subagent.parallel.started",
|
|
230
229
|
ts: startedAt,
|
|
@@ -110,9 +110,7 @@ export function publishStepOutput(
|
|
|
110
110
|
originalPath.length > 0 &&
|
|
111
111
|
step.outputMode !== "file-only" &&
|
|
112
112
|
step.outputPathFromAgentDefault === true;
|
|
113
|
-
const cleanup = shouldConsume
|
|
114
|
-
? cleanupSingleOutputFile(originalPath, output, undefined)
|
|
115
|
-
: undefined;
|
|
113
|
+
const cleanup = shouldConsume ? cleanupSingleOutputFile(originalPath, output) : undefined;
|
|
116
114
|
const savedPath = relocateOutput(ctx, originalPath, cleanup);
|
|
117
115
|
const reference = outputReference(savedPath, state.resolvedOutput.fullOutput, cleanup);
|
|
118
116
|
const displayOutput = finalizeSingleOutput({
|
|
@@ -402,7 +402,7 @@ export function formatAcceptanceFinalizationPrompt(input: {
|
|
|
402
402
|
return lines.join("\n");
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
-
|
|
405
|
+
function createFinalizationTurn(input: {
|
|
406
406
|
readonly turn: number;
|
|
407
407
|
readonly prompt: string;
|
|
408
408
|
readonly rawOutput: string;
|
|
@@ -447,7 +447,7 @@ export function createFinalizationProcessFailureTurn(input: {
|
|
|
447
447
|
};
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
-
|
|
450
|
+
function attachFinalizationToLedger(input: {
|
|
451
451
|
readonly initialLedger: AcceptanceLedger;
|
|
452
452
|
readonly authoritativeLedger: AcceptanceLedger;
|
|
453
453
|
readonly turns: readonly AcceptanceFinalizationTurn[];
|
|
@@ -18,10 +18,8 @@ export {
|
|
|
18
18
|
evaluateAcceptanceReport,
|
|
19
19
|
} from "./acceptance-evaluation.ts";
|
|
20
20
|
export {
|
|
21
|
-
attachFinalizationToLedger,
|
|
22
21
|
buildFinalizationProcessFailureLedger,
|
|
23
22
|
createFinalizationProcessFailureTurn,
|
|
24
|
-
createFinalizationTurn,
|
|
25
23
|
createFinalizationReportRuntime,
|
|
26
24
|
type FinalizationReportSubmission,
|
|
27
25
|
readFinalizationReport,
|
|
@@ -51,8 +51,11 @@ export class ChildObservationSpool {
|
|
|
51
51
|
fs.writeFileSync(this.stderr, text);
|
|
52
52
|
}
|
|
53
53
|
resetText(): void {
|
|
54
|
-
this.textOffset
|
|
54
|
+
if (this.textOffset === 0) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
55
57
|
fs.ftruncateSync(this.text, 0);
|
|
58
|
+
this.textOffset = 0;
|
|
56
59
|
}
|
|
57
60
|
appendText(text: string): void {
|
|
58
61
|
const bytes = Buffer.from(text);
|
|
@@ -72,14 +75,16 @@ export class ChildObservationSpool {
|
|
|
72
75
|
this.textOffset += bytes.length;
|
|
73
76
|
}
|
|
74
77
|
replayText(consume: (text: string) => void): void {
|
|
75
|
-
|
|
78
|
+
if (this.textOffset > 0) {
|
|
79
|
+
this.decode(this.text, 0, this.textOffset, consume);
|
|
80
|
+
}
|
|
76
81
|
}
|
|
77
82
|
replayWire(start: number, end: number, consume: (text: string) => void): void {
|
|
78
83
|
this.decode(this.wire, start, end, consume);
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
private copy(fd: number, start: number, end: number, consume: (bytes: Buffer) => void): void {
|
|
82
|
-
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
87
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, end - start));
|
|
83
88
|
for (let position = start; position < end;) {
|
|
84
89
|
const count = fs.readSync(fd, buffer, 0, Math.min(buffer.length, end - position), position);
|
|
85
90
|
if (count === 0) {
|
|
@@ -154,6 +154,9 @@ const SAME_MODEL_RECOVERY_PATTERNS = [
|
|
|
154
154
|
/socket/i,
|
|
155
155
|
/connection/i,
|
|
156
156
|
/fetch failed/i,
|
|
157
|
+
// Node fetch reports a response body cut off mid-stream as bare "terminated"; Pi retries it too.
|
|
158
|
+
// Match the whole message: process failures embed stderr and paths that can contain the word.
|
|
159
|
+
/^\s*terminated\s*$/i,
|
|
157
160
|
/network/i,
|
|
158
161
|
/http idle/i,
|
|
159
162
|
/timed? out/i,
|
|
@@ -44,8 +44,6 @@ export function flattenSteps(
|
|
|
44
44
|
return flat;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export { mapConcurrent } from "../../shared/concurrency.ts";
|
|
48
|
-
|
|
49
47
|
export interface ParallelTaskResult {
|
|
50
48
|
agent: string;
|
|
51
49
|
taskIndex?: number;
|
|
@@ -54,8 +52,6 @@ export interface ParallelTaskResult {
|
|
|
54
52
|
error?: string;
|
|
55
53
|
model?: string;
|
|
56
54
|
attemptedModels?: string[];
|
|
57
|
-
outputTargetPath?: string;
|
|
58
|
-
outputTargetExists?: boolean;
|
|
59
55
|
}
|
|
60
56
|
|
|
61
57
|
function parallelResultStatus(
|
|
@@ -63,30 +59,10 @@ function parallelResultStatus(
|
|
|
63
59
|
hasOutput: boolean,
|
|
64
60
|
): string {
|
|
65
61
|
const error = result.error ?? "";
|
|
66
|
-
const suffix = error.length > 0 ? `: ${error}` : "";
|
|
67
|
-
if (result.exitCode === -1) {
|
|
68
|
-
return `SKIPPED${suffix}`;
|
|
69
|
-
}
|
|
70
|
-
if (result.exitCode !== 0 && result.exitCode !== null) {
|
|
71
|
-
return `FAILED (exit code ${result.exitCode})${suffix}`;
|
|
72
|
-
}
|
|
73
62
|
if (error.length > 0) {
|
|
74
63
|
return `WARNING: ${error}`;
|
|
75
64
|
}
|
|
76
|
-
return
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function emptyOutputStatus(result: ReadonlyInput<ParallelTaskResult>, hasOutput: boolean): string {
|
|
80
|
-
if (hasOutput) {
|
|
81
|
-
return "";
|
|
82
|
-
}
|
|
83
|
-
const target = result.outputTargetPath ?? "";
|
|
84
|
-
if (target.length === 0) {
|
|
85
|
-
return "EMPTY OUTPUT (no textual response returned)";
|
|
86
|
-
}
|
|
87
|
-
return result.outputTargetExists === false
|
|
88
|
-
? `EMPTY OUTPUT (expected output file missing: ${target})`
|
|
89
|
-
: "";
|
|
65
|
+
return hasOutput ? "" : "EMPTY OUTPUT (no textual response returned)";
|
|
90
66
|
}
|
|
91
67
|
|
|
92
68
|
export function aggregateParallelOutputs(
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import {
|
|
23
23
|
getRunMetadataDir,
|
|
24
24
|
migrateSupervisorQuestionSteps,
|
|
25
|
+
readQuestionOwner,
|
|
25
26
|
readRunJson,
|
|
26
27
|
saveQuestionOwner,
|
|
27
28
|
saveRunStatus,
|
|
@@ -254,14 +255,13 @@ function nativeLaunchCwd(run: OwnedRun, details: ReadonlyDetails): OwnedRun {
|
|
|
254
255
|
}
|
|
255
256
|
|
|
256
257
|
function repairQuestionOwner(run: OwnedRun): void {
|
|
257
|
-
let
|
|
258
|
+
let sessionId: string | undefined;
|
|
258
259
|
try {
|
|
259
|
-
|
|
260
|
+
sessionId = readQuestionOwner(run.runId)?.sessionId;
|
|
260
261
|
} catch {
|
|
261
262
|
// A genuine receipt can repair unusable supplemental metadata.
|
|
262
263
|
}
|
|
263
|
-
|
|
264
|
-
if (typeof sessionId !== "string" || sessionId.trim() === "") {
|
|
264
|
+
if (sessionId === undefined || sessionId.trim() === "") {
|
|
265
265
|
saveQuestionOwner(run.runId, run.ownerSessionId);
|
|
266
266
|
}
|
|
267
267
|
}
|
|
@@ -325,7 +325,6 @@ function unchangedOutputIdentity(fd: number, outputPath: string, stat: fs.Stats)
|
|
|
325
325
|
export function cleanupSingleOutputFile(
|
|
326
326
|
outputPath: string | undefined,
|
|
327
327
|
fullOutput: string,
|
|
328
|
-
beforeRun: SingleOutputSnapshot | undefined,
|
|
329
328
|
): SingleOutputCleanupResult | undefined {
|
|
330
329
|
if (!nonempty(outputPath)) {
|
|
331
330
|
return undefined;
|
|
@@ -345,9 +344,6 @@ export function cleanupSingleOutputFile(
|
|
|
345
344
|
if (!equal) {
|
|
346
345
|
return { path: absolutePath, action: "skipped", reason: "file changed after capture" };
|
|
347
346
|
}
|
|
348
|
-
if (matchesSnapshot(stat, beforeRun)) {
|
|
349
|
-
return { path: absolutePath, action: "skipped", reason: "file preexisted and was unchanged" };
|
|
350
|
-
}
|
|
351
347
|
fs.unlinkSync(outputPath);
|
|
352
348
|
return { path: absolutePath, action: "deleted" };
|
|
353
349
|
} catch (error) {
|
|
@@ -24,7 +24,7 @@ const STRUCTURED_OUTPUT_INSTRUCTIONS = [
|
|
|
24
24
|
"Do not rely on prose-only completion; if you do not call `structured_output`, the parent will fail this step.",
|
|
25
25
|
].join("\n");
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
const CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS = [
|
|
28
28
|
"You are a child subagent, not the parent orchestrator.",
|
|
29
29
|
"The parent session owns delegation, orchestration, review fanout, and follow-up worker launches.",
|
|
30
30
|
"Ignore prior parent-only orchestration instructions in inherited conversation history.",
|
|
@@ -32,7 +32,7 @@ export const CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS = [
|
|
|
32
32
|
"If you need to edit files, call the actual edit/write tools. Do not print tool-call syntax, patches, or pseudo-tool calls as text.",
|
|
33
33
|
].join("\n");
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
const CHILD_FANOUT_BOUNDARY_INSTRUCTIONS = [
|
|
36
36
|
"You are a child subagent with delegation enabled for your assigned task.",
|
|
37
37
|
"You may delegate useful helper work within that task when it saves time or improves quality, using the available delegation tools.",
|
|
38
38
|
"You remain responsible for your assigned result. The original parent owns integration, review synthesis, and final delivery.",
|
|
@@ -216,7 +216,10 @@ export function createSupervisorQuestion(
|
|
|
216
216
|
return question;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
export function listRunQuestions(
|
|
219
|
+
export function listRunQuestions(
|
|
220
|
+
runDir: string,
|
|
221
|
+
ownerSessionId?: string,
|
|
222
|
+
): SupervisorQuestionView[] {
|
|
220
223
|
let entries: fs.Dirent[];
|
|
221
224
|
try {
|
|
222
225
|
entries = fs.readdirSync(path.join(runDir, "questions"), { withFileTypes: true });
|
|
@@ -233,7 +236,10 @@ export function listRunQuestions(runDir: string): SupervisorQuestionView[] {
|
|
|
233
236
|
path.join(runDir, "questions", entry.name, "question.json"),
|
|
234
237
|
parseSupervisorQuestion,
|
|
235
238
|
);
|
|
236
|
-
return question
|
|
239
|
+
return question &&
|
|
240
|
+
(ownerSessionId === undefined || question.ownerSessionId === ownerSessionId)
|
|
241
|
+
? [readQuestionState(question, path.dirname(runDir))]
|
|
242
|
+
: [];
|
|
237
243
|
});
|
|
238
244
|
}
|
|
239
245
|
|
|
@@ -243,9 +249,7 @@ export function listOwnedRunQuestions(
|
|
|
243
249
|
runId: string,
|
|
244
250
|
): SupervisorQuestionView[] {
|
|
245
251
|
migrateSupervisorQuestions(ownerSessionId, runId);
|
|
246
|
-
return listRunQuestions(getRunMetadataDir(runId))
|
|
247
|
-
(question) => question.ownerSessionId === ownerSessionId,
|
|
248
|
-
);
|
|
252
|
+
return listRunQuestions(getRunMetadataDir(runId), ownerSessionId);
|
|
249
253
|
}
|
|
250
254
|
|
|
251
255
|
export function pendingSupervisorQuestion(input: {
|
|
@@ -321,11 +325,7 @@ function* supervisorQuestionSteps(
|
|
|
321
325
|
for (const entry of runs) {
|
|
322
326
|
yield;
|
|
323
327
|
try {
|
|
324
|
-
questions.push(
|
|
325
|
-
...listRunQuestions(path.join(root, entry.name)).filter(
|
|
326
|
-
(question) => question.ownerSessionId === ownerSessionId,
|
|
327
|
-
),
|
|
328
|
-
);
|
|
328
|
+
questions.push(...listRunQuestions(path.join(root, entry.name), ownerSessionId));
|
|
329
329
|
} catch (error) {
|
|
330
330
|
if (runId !== undefined) {
|
|
331
331
|
throw error;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { errorMessage } from "../../shared/unknown.ts";
|
|
2
|
+
import { mapConcurrent } from "../../shared/concurrency.ts";
|
|
2
3
|
import type { DynamicParallelStep } from "../../shared/types/workflow.ts";
|
|
3
4
|
import type {
|
|
4
5
|
AcceptanceLedger,
|
|
@@ -17,7 +18,6 @@ import {
|
|
|
17
18
|
import {
|
|
18
19
|
aggregateParallelOutputs,
|
|
19
20
|
FAIL_FAST_REASON,
|
|
20
|
-
mapConcurrent,
|
|
21
21
|
type ParallelTaskResult,
|
|
22
22
|
} from "./parallel-utils.ts";
|
|
23
23
|
|
|
@@ -172,7 +172,7 @@ export function readJsonProjection(file: string, select: Projection): UnknownRec
|
|
|
172
172
|
const projection = new JsonProjection(select, undefined, () => Infinity);
|
|
173
173
|
try {
|
|
174
174
|
const end = fs.fstatSync(fd).size;
|
|
175
|
-
const bytes = Buffer.allocUnsafe(64 * 1024);
|
|
175
|
+
const bytes = Buffer.allocUnsafe(Math.min(64 * 1024, end));
|
|
176
176
|
for (let offset = 0; offset < end;) {
|
|
177
177
|
const count = fs.readSync(fd, bytes, 0, Math.min(bytes.length, end - offset), offset);
|
|
178
178
|
if (count === 0) {
|
package/src/shared/settings.ts
CHANGED
|
@@ -126,7 +126,7 @@ export function resolveStepBehavior(
|
|
|
126
126
|
function resolveChainPath(filePath: string, chainDir: string): string {
|
|
127
127
|
return path.isAbsolute(filePath) ? filePath : path.join(chainDir, filePath);
|
|
128
128
|
}
|
|
129
|
-
/**
|
|
129
|
+
/** Compose read paths and progress instructions for a chain child. */
|
|
130
130
|
export function buildChainInstructions(
|
|
131
131
|
behavior: ResolvedStepBehavior,
|
|
132
132
|
chainDir: string,
|
|
@@ -139,9 +139,6 @@ export function buildChainInstructions(
|
|
|
139
139
|
`[Read from: ${behavior.reads.map((file) => resolveChainPath(file, chainDir)).join(", ")}]`,
|
|
140
140
|
);
|
|
141
141
|
}
|
|
142
|
-
if (behavior.output !== false && behavior.output.length > 0) {
|
|
143
|
-
prefixParts.push(`[Write to: ${resolveChainPath(behavior.output, chainDir)}]`);
|
|
144
|
-
}
|
|
145
142
|
if (behavior.progress) {
|
|
146
143
|
const progressPath = path.join(chainDir, "progress.md");
|
|
147
144
|
suffixParts.push(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { discoverAgents,
|
|
2
|
+
import { discoverAgents, discoverChains } from "../agents/agents.ts";
|
|
3
3
|
import type { ChainConfig } from "../shared/types/config.ts";
|
|
4
4
|
import { resolveExecutionCwd } from "../shared/execution-cwd.ts";
|
|
5
5
|
|
|
@@ -51,21 +51,19 @@ export function makeAgentCompletions(
|
|
|
51
51
|
multiAgent: boolean,
|
|
52
52
|
): (prefix: string) => Completion[] | null {
|
|
53
53
|
return (prefix) => {
|
|
54
|
+
const position = multiAgent ? completionPosition(prefix) : { word: prefix, before: "" };
|
|
55
|
+
if (!position || (!multiAgent && prefix.includes(" "))) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
54
58
|
const cwd = completionCwd(pi, state);
|
|
55
59
|
if (cwd === null || cwd.length === 0) {
|
|
56
60
|
return null;
|
|
57
61
|
}
|
|
58
62
|
const agents = discoverAgents(cwd, "both", { projectTrusted: projectTrusted(state) }).agents;
|
|
59
63
|
if (!multiAgent) {
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
:
|
|
63
|
-
.filter((agent) => agent.name.startsWith(prefix))
|
|
64
|
-
.map((agent) => ({ value: agent.name, label: agent.name }));
|
|
65
|
-
}
|
|
66
|
-
const position = completionPosition(prefix);
|
|
67
|
-
if (!position) {
|
|
68
|
-
return null;
|
|
64
|
+
return agents
|
|
65
|
+
.filter((agent) => agent.name.startsWith(prefix))
|
|
66
|
+
.map((agent) => ({ value: agent.name, label: agent.name }));
|
|
69
67
|
}
|
|
70
68
|
const { word: lastWord, before: beforeLastWord } = position;
|
|
71
69
|
if (lastWord === "->") {
|
|
@@ -79,7 +77,7 @@ export function makeAgentCompletions(
|
|
|
79
77
|
|
|
80
78
|
export function discoverSavedChains(cwd: string, trusted: boolean): ChainConfig[] {
|
|
81
79
|
const chains = new Map<string, ChainConfig>();
|
|
82
|
-
for (const chain of
|
|
80
|
+
for (const chain of discoverChains(cwd, { projectTrusted: trusted }).chains) {
|
|
83
81
|
const existing = chains.get(chain.name);
|
|
84
82
|
const projectOverride = existing?.source === "user" && chain.source === "project";
|
|
85
83
|
const jsonOverride =
|
|
@@ -98,8 +96,11 @@ export function makeChainCompletions(
|
|
|
98
96
|
state: SlashDiscoveryState,
|
|
99
97
|
): (prefix: string) => Completion[] | null {
|
|
100
98
|
return (prefix) => {
|
|
99
|
+
if (prefix.includes(" ")) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
101
102
|
const cwd = completionCwd(pi, state);
|
|
102
|
-
if (
|
|
103
|
+
if (cwd === null || cwd.length === 0) {
|
|
103
104
|
return null;
|
|
104
105
|
}
|
|
105
106
|
return discoverSavedChains(cwd, projectTrusted(state))
|
package/src/tui/agent-history.ts
CHANGED
|
@@ -1,27 +1,10 @@
|
|
|
1
|
-
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
1
|
import { stripAcceptanceReport } from "../runs/shared/acceptance-reports.ts";
|
|
3
|
-
import {
|
|
4
|
-
import type { DisplayEntry } from "./history-record.ts";
|
|
2
|
+
import type { AgentHistory, AgentHistoryItem } from "./history-display.ts";
|
|
5
3
|
import { readableText } from "./history-text.ts";
|
|
6
4
|
export { readableText } from "./history-text.ts";
|
|
7
5
|
export { indexedHistory } from "./indexed-history.ts";
|
|
8
6
|
export type { AgentHistory, AgentHistoryItem } from "./history-display.ts";
|
|
9
7
|
|
|
10
|
-
/** Read native entries, not a compacted model context: earlier messages remain inspectable. */
|
|
11
|
-
export function historyItems(entries: readonly SessionEntry[]): AgentHistory {
|
|
12
|
-
const visible: DisplayEntry[] = [];
|
|
13
|
-
for (const entry of entries) {
|
|
14
|
-
if (
|
|
15
|
-
entry.type === "message" ||
|
|
16
|
-
entry.type === "custom_message" ||
|
|
17
|
-
entry.type === "compaction" ||
|
|
18
|
-
entry.type === "branch_summary"
|
|
19
|
-
) {
|
|
20
|
-
visible.push(entry.type === "message" ? { ...entry, native: entry.message } : entry);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return displayHistory(visible);
|
|
24
|
-
}
|
|
25
8
|
function finalMatch(history: AgentHistory, text: string): string | undefined {
|
|
26
9
|
if (history.finalId !== undefined) {
|
|
27
10
|
return history.finalId;
|
package/src/tui/agent-view.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { errorText } from "./text-values.ts";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
3
|
import {
|
|
3
4
|
historyStatus,
|
|
4
5
|
pageNotices,
|
|
@@ -183,9 +184,22 @@ export class ConversationHistory {
|
|
|
183
184
|
? { ...item, load: () => this.controller.savedResult(this.key, item.id) }
|
|
184
185
|
: item,
|
|
185
186
|
);
|
|
187
|
+
const previous = task.page,
|
|
188
|
+
sameSource =
|
|
189
|
+
previous !== undefined &&
|
|
190
|
+
previous.sourceId === result.page.sourceId &&
|
|
191
|
+
previous.generation === result.page.generation &&
|
|
192
|
+
previous.sessionId === result.page.sessionId &&
|
|
193
|
+
previous.sourceState === result.page.sourceState &&
|
|
194
|
+
isDeepStrictEqual(
|
|
195
|
+
previous.entries.map((entry) => entry.ref),
|
|
196
|
+
result.page.entries.map((entry) => entry.ref),
|
|
197
|
+
);
|
|
186
198
|
this.seenSource ||= result.page.count > 0;
|
|
187
199
|
this.controller.applyHistoryPage(this.key, { ...history, items }, result.page, this.seenSource);
|
|
188
|
-
|
|
200
|
+
if (!sameSource) {
|
|
201
|
+
this.source.clearCards();
|
|
202
|
+
}
|
|
189
203
|
}
|
|
190
204
|
|
|
191
205
|
assignment(): AgentHistoryItem {
|