@cr1ms0n/pi-subagent 0.8.8 → 0.9.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 +14 -6
- package/README.md +218 -128
- package/docs/ARCHITECTURE.md +168 -132
- package/docs/COST-ACCOUNTING.md +116 -66
- package/docs/RELEASING.md +32 -32
- package/docs/SECURITY.md +125 -97
- package/docs/UX.md +158 -141
- package/package.json +2 -2
- package/skills/subagent/SKILL.md +142 -121
- package/src/agents.ts +282 -288
- package/src/backends/pi.ts +164 -94
- package/src/child-preflight.ts +166 -0
- package/src/config.ts +254 -252
- package/src/dispatch-preflight.ts +87 -0
- package/src/dispatch-routing.ts +56 -0
- package/src/extension.ts +368 -187
- package/src/format.ts +436 -365
- package/src/jev-router.ts +1036 -0
- package/src/orchestrator.ts +303 -312
- package/src/persistence.ts +643 -335
- package/src/policy.ts +562 -561
- package/src/process-lock.ts +730 -687
- package/src/protocol.ts +320 -290
- package/src/registry.ts +730 -632
- package/src/routing-policy.ts +268 -0
- package/src/routing-types.ts +217 -0
- package/src/runner.ts +1299 -850
- package/src/schema.ts +189 -189
- package/src/startup-check.ts +481 -0
- package/src/types.ts +208 -198
- package/src/usage.ts +316 -274
- package/src/context-policy.ts +0 -169
- package/src/model-policy.ts +0 -169
package/src/protocol.ts
CHANGED
|
@@ -1,290 +1,320 @@
|
|
|
1
|
-
import type { Message } from "@earendil-works/pi-ai";
|
|
2
|
-
import type { TaskResult, UsageStats } from "./types.js";
|
|
3
|
-
import { emptyUsage } from "./types.js";
|
|
4
|
-
import { addUsage, hasBilledUsage, usageFromMessage, usageFromToolResultMessage } from "./usage.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
| { type: "
|
|
9
|
-
| { type: "
|
|
10
|
-
| { type: "
|
|
11
|
-
| { type: "agent-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
private
|
|
32
|
-
private
|
|
33
|
-
private
|
|
34
|
-
private
|
|
35
|
-
private
|
|
36
|
-
private
|
|
37
|
-
private
|
|
38
|
-
|
|
39
|
-
private
|
|
40
|
-
|
|
41
|
-
private
|
|
42
|
-
private
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
private
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (!
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (event.type === "
|
|
133
|
-
if (
|
|
134
|
-
this.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
return [
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
this.
|
|
196
|
-
this.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
:
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
1
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { TaskResult, UsageStats } from "./types.js";
|
|
3
|
+
import { emptyUsage } from "./types.js";
|
|
4
|
+
import { addUsage, hasBilledUsage, usageFromMessage, usageFromToolResultMessage } from "./usage.js";
|
|
5
|
+
import { PREFLIGHT_ACK_TYPE } from "./startup-check.js";
|
|
6
|
+
|
|
7
|
+
export type ProtocolUpdate =
|
|
8
|
+
| { type: "session"; sessionId: string }
|
|
9
|
+
| { type: "live-text"; delta: string; liveText: string }
|
|
10
|
+
| { type: "message"; message: Message; usage: UsageStats }
|
|
11
|
+
| { type: "agent-end"; willRetry?: boolean }
|
|
12
|
+
| { type: "agent-settled" }
|
|
13
|
+
/** RPC extension UI dialog awaiting an answer; headless children auto-cancel. */
|
|
14
|
+
| { type: "ui-request"; id: string }
|
|
15
|
+
/**
|
|
16
|
+
* Correlated RPC command response (only when the request carried an `id`).
|
|
17
|
+
* Uncorrelated prompt/parse failures keep the legacy `fatal` behaviour below, so
|
|
18
|
+
* existing SDK callers that never set an `id` are unaffected.
|
|
19
|
+
*/
|
|
20
|
+
| { type: "rpc-response"; id: string; command: string; success: boolean; data?: unknown; error?: string }
|
|
21
|
+
/**
|
|
22
|
+
* Bounded startup-handshake acknowledgement from the private preflight extension.
|
|
23
|
+
* Kept out of `messages`, live text and usage: verification traffic is not task output.
|
|
24
|
+
*/
|
|
25
|
+
| { type: "preflight-ack"; content: string }
|
|
26
|
+
/** The child rejected the prompt; no agent events will follow. */
|
|
27
|
+
| { type: "fatal"; error: string };
|
|
28
|
+
|
|
29
|
+
/** Strict-enough, line-buffered parser for Pi's documented JSON event stream. */
|
|
30
|
+
export class ProtocolParser {
|
|
31
|
+
private buffer = "";
|
|
32
|
+
private sessionId?: string;
|
|
33
|
+
private messages: Message[] = [];
|
|
34
|
+
private usage = emptyUsage();
|
|
35
|
+
private liveText = "";
|
|
36
|
+
private parseErrors = 0;
|
|
37
|
+
private validEvents = 0;
|
|
38
|
+
private headerSeen = false;
|
|
39
|
+
private assistantEndSeen = false;
|
|
40
|
+
private agentEndSeen = false;
|
|
41
|
+
private agentSettledSeen = false;
|
|
42
|
+
private pendingRetry = false;
|
|
43
|
+
private model?: string;
|
|
44
|
+
private stopReason?: string;
|
|
45
|
+
private errorMessage?: string;
|
|
46
|
+
private transcriptLines: string[] = [];
|
|
47
|
+
private transcriptBytes = 0;
|
|
48
|
+
private transcriptTruncated = false;
|
|
49
|
+
private transcriptJoined?: string;
|
|
50
|
+
|
|
51
|
+
private static readonly MAX_TRANSCRIPT_BYTES = 32_768;
|
|
52
|
+
/** Guard against a single unbounded JSON line exhausting parent memory. */
|
|
53
|
+
private static readonly MAX_LINE_BYTES = 4 * 1024 * 1024;
|
|
54
|
+
private static readonly MAX_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
55
|
+
|
|
56
|
+
/** Append transcript lines incrementally; never re-flattens the full message list. */
|
|
57
|
+
private appendTranscript(lines: string[]): void {
|
|
58
|
+
for (const line of lines) {
|
|
59
|
+
if (!line) continue;
|
|
60
|
+
if (this.transcriptTruncated) return;
|
|
61
|
+
const bytes = Buffer.byteLength(line, "utf8") + 1;
|
|
62
|
+
if (this.transcriptBytes + bytes > ProtocolParser.MAX_TRANSCRIPT_BYTES) {
|
|
63
|
+
this.transcriptLines.push("[transcript truncated]");
|
|
64
|
+
this.transcriptTruncated = true;
|
|
65
|
+
this.transcriptJoined = undefined;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this.transcriptLines.push(line);
|
|
69
|
+
this.transcriptBytes += bytes;
|
|
70
|
+
this.transcriptJoined = undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private transcriptFromMessage(message: any): string[] {
|
|
75
|
+
if (message.role === "assistant" && Array.isArray(message.content)) {
|
|
76
|
+
return (message.content as any[]).flatMap((part) => {
|
|
77
|
+
if (part?.type === "text" && part.text) return [String(part.text)];
|
|
78
|
+
if (part?.type === "toolCall") return [`→ ${part.name} ${JSON.stringify(part.arguments ?? {})}`];
|
|
79
|
+
return [];
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (message.role === "toolResult") {
|
|
83
|
+
const text = Array.isArray(message.content)
|
|
84
|
+
? message.content.filter((part: any) => part?.type === "text").map((part: any) => part.text).join("")
|
|
85
|
+
: "";
|
|
86
|
+
return [`← ${message.toolName}${message.isError ? " [error]" : ""} ${text}`];
|
|
87
|
+
}
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
feed(data: Buffer | string): ProtocolUpdate[] {
|
|
92
|
+
this.buffer += data.toString();
|
|
93
|
+
// If a single line grows past the hard limit without a newline, drop it as a parse error.
|
|
94
|
+
if (Buffer.byteLength(this.buffer, "utf8") > ProtocolParser.MAX_BUFFER_BYTES) {
|
|
95
|
+
this.parseErrors++;
|
|
96
|
+
this.buffer = "";
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
const lines = this.buffer.split("\n");
|
|
100
|
+
this.buffer = lines.pop() ?? "";
|
|
101
|
+
return lines.flatMap((line) => this.parseLine(line));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Parse a final JSON object even when stdout omitted its trailing newline. */
|
|
105
|
+
flush(): ProtocolUpdate[] {
|
|
106
|
+
if (!this.buffer.trim()) return [];
|
|
107
|
+
const line = this.buffer;
|
|
108
|
+
this.buffer = "";
|
|
109
|
+
return this.parseLine(line);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private parseLine(line: string): ProtocolUpdate[] {
|
|
113
|
+
const trimmed = line.trim();
|
|
114
|
+
if (!trimmed) return [];
|
|
115
|
+
if (Buffer.byteLength(trimmed, "utf8") > ProtocolParser.MAX_LINE_BYTES) {
|
|
116
|
+
this.parseErrors++;
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
let event: any;
|
|
120
|
+
try {
|
|
121
|
+
event = JSON.parse(trimmed);
|
|
122
|
+
} catch {
|
|
123
|
+
this.parseErrors++;
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
if (!event || typeof event !== "object" || typeof event.type !== "string") {
|
|
127
|
+
this.parseErrors++;
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
this.validEvents++;
|
|
131
|
+
|
|
132
|
+
if (event.type === "session") {
|
|
133
|
+
if (typeof event.id !== "string" || !event.id) {
|
|
134
|
+
this.parseErrors++;
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
this.sessionId = event.id;
|
|
138
|
+
this.headerSeen = true;
|
|
139
|
+
return [{ type: "session", sessionId: event.id }];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// RPC-mode command responses. get_state supplies the session identity
|
|
143
|
+
// (RPC mode has no print-mode session header line).
|
|
144
|
+
if (event.type === "response") {
|
|
145
|
+
const updates: ProtocolUpdate[] = [];
|
|
146
|
+
const correlated = typeof event.id === "string" && event.id.length > 0;
|
|
147
|
+
if (correlated) {
|
|
148
|
+
updates.push({
|
|
149
|
+
type: "rpc-response",
|
|
150
|
+
id: event.id,
|
|
151
|
+
command: typeof event.command === "string" ? event.command : "",
|
|
152
|
+
success: event.success === true,
|
|
153
|
+
data: event.data,
|
|
154
|
+
error: typeof event.error === "string" ? event.error : event.error === undefined ? undefined : String(event.error),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
if (event.command === "get_state" && event.success && typeof event.data?.sessionId === "string" && event.data.sessionId) {
|
|
158
|
+
this.sessionId = event.data.sessionId;
|
|
159
|
+
this.headerSeen = true;
|
|
160
|
+
updates.push({ type: "session", sessionId: this.sessionId! });
|
|
161
|
+
}
|
|
162
|
+
// A rejected prompt means the child will idle forever; surface it. Correlated
|
|
163
|
+
// responses are owned by their requester, which can report a precise reason.
|
|
164
|
+
if (!correlated && event.success === false && (event.command === "prompt" || event.command === "parse")) {
|
|
165
|
+
updates.push({ type: "fatal", error: String(event.error ?? "child rejected the prompt") });
|
|
166
|
+
}
|
|
167
|
+
return updates;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Extension UI dialogs block the child until answered. Headless subagents
|
|
171
|
+
// cannot answer; the runner replies with a cancellation.
|
|
172
|
+
if (event.type === "extension_ui_request") {
|
|
173
|
+
const dialog = ["select", "confirm", "input", "editor"].includes(event.method);
|
|
174
|
+
return dialog && typeof event.id === "string" ? [{ type: "ui-request", id: event.id }] : [];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (event.type === "message_update" && event.message?.role === "assistant") {
|
|
178
|
+
const delta =
|
|
179
|
+
typeof event.assistantMessageEvent?.delta === "string"
|
|
180
|
+
? event.assistantMessageEvent.delta
|
|
181
|
+
: this.textParts(event.message).join("");
|
|
182
|
+
if (!delta) return [];
|
|
183
|
+
// Cap live text growth: keep the last 64KB of visible text so long runs stay bounded.
|
|
184
|
+
this.liveText = (this.liveText + delta).slice(-64 * 1024);
|
|
185
|
+
return [{ type: "live-text", delta, liveText: this.liveText }];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (event.type === "message_end" && event.message) {
|
|
189
|
+
const message = event.message as Message & { customType?: unknown };
|
|
190
|
+
// Startup-handshake acknowledgement: report it, but never fold verification
|
|
191
|
+
// traffic into messages, transcript, live text or usage accounting.
|
|
192
|
+
if (message.customType === PREFLIGHT_ACK_TYPE) {
|
|
193
|
+
return [{ type: "preflight-ack", content: typeof (message as any).content === "string" ? (message as any).content : "" }];
|
|
194
|
+
}
|
|
195
|
+
this.messages.push(message);
|
|
196
|
+
this.appendTranscript(this.transcriptFromMessage(message));
|
|
197
|
+
if (message.role === "assistant") {
|
|
198
|
+
this.assistantEndSeen = true;
|
|
199
|
+
this.usage = addUsage(this.usage, usageFromMessage(message));
|
|
200
|
+
this.model ||= message.model;
|
|
201
|
+
this.stopReason = message.stopReason;
|
|
202
|
+
this.errorMessage = message.errorMessage;
|
|
203
|
+
const text = this.textParts(message).join("");
|
|
204
|
+
if (text) this.liveText = text;
|
|
205
|
+
} else if (message.role === "toolResult") {
|
|
206
|
+
// Pi ≥ #6671: tool results may carry nested LLM usage (e.g. a
|
|
207
|
+
// grandchild subagent). Fold it into the run's cumulative spend so
|
|
208
|
+
// budgets and parent ledgers see true subtree cost.
|
|
209
|
+
const nested = usageFromToolResultMessage(message);
|
|
210
|
+
if (hasBilledUsage(nested)) this.usage = addUsage(this.usage, nested);
|
|
211
|
+
}
|
|
212
|
+
return [{ type: "message", message, usage: { ...this.usage } }];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (event.type === "agent_end") {
|
|
216
|
+
this.agentEndSeen = true;
|
|
217
|
+
// Pi may retry after agent_end (willRetry: true). That is NOT terminal;
|
|
218
|
+
// only agent_settled marks a fully settled run.
|
|
219
|
+
const willRetry = event.willRetry === true;
|
|
220
|
+
this.pendingRetry = willRetry;
|
|
221
|
+
return [{ type: "agent-end", willRetry }];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (event.type === "agent_settled") {
|
|
225
|
+
this.agentSettledSeen = true;
|
|
226
|
+
this.pendingRetry = false;
|
|
227
|
+
return [{ type: "agent-settled" }];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Other documented event types are valid but do not affect this projection.
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private textParts(message: any): string[] {
|
|
235
|
+
if (!Array.isArray(message?.content)) return [];
|
|
236
|
+
return message.content
|
|
237
|
+
.filter((part: any) => part?.type === "text" && typeof part.text === "string")
|
|
238
|
+
.map((part: any) => part.text);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
finalize(exitCode: number | null, signal?: NodeJS.Signals, stderr = ""): TaskResult {
|
|
242
|
+
this.flush();
|
|
243
|
+
const protocol = {
|
|
244
|
+
headerSeen: this.headerSeen,
|
|
245
|
+
assistantEndSeen: this.assistantEndSeen,
|
|
246
|
+
agentEndSeen: this.agentEndSeen,
|
|
247
|
+
agentSettledSeen: this.agentSettledSeen,
|
|
248
|
+
validEvents: this.validEvents,
|
|
249
|
+
parseErrors: this.parseErrors,
|
|
250
|
+
};
|
|
251
|
+
// Prefer agent_settled as the true terminal watermark. Fall back to
|
|
252
|
+
// agent_end without a pending retry for older Pi builds that never emitted
|
|
253
|
+
// agent_settled (json print historically closed after agent_end).
|
|
254
|
+
const settled = this.agentSettledSeen || (this.agentEndSeen && !this.pendingRetry);
|
|
255
|
+
const completeProtocol = this.headerSeen && this.assistantEndSeen && settled;
|
|
256
|
+
const assistantFailed = this.stopReason === "error" || this.stopReason === "aborted";
|
|
257
|
+
const successfulExit = exitCode === 0 && !signal && !assistantFailed;
|
|
258
|
+
// Preserve partial paid work when the process crashed (signal/nonzero) AFTER
|
|
259
|
+
// producing assistant output but BEFORE a complete terminal protocol. A
|
|
260
|
+
// complete protocol that still exits nonzero remains "failed".
|
|
261
|
+
const hasUsefulOutput = this.assistantEndSeen && (this.liveText.length > 0 || this.usage.turns > 0);
|
|
262
|
+
let state: TaskResult["state"];
|
|
263
|
+
if (successfulExit && completeProtocol) {
|
|
264
|
+
state = "completed";
|
|
265
|
+
} else if (hasUsefulOutput && !assistantFailed && !completeProtocol) {
|
|
266
|
+
state = "partial";
|
|
267
|
+
} else if (hasUsefulOutput && !assistantFailed && signal) {
|
|
268
|
+
// Complete protocol but killed by signal (e.g. parent shutdown): partial.
|
|
269
|
+
state = "partial";
|
|
270
|
+
} else {
|
|
271
|
+
state = "failed";
|
|
272
|
+
}
|
|
273
|
+
const stopReason = signal
|
|
274
|
+
? "unexpected_signal"
|
|
275
|
+
: exitCode !== 0
|
|
276
|
+
? "nonzero_exit"
|
|
277
|
+
: !completeProtocol
|
|
278
|
+
? "protocol_error"
|
|
279
|
+
: assistantFailed
|
|
280
|
+
? this.stopReason!
|
|
281
|
+
: this.stopReason || "stop";
|
|
282
|
+
return {
|
|
283
|
+
label: "subagent",
|
|
284
|
+
task: "",
|
|
285
|
+
state,
|
|
286
|
+
exitCode: exitCode === 0 && state === "failed" ? 1 : exitCode,
|
|
287
|
+
signal,
|
|
288
|
+
messages: [...this.messages],
|
|
289
|
+
stderr,
|
|
290
|
+
usage: { ...this.usage },
|
|
291
|
+
model: this.model,
|
|
292
|
+
stopReason,
|
|
293
|
+
errorMessage:
|
|
294
|
+
this.errorMessage ||
|
|
295
|
+
(signal ? `Subagent terminated unexpectedly by ${signal}` : undefined) ||
|
|
296
|
+
(exitCode !== 0 ? `Subagent exited with code ${exitCode}` : undefined) ||
|
|
297
|
+
(state === "partial" && !completeProtocol
|
|
298
|
+
? "Protocol stream truncated; partial output preserved"
|
|
299
|
+
: undefined),
|
|
300
|
+
liveText: this.liveText || undefined,
|
|
301
|
+
transcript: this.getTranscript(),
|
|
302
|
+
protocol,
|
|
303
|
+
sessionId: this.sessionId,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Cached join; safe to call on every progress tick. */
|
|
308
|
+
getTranscript(): string | undefined {
|
|
309
|
+
if (this.transcriptJoined === undefined) this.transcriptJoined = this.transcriptLines.join("\n");
|
|
310
|
+
return this.transcriptJoined || undefined;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
getLiveText(): string {
|
|
314
|
+
return this.liveText;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
getMessages(): Message[] {
|
|
318
|
+
return [...this.messages];
|
|
319
|
+
}
|
|
320
|
+
}
|