tau-coding-agent 0.1.4 → 0.1.6
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 +1 -1
- package/extensions/answer.ts +59 -47
- package/extensions/branch-term.ts +8 -13
- package/extensions/btw.ts +77 -6
- package/extensions/ghostty.ts +64 -11
- package/extensions/insights.ts +42 -4
- package/extensions/loop.ts +51 -12
- package/extensions/memory.ts +35 -17
- package/extensions/notify.ts +24 -5
- package/extensions/openai-fast.ts +6 -4
- package/extensions/openai-verbosity.ts +6 -4
- package/extensions/review/fix.ts +239 -0
- package/extensions/review/git.ts +517 -0
- package/extensions/review/index.ts +232 -0
- package/extensions/review/message-queue.ts +249 -0
- package/extensions/review/models.ts +589 -0
- package/extensions/review/prompts.ts +337 -0
- package/extensions/review/renderers/inline.ts +126 -0
- package/extensions/review/request.ts +504 -0
- package/extensions/review/review.ts +1305 -0
- package/extensions/review/runner.ts +522 -0
- package/extensions/review/runtime.ts +275 -0
- package/extensions/review/schema.ts +160 -0
- package/extensions/review/submit-review-tool.ts +62 -0
- package/extensions/review/submit-triage-tool.ts +60 -0
- package/extensions/review/triage.ts +746 -0
- package/extensions/sandbox/index.ts +976 -185
- package/extensions/sandbox/macos-sandbox-shell.mjs +202 -0
- package/extensions/{interlude.ts → stash.ts} +13 -11
- package/extensions/tool-display-mode.ts +2 -3
- package/extensions/usage/anthropic.ts +5 -6
- package/extensions/usage/github-copilot.ts +30 -9
- package/extensions/usage/index.ts +9 -5
- package/extensions/usage/openai-codex.ts +2 -2
- package/extensions/websearch/README.md +1 -1
- package/extensions/websearch/browser/chromium.ts +42 -31
- package/extensions/websearch/browser/discovery.ts +11 -9
- package/extensions/websearch/browser/firefox.ts +10 -7
- package/extensions/websearch/browser/fs.ts +10 -0
- package/extensions/websearch/browser/sqlite.ts +22 -15
- package/extensions/websearch/config.ts +2 -2
- package/extensions/websearch/index.ts +6 -6
- package/extensions/websearch/providers/pi-model.shared.ts +19 -1
- package/extensions/worktree.ts +33 -33
- package/package.json +4 -4
- package/skills/git-clean-history/SKILL.md +1 -0
- package/skills/git-commit/SKILL.md +2 -2
- package/skills/oracle/SKILL.md +3 -3
- package/skills/oracle/scripts/oracle +82 -61
- package/themes/tau-dark.json +2 -1
- package/extensions/review.ts +0 -4420
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { SUBMIT_TOOL_RETRY_PROMPT } from "./prompts.js";
|
|
7
|
+
|
|
8
|
+
export const REVIEW_INSPECTION_TOOLS = "read,bash,grep,find,ls";
|
|
9
|
+
|
|
10
|
+
export const REVIEW_TASK_TIMEOUT_MS = 30 * 60 * 1000;
|
|
11
|
+
const REVIEW_OUTPUT_EXCERPT_MAX_LENGTH = 240;
|
|
12
|
+
export const REVIEW_STARTUP_RETRY_DELAYS_MS = [500, 1000, 2000, 4000, 8000] as const;
|
|
13
|
+
const REVIEW_STARTUP_RETRY_JITTER_RATIO = 0.2;
|
|
14
|
+
|
|
15
|
+
export type TaskErrorKind =
|
|
16
|
+
| "lock_contention"
|
|
17
|
+
| "missing_api_key"
|
|
18
|
+
| "rate_limit"
|
|
19
|
+
| "unsupported_model"
|
|
20
|
+
| "unsupported_reasoning"
|
|
21
|
+
| "other";
|
|
22
|
+
|
|
23
|
+
export type TaskExecutionControl = {
|
|
24
|
+
isCancelled: () => boolean;
|
|
25
|
+
registerProcess: (proc: ChildProcess) => () => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type PiTaskStatus =
|
|
29
|
+
| "ok"
|
|
30
|
+
| "cancelled"
|
|
31
|
+
| "timeout"
|
|
32
|
+
| "spawn_error"
|
|
33
|
+
| "non_zero_exit"
|
|
34
|
+
| "assistant_error";
|
|
35
|
+
|
|
36
|
+
export type PiTaskResult = {
|
|
37
|
+
status: PiTaskStatus;
|
|
38
|
+
assistantOutput: string;
|
|
39
|
+
stderr: string;
|
|
40
|
+
exitCode?: number;
|
|
41
|
+
error?: string;
|
|
42
|
+
submittedPayloads: unknown[];
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type PiTaskOptions = {
|
|
46
|
+
args: string[];
|
|
47
|
+
prompt: string;
|
|
48
|
+
cwd: string;
|
|
49
|
+
timeoutMs: number;
|
|
50
|
+
control?: TaskExecutionControl;
|
|
51
|
+
submitTool?: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type PiSubmitToolTaskOptions = PiTaskOptions & {
|
|
55
|
+
submitTool: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export function withJitter(baseMs: number): number {
|
|
59
|
+
const range = Math.max(0, Math.floor(baseMs * REVIEW_STARTUP_RETRY_JITTER_RATIO));
|
|
60
|
+
if (range === 0) return baseMs;
|
|
61
|
+
const offset = Math.floor(Math.random() * (range * 2 + 1) - range);
|
|
62
|
+
return Math.max(0, baseMs + offset);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function asRecord(value: unknown): Record<string, unknown> | null {
|
|
66
|
+
return value && typeof value === "object" ? (value as Record<string, unknown>) : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function extractTextContent(content: unknown): string {
|
|
70
|
+
if (typeof content === "string") return content;
|
|
71
|
+
if (!Array.isArray(content)) return "";
|
|
72
|
+
return content
|
|
73
|
+
.map((item) => {
|
|
74
|
+
if (typeof item !== "object" || item === null) return "";
|
|
75
|
+
if (!("type" in item) || item.type !== "text") return "";
|
|
76
|
+
if (!("text" in item) || typeof item.text !== "string") return "";
|
|
77
|
+
return item.text;
|
|
78
|
+
})
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.join("\n");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function extractAssistantMessageFromEvent(event: unknown): Record<string, unknown> | null {
|
|
84
|
+
const record = asRecord(event);
|
|
85
|
+
if (!record || typeof record.type !== "string") return null;
|
|
86
|
+
|
|
87
|
+
if (record.type === "message_end" || record.type === "turn_end") {
|
|
88
|
+
const message = asRecord(record.message);
|
|
89
|
+
return message?.role === "assistant" ? message : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (record.type !== "agent_end" || !Array.isArray(record.messages)) return null;
|
|
93
|
+
for (let i = record.messages.length - 1; i >= 0; i -= 1) {
|
|
94
|
+
const message = asRecord(record.messages[i]);
|
|
95
|
+
if (message?.role === "assistant") {
|
|
96
|
+
return message;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function extractSubmitToolPayloadFromEvent(event: unknown, submitTool: string): unknown | null {
|
|
103
|
+
const record = asRecord(event);
|
|
104
|
+
if (!record || record.type !== "message_end") return null;
|
|
105
|
+
|
|
106
|
+
const message = asRecord(record.message);
|
|
107
|
+
return message?.role === "toolResult" && message.toolName === submitTool ? message.details : null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function parseJsonFromText(raw: string): unknown {
|
|
111
|
+
const trimmed = raw.trim();
|
|
112
|
+
if (!trimmed) throw new Error("Empty output");
|
|
113
|
+
|
|
114
|
+
const codeFenceMatch = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
115
|
+
const candidate = codeFenceMatch?.[1]?.trim() || trimmed;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
return JSON.parse(candidate);
|
|
119
|
+
} catch {
|
|
120
|
+
const firstBrace = candidate.indexOf("{");
|
|
121
|
+
const lastBrace = candidate.lastIndexOf("}");
|
|
122
|
+
if (firstBrace >= 0 && lastBrace > firstBrace) {
|
|
123
|
+
const sliced = candidate.slice(firstBrace, lastBrace + 1);
|
|
124
|
+
return JSON.parse(sliced);
|
|
125
|
+
}
|
|
126
|
+
throw new Error("Output is not valid JSON");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function getSubmittedPayload(options: {
|
|
131
|
+
submittedPayloads: unknown[];
|
|
132
|
+
assistantOutput: string;
|
|
133
|
+
submitTool: string;
|
|
134
|
+
taskLabel: string;
|
|
135
|
+
}): { ok: true; payload: unknown } | { ok: false; error: string } {
|
|
136
|
+
const { submittedPayloads, assistantOutput, submitTool, taskLabel } = options;
|
|
137
|
+
if (submittedPayloads.length === 1) {
|
|
138
|
+
return { ok: true, payload: submittedPayloads[0] };
|
|
139
|
+
}
|
|
140
|
+
if (submittedPayloads.length > 1) {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
error: `${taskLabel} called ${submitTool} ${submittedPayloads.length} times.`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
error: buildMissingSubmitPayloadError(taskLabel, submitTool, assistantOutput),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildMissingSubmitPayloadError(
|
|
154
|
+
taskLabel: string,
|
|
155
|
+
submitTool: string,
|
|
156
|
+
assistantOutput: string,
|
|
157
|
+
): string {
|
|
158
|
+
const collapsedOutput = assistantOutput.trim().replace(/\s+/g, " ");
|
|
159
|
+
if (!collapsedOutput) return `${taskLabel} did not call ${submitTool}.`;
|
|
160
|
+
|
|
161
|
+
const outputExcerpt =
|
|
162
|
+
collapsedOutput.length <= REVIEW_OUTPUT_EXCERPT_MAX_LENGTH
|
|
163
|
+
? collapsedOutput
|
|
164
|
+
: `${collapsedOutput.slice(0, REVIEW_OUTPUT_EXCERPT_MAX_LENGTH - 1).trimEnd()}…`;
|
|
165
|
+
|
|
166
|
+
return `${taskLabel} did not call ${submitTool}. Output: ${outputExcerpt}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function extractStructuredErrorRecord(value: unknown, depth = 0): Record<string, unknown> | null {
|
|
170
|
+
if (depth > 4) return null;
|
|
171
|
+
|
|
172
|
+
const record = asRecord(value);
|
|
173
|
+
if (!record) return null;
|
|
174
|
+
|
|
175
|
+
for (const key of ["error", "details", "response", "body"]) {
|
|
176
|
+
const nested = extractStructuredErrorRecord(record[key], depth + 1);
|
|
177
|
+
if (nested) return nested;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return record;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function extractStructuredErrorMessage(value: unknown, depth = 0): string | undefined {
|
|
184
|
+
if (depth > 4) return undefined;
|
|
185
|
+
if (typeof value === "string") {
|
|
186
|
+
const trimmed = value.trim();
|
|
187
|
+
return trimmed || undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (Array.isArray(value)) {
|
|
191
|
+
for (const item of value) {
|
|
192
|
+
const message = extractStructuredErrorMessage(item, depth + 1);
|
|
193
|
+
if (message) return message;
|
|
194
|
+
}
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const record = asRecord(value);
|
|
199
|
+
if (!record) return undefined;
|
|
200
|
+
|
|
201
|
+
for (const key of ["message", "detail", "error_description", "title", "reason"]) {
|
|
202
|
+
const candidate = record[key];
|
|
203
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
204
|
+
return candidate.trim();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const key of ["error", "details", "response", "body"]) {
|
|
209
|
+
const message = extractStructuredErrorMessage(record[key], depth + 1);
|
|
210
|
+
if (message) return message;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function parseStructuredErrorPayload(detail: string | undefined): Record<string, unknown> | null {
|
|
217
|
+
const trimmed = detail?.trim();
|
|
218
|
+
if (!trimmed) return null;
|
|
219
|
+
|
|
220
|
+
const candidates = [trimmed];
|
|
221
|
+
const jsonStart = trimmed.indexOf("{");
|
|
222
|
+
if (jsonStart > 0) {
|
|
223
|
+
candidates.push(trimmed.slice(jsonStart));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
for (const candidate of candidates) {
|
|
227
|
+
try {
|
|
228
|
+
const parsed = parseJsonFromText(candidate);
|
|
229
|
+
const record = extractStructuredErrorRecord(parsed);
|
|
230
|
+
if (record) return record;
|
|
231
|
+
} catch {
|
|
232
|
+
// Ignore parse failures.
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function getStructuredErrorString(
|
|
240
|
+
value: Record<string, unknown> | null,
|
|
241
|
+
key: string,
|
|
242
|
+
): string | undefined {
|
|
243
|
+
const candidate = value?.[key];
|
|
244
|
+
return typeof candidate === "string" && candidate.trim() ? candidate.trim() : undefined;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function summarizeErrorDetail(detail: string | undefined): string | undefined {
|
|
248
|
+
const trimmed = detail?.trim();
|
|
249
|
+
if (!trimmed) return undefined;
|
|
250
|
+
|
|
251
|
+
const payload = parseStructuredErrorPayload(trimmed);
|
|
252
|
+
if (payload) {
|
|
253
|
+
return extractStructuredErrorMessage(payload) ?? trimmed;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const parsed = parseJsonFromText(trimmed);
|
|
258
|
+
return extractStructuredErrorMessage(parsed) ?? trimmed;
|
|
259
|
+
} catch {
|
|
260
|
+
return trimmed;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function appendErrorDetails(message: string, detail: string | undefined): string {
|
|
265
|
+
const summarized = summarizeErrorDetail(detail);
|
|
266
|
+
if (!summarized || summarized === message) return message;
|
|
267
|
+
return `${message} Details: ${summarized}`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function buildProviderErrorMessage(message: string, detail: string | undefined): string {
|
|
271
|
+
const summarized = summarizeErrorDetail(detail);
|
|
272
|
+
if (!summarized || summarized === message) return message;
|
|
273
|
+
if (summarized !== detail?.trim()) return summarized;
|
|
274
|
+
return `${message} Details: ${summarized}`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function classifyTaskError(errorText: string): {
|
|
278
|
+
errorKind: TaskErrorKind;
|
|
279
|
+
missingApiProvider?: string;
|
|
280
|
+
} {
|
|
281
|
+
if (/Lock file is already being held/i.test(errorText)) {
|
|
282
|
+
return { errorKind: "lock_contention" };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const apiKeyMatch = errorText.match(/No API key found for\s+([\w.-]+)/i);
|
|
286
|
+
if (apiKeyMatch?.[1]) {
|
|
287
|
+
return { errorKind: "missing_api_key", missingApiProvider: apiKeyMatch[1] };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const authFailedMatch = errorText.match(/Authentication failed for\s+"([\w.-]+)"/i);
|
|
291
|
+
if (authFailedMatch?.[1]) {
|
|
292
|
+
return { errorKind: "missing_api_key", missingApiProvider: authFailedMatch[1] };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const payload = parseStructuredErrorPayload(errorText);
|
|
296
|
+
const structuredMessage = getStructuredErrorString(payload, "message")?.toLowerCase();
|
|
297
|
+
const structuredCode = getStructuredErrorString(payload, "code")?.toLowerCase();
|
|
298
|
+
const structuredParam = getStructuredErrorString(payload, "param")?.toLowerCase();
|
|
299
|
+
|
|
300
|
+
if (structuredCode === "model_not_supported" || structuredParam === "model") {
|
|
301
|
+
return { errorKind: "unsupported_model" };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (
|
|
305
|
+
structuredParam?.includes("reasoning") ||
|
|
306
|
+
structuredParam?.includes("thinking") ||
|
|
307
|
+
structuredCode?.includes("reasoning") ||
|
|
308
|
+
structuredCode?.includes("thinking") ||
|
|
309
|
+
structuredMessage?.includes("reasoning") ||
|
|
310
|
+
structuredMessage?.includes("thinking")
|
|
311
|
+
) {
|
|
312
|
+
return { errorKind: "unsupported_reasoning" };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (/rate.?limit|too many requests|\b429\b/i.test(errorText)) {
|
|
316
|
+
return { errorKind: "rate_limit" };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const summarized = summarizeErrorDetail(errorText)?.toLowerCase();
|
|
320
|
+
if (summarized?.includes("service_tier") && summarized.includes("not supported")) {
|
|
321
|
+
return { errorKind: "unsupported_model" };
|
|
322
|
+
}
|
|
323
|
+
if (summarized?.includes("requested model is not supported")) {
|
|
324
|
+
return { errorKind: "unsupported_model" };
|
|
325
|
+
}
|
|
326
|
+
if (
|
|
327
|
+
summarized &&
|
|
328
|
+
(summarized.includes("reasoning") || summarized.includes("thinking")) &&
|
|
329
|
+
summarized.includes("not supported")
|
|
330
|
+
) {
|
|
331
|
+
return { errorKind: "unsupported_reasoning" };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return { errorKind: "other" };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export async function runPiSubmitToolTask(options: PiSubmitToolTaskOptions): Promise<PiTaskResult> {
|
|
338
|
+
const sessionDir = await fs.mkdtemp(path.join(os.tmpdir(), "tau-review-"));
|
|
339
|
+
const args = [
|
|
340
|
+
...options.args.filter((arg) => arg !== "--no-session"),
|
|
341
|
+
"--session",
|
|
342
|
+
path.join(sessionDir, "session.jsonl"),
|
|
343
|
+
];
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
const firstResult = await runPiOneShotTask({ ...options, args });
|
|
347
|
+
if (
|
|
348
|
+
firstResult.status !== "ok" ||
|
|
349
|
+
firstResult.submittedPayloads.length > 0 ||
|
|
350
|
+
options.control?.isCancelled()
|
|
351
|
+
) {
|
|
352
|
+
return firstResult;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return runPiOneShotTask({
|
|
356
|
+
...options,
|
|
357
|
+
args,
|
|
358
|
+
prompt: SUBMIT_TOOL_RETRY_PROMPT.replaceAll("{SUBMIT_TOOL}", options.submitTool),
|
|
359
|
+
});
|
|
360
|
+
} finally {
|
|
361
|
+
await fs.rm(sessionDir, { recursive: true, force: true });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export async function runPiOneShotTask({
|
|
366
|
+
args,
|
|
367
|
+
prompt,
|
|
368
|
+
cwd,
|
|
369
|
+
timeoutMs,
|
|
370
|
+
control,
|
|
371
|
+
submitTool,
|
|
372
|
+
}: PiTaskOptions): Promise<PiTaskResult> {
|
|
373
|
+
if (control?.isCancelled()) {
|
|
374
|
+
return {
|
|
375
|
+
status: "cancelled",
|
|
376
|
+
assistantOutput: "",
|
|
377
|
+
stderr: "",
|
|
378
|
+
submittedPayloads: [],
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return new Promise<PiTaskResult>((resolve) => {
|
|
383
|
+
const proc = spawn("pi", [...args, prompt], {
|
|
384
|
+
cwd,
|
|
385
|
+
shell: false,
|
|
386
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
387
|
+
env: process.env,
|
|
388
|
+
});
|
|
389
|
+
const unregisterProcess = control?.registerProcess(proc);
|
|
390
|
+
|
|
391
|
+
let stdoutBuffer = "";
|
|
392
|
+
let latestAssistantOutput = "";
|
|
393
|
+
let latestAssistantError = "";
|
|
394
|
+
const submittedPayloads: unknown[] = [];
|
|
395
|
+
let stderr = "";
|
|
396
|
+
let settled = false;
|
|
397
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
398
|
+
|
|
399
|
+
const finish = (result: Omit<PiTaskResult, "submittedPayloads">) => {
|
|
400
|
+
if (settled) return;
|
|
401
|
+
settled = true;
|
|
402
|
+
if (timeoutId) {
|
|
403
|
+
clearTimeout(timeoutId);
|
|
404
|
+
timeoutId = undefined;
|
|
405
|
+
}
|
|
406
|
+
unregisterProcess?.();
|
|
407
|
+
resolve({
|
|
408
|
+
...result,
|
|
409
|
+
submittedPayloads,
|
|
410
|
+
});
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const processLine = (line: string) => {
|
|
414
|
+
const trimmed = line.trim();
|
|
415
|
+
if (!trimmed) return;
|
|
416
|
+
try {
|
|
417
|
+
const event = JSON.parse(trimmed);
|
|
418
|
+
const submittedPayload = submitTool
|
|
419
|
+
? extractSubmitToolPayloadFromEvent(event, submitTool)
|
|
420
|
+
: null;
|
|
421
|
+
if (submittedPayload !== null) {
|
|
422
|
+
submittedPayloads.push(submittedPayload);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const message = extractAssistantMessageFromEvent(event);
|
|
426
|
+
if (message) {
|
|
427
|
+
const text = extractTextContent(message.content);
|
|
428
|
+
if (text) latestAssistantOutput = text;
|
|
429
|
+
latestAssistantError =
|
|
430
|
+
message.stopReason === "error" && typeof message.errorMessage === "string"
|
|
431
|
+
? message.errorMessage
|
|
432
|
+
: "";
|
|
433
|
+
}
|
|
434
|
+
} catch {
|
|
435
|
+
// Ignore non-JSON lines.
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
if (control?.isCancelled()) {
|
|
440
|
+
try {
|
|
441
|
+
proc.kill("SIGKILL");
|
|
442
|
+
} catch {
|
|
443
|
+
// Best effort.
|
|
444
|
+
}
|
|
445
|
+
finish({
|
|
446
|
+
status: "cancelled",
|
|
447
|
+
assistantOutput: latestAssistantOutput,
|
|
448
|
+
stderr,
|
|
449
|
+
});
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
timeoutId = setTimeout(() => {
|
|
454
|
+
try {
|
|
455
|
+
proc.kill("SIGKILL");
|
|
456
|
+
} catch {
|
|
457
|
+
// Best effort.
|
|
458
|
+
}
|
|
459
|
+
finish({
|
|
460
|
+
status: "timeout",
|
|
461
|
+
assistantOutput: latestAssistantOutput,
|
|
462
|
+
stderr,
|
|
463
|
+
});
|
|
464
|
+
}, timeoutMs);
|
|
465
|
+
|
|
466
|
+
proc.stdout.on("data", (chunk) => {
|
|
467
|
+
stdoutBuffer += chunk.toString();
|
|
468
|
+
const lines = stdoutBuffer.split("\n");
|
|
469
|
+
stdoutBuffer = lines.pop() ?? "";
|
|
470
|
+
for (const line of lines) {
|
|
471
|
+
processLine(line);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
proc.stderr.on("data", (chunk) => {
|
|
476
|
+
stderr += chunk.toString();
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
proc.on("error", (error) => {
|
|
480
|
+
finish({
|
|
481
|
+
status: "spawn_error",
|
|
482
|
+
assistantOutput: latestAssistantOutput,
|
|
483
|
+
stderr,
|
|
484
|
+
error: error.message,
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
proc.on("close", (code) => {
|
|
489
|
+
if (stdoutBuffer.trim()) {
|
|
490
|
+
processLine(stdoutBuffer);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if ((code ?? 1) !== 0) {
|
|
494
|
+
finish({
|
|
495
|
+
status: "non_zero_exit",
|
|
496
|
+
assistantOutput: latestAssistantOutput,
|
|
497
|
+
stderr,
|
|
498
|
+
exitCode: code ?? 1,
|
|
499
|
+
});
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (latestAssistantError) {
|
|
504
|
+
finish({
|
|
505
|
+
status: "assistant_error",
|
|
506
|
+
assistantOutput: latestAssistantOutput,
|
|
507
|
+
stderr,
|
|
508
|
+
error: latestAssistantError,
|
|
509
|
+
exitCode: 0,
|
|
510
|
+
});
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
finish({
|
|
515
|
+
status: "ok",
|
|
516
|
+
assistantOutput: latestAssistantOutput,
|
|
517
|
+
stderr,
|
|
518
|
+
exitCode: 0,
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
}
|