@wichayutdew/pi-workflows 0.2.2 → 0.3.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/LICENSE +21 -201
- package/README.md +207 -106
- package/agents/step.md +15 -1
- package/dist/index.js +1964 -463
- package/examples/mr-comments.workflow.yaml +4 -4
- package/examples/prompts/mr-comments/implement.md +10 -5
- package/examples/prompts/mr-comments/plan.md +10 -5
- package/examples/prompts/mr-comments/verify.md +5 -4
- package/examples/settings.yaml +3 -1
- package/package.json +10 -6
- package/schemas/settings.schema.json +8 -0
- package/schemas/workflow.schema.json +10 -2
- package/src/command-names.ts +0 -1
- package/src/commands.ts +0 -6
- package/src/config/ceiling.ts +8 -0
- package/src/config/load.ts +3 -9
- package/src/config/types.ts +15 -3
- package/src/config/validate.ts +147 -22
- package/src/engine/state.ts +7 -0
- package/src/engine/transitions.ts +52 -7
- package/src/harness.ts +804 -74
- package/src/index.ts +6 -2
- package/src/integrations/prompt-gate.ts +14 -13
- package/src/integrations/subagents/child-runtime.ts +187 -69
- package/src/integrations/subagents/client.ts +4 -3
- package/src/integrations/subagents/diagnostics.ts +977 -0
- package/src/integrations/subagents/protocol.ts +86 -15
- package/src/policy/approved-commands.ts +212 -5
- package/src/policy/bash.ts +0 -9
- package/src/prompt.ts +115 -7
- package/src/runtime/serial-task-queue.ts +5 -1
- package/src/workflow-status.ts +244 -35
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { lstat, open, realpath } from 'node:fs/promises';
|
|
3
|
+
import {
|
|
4
|
+
basename,
|
|
5
|
+
dirname,
|
|
6
|
+
isAbsolute,
|
|
7
|
+
join,
|
|
8
|
+
relative,
|
|
9
|
+
resolve,
|
|
10
|
+
sep,
|
|
11
|
+
} from 'node:path';
|
|
12
|
+
import type { BashPermission } from '../../config/types.ts';
|
|
13
|
+
import { authorizeBash } from '../../policy/bash.ts';
|
|
14
|
+
|
|
15
|
+
const SESSION_FILE_NAME = 'session.jsonl';
|
|
16
|
+
const SESSION_RUN_DIRECTORY = /^run-\d+$/;
|
|
17
|
+
const SESSION_FILE_SUFFIX = '.jsonl';
|
|
18
|
+
const MAX_SESSION_TAIL_BYTES = 1024 * 1024;
|
|
19
|
+
const MAX_DIAGNOSTIC_FIELD_CHARS = 1_600;
|
|
20
|
+
const TRUNCATION_MARKER = '… [truncated] …';
|
|
21
|
+
const REPLAY_SAFE_TOOLS = new Set([
|
|
22
|
+
'find',
|
|
23
|
+
'grep',
|
|
24
|
+
'ls',
|
|
25
|
+
'read',
|
|
26
|
+
'structured_output',
|
|
27
|
+
]);
|
|
28
|
+
const PRE_EXECUTION_BASH_FAILURES = [
|
|
29
|
+
'command does not match this step',
|
|
30
|
+
'environment assignments are not allowed',
|
|
31
|
+
'not enabled by subagent',
|
|
32
|
+
'shell operators, substitutions, expansions, and comments are not allowed',
|
|
33
|
+
'shell wrapper',
|
|
34
|
+
'substitutions and escapes are not allowed inside double quotes',
|
|
35
|
+
'trailing bash escape is not allowed',
|
|
36
|
+
'unterminated bash quote',
|
|
37
|
+
'unquoted pathname and tilde expansion are not allowed',
|
|
38
|
+
] as const;
|
|
39
|
+
const HIDDEN_BASH_FATAL_PATTERNS = [
|
|
40
|
+
/command not found/i,
|
|
41
|
+
/permission denied/i,
|
|
42
|
+
/no such file or directory/i,
|
|
43
|
+
/segmentation fault/i,
|
|
44
|
+
/killed|terminated/i,
|
|
45
|
+
/out of memory/i,
|
|
46
|
+
/connection refused/i,
|
|
47
|
+
/timeout/i,
|
|
48
|
+
] as const;
|
|
49
|
+
const HIDDEN_BASH_EXIT_PATTERN =
|
|
50
|
+
/exit(?:ed)?\s*(?:with\s*)?(?:code|status)?\s*[:\s]?\s*(\d+)/i;
|
|
51
|
+
|
|
52
|
+
export interface ToolFailureDiagnostic {
|
|
53
|
+
tool: string;
|
|
54
|
+
call?: string;
|
|
55
|
+
output?: string;
|
|
56
|
+
replaySafe?: true;
|
|
57
|
+
completionAfterFailure?: true;
|
|
58
|
+
completionValue?: Record<string, unknown>;
|
|
59
|
+
transcriptToolCount?: number;
|
|
60
|
+
transcriptTurnCount?: number;
|
|
61
|
+
correlation?:
|
|
62
|
+
'latest-before-completion' | 'successful-output-before-completion';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface DelegationReplayAudit {
|
|
66
|
+
replaySafe: boolean;
|
|
67
|
+
toolCount: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface DelegationReplayExpectation {
|
|
71
|
+
task: string;
|
|
72
|
+
bashPermission: BashPermission;
|
|
73
|
+
approvedBashCommands: readonly string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface SubagentSessionIdentity {
|
|
77
|
+
runId: string;
|
|
78
|
+
childIndex: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface RecordedToolCall {
|
|
82
|
+
id: string;
|
|
83
|
+
order: number;
|
|
84
|
+
tool: string;
|
|
85
|
+
call?: string;
|
|
86
|
+
completionValue?: Record<string, unknown>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface RecordedToolFailure extends ToolFailureDiagnostic {
|
|
90
|
+
callId?: string;
|
|
91
|
+
order: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface RecordedToolSuccess {
|
|
95
|
+
order: number;
|
|
96
|
+
tool: string;
|
|
97
|
+
call?: string;
|
|
98
|
+
output?: string;
|
|
99
|
+
detectorOutput?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface RecordedCompletion {
|
|
103
|
+
order: number;
|
|
104
|
+
value: Record<string, unknown>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface RecordedMessage {
|
|
108
|
+
order: number;
|
|
109
|
+
value: Record<string, unknown>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface SessionTail {
|
|
113
|
+
content: string;
|
|
114
|
+
truncated: boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
118
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function bounded(value: string): string {
|
|
122
|
+
if (value.length <= MAX_DIAGNOSTIC_FIELD_CHARS) return value;
|
|
123
|
+
const available = MAX_DIAGNOSTIC_FIELD_CHARS - TRUNCATION_MARKER.length - 2;
|
|
124
|
+
const startLength = Math.ceil(available / 2);
|
|
125
|
+
const endLength = Math.floor(available / 2);
|
|
126
|
+
return `${value.slice(0, startLength)}\n${TRUNCATION_MARKER}\n${value.slice(-endLength)}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function textContent(value: unknown): string | undefined {
|
|
130
|
+
if (!Array.isArray(value)) return undefined;
|
|
131
|
+
const text = value
|
|
132
|
+
.flatMap((item) =>
|
|
133
|
+
isRecord(item) && item.type === 'text' && typeof item.text === 'string'
|
|
134
|
+
? [item.text]
|
|
135
|
+
: [],
|
|
136
|
+
)
|
|
137
|
+
.join('\n')
|
|
138
|
+
.trim();
|
|
139
|
+
return text ? bounded(text) : undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function firstTextContent(value: unknown): string | undefined {
|
|
143
|
+
if (!Array.isArray(value)) return undefined;
|
|
144
|
+
const text = value.find(
|
|
145
|
+
(item) =>
|
|
146
|
+
isRecord(item) && item.type === 'text' && typeof item.text === 'string',
|
|
147
|
+
);
|
|
148
|
+
return isRecord(text) && typeof text.text === 'string'
|
|
149
|
+
? text.text
|
|
150
|
+
: undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function toolCallText(
|
|
154
|
+
tool: string,
|
|
155
|
+
argumentsValue: unknown,
|
|
156
|
+
): string | undefined {
|
|
157
|
+
if (!isRecord(argumentsValue)) return undefined;
|
|
158
|
+
if (tool === 'bash') {
|
|
159
|
+
const command = argumentsValue.command ?? argumentsValue.cmd;
|
|
160
|
+
if (typeof command === 'string' && command.trim()) {
|
|
161
|
+
return bounded(command.trim());
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return bounded(JSON.stringify(argumentsValue));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function failedToolName(error: string | undefined): string | undefined {
|
|
168
|
+
return error?.match(/\b([a-z][\w-]*) failed(?:\s*\(|:)/i)?.[1];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function initialDelegationTask(transcript: string): string | undefined {
|
|
172
|
+
for (const line of transcript.split('\n')) {
|
|
173
|
+
if (!line.trim()) continue;
|
|
174
|
+
let entry: unknown;
|
|
175
|
+
try {
|
|
176
|
+
entry = JSON.parse(line);
|
|
177
|
+
} catch {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (!isRecord(entry) || entry.type !== 'message') continue;
|
|
181
|
+
const message = entry.message;
|
|
182
|
+
if (!isRecord(message) || message.role !== 'user') continue;
|
|
183
|
+
if (!Array.isArray(message.content)) return undefined;
|
|
184
|
+
const textParts = message.content.flatMap((item) =>
|
|
185
|
+
isRecord(item) && item.type === 'text' && typeof item.text === 'string'
|
|
186
|
+
? [item.text]
|
|
187
|
+
: [],
|
|
188
|
+
);
|
|
189
|
+
if (textParts.length !== 1) return undefined;
|
|
190
|
+
const text = textParts[0];
|
|
191
|
+
if (text === undefined) return undefined;
|
|
192
|
+
return text;
|
|
193
|
+
}
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function transcriptMatchesDelegation(
|
|
198
|
+
transcript: string,
|
|
199
|
+
expectedTask: string,
|
|
200
|
+
): boolean {
|
|
201
|
+
return initialDelegationTask(transcript) === expectedTask;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function parseDelegationReplayAudit(
|
|
205
|
+
transcript: string,
|
|
206
|
+
expectation: DelegationReplayExpectation,
|
|
207
|
+
completeTranscript = true,
|
|
208
|
+
): DelegationReplayAudit {
|
|
209
|
+
const calls = new Map<string, RecordedToolCall>();
|
|
210
|
+
const recordedCalls: RecordedToolCall[] = [];
|
|
211
|
+
const diagnostics: RecordedToolFailure[] = [];
|
|
212
|
+
const resultCallIds = new Set<string>();
|
|
213
|
+
let structurallyValid = true;
|
|
214
|
+
let order = 0;
|
|
215
|
+
|
|
216
|
+
for (const line of transcript.split('\n')) {
|
|
217
|
+
order += 1;
|
|
218
|
+
if (!line.trim()) continue;
|
|
219
|
+
let entry: unknown;
|
|
220
|
+
try {
|
|
221
|
+
entry = JSON.parse(line);
|
|
222
|
+
} catch {
|
|
223
|
+
structurallyValid = false;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (!isRecord(entry) || entry.type !== 'message') continue;
|
|
227
|
+
const message = entry.message;
|
|
228
|
+
if (!isRecord(message)) {
|
|
229
|
+
structurallyValid = false;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (message.role === 'assistant') {
|
|
234
|
+
if (!Array.isArray(message.content)) {
|
|
235
|
+
structurallyValid = false;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
for (const item of message.content) {
|
|
239
|
+
if (!isRecord(item) || item.type !== 'toolCall') continue;
|
|
240
|
+
if (
|
|
241
|
+
typeof item.id !== 'string' ||
|
|
242
|
+
typeof item.name !== 'string' ||
|
|
243
|
+
calls.has(item.id)
|
|
244
|
+
) {
|
|
245
|
+
structurallyValid = false;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const call = toolCallText(item.name, item.arguments);
|
|
249
|
+
const recordedCall: RecordedToolCall = {
|
|
250
|
+
id: item.id,
|
|
251
|
+
order,
|
|
252
|
+
tool: item.name,
|
|
253
|
+
...(call ? { call } : {}),
|
|
254
|
+
};
|
|
255
|
+
calls.set(item.id, recordedCall);
|
|
256
|
+
recordedCalls.push(recordedCall);
|
|
257
|
+
}
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (message.role !== 'toolResult') continue;
|
|
262
|
+
if (
|
|
263
|
+
typeof message.toolCallId !== 'string' ||
|
|
264
|
+
typeof message.toolName !== 'string' ||
|
|
265
|
+
typeof message.isError !== 'boolean' ||
|
|
266
|
+
!Array.isArray(message.content) ||
|
|
267
|
+
resultCallIds.has(message.toolCallId)
|
|
268
|
+
) {
|
|
269
|
+
structurallyValid = false;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
resultCallIds.add(message.toolCallId);
|
|
273
|
+
const recorded = calls.get(message.toolCallId);
|
|
274
|
+
if (recorded?.tool !== message.toolName) {
|
|
275
|
+
structurallyValid = false;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (message.isError) {
|
|
279
|
+
const output = textContent(message.content);
|
|
280
|
+
diagnostics.push({
|
|
281
|
+
tool: message.toolName,
|
|
282
|
+
callId: message.toolCallId,
|
|
283
|
+
order,
|
|
284
|
+
...(recorded.call ? { call: recorded.call } : {}),
|
|
285
|
+
...(output ? { output } : {}),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
replaySafe:
|
|
292
|
+
completeTranscript &&
|
|
293
|
+
structurallyValid &&
|
|
294
|
+
transcriptMatchesDelegation(transcript, expectation.task) &&
|
|
295
|
+
recordedCalls.every((call) =>
|
|
296
|
+
replaySafeToolCall(
|
|
297
|
+
call,
|
|
298
|
+
diagnostics,
|
|
299
|
+
expectation.bashPermission,
|
|
300
|
+
expectation.approvedBashCommands,
|
|
301
|
+
),
|
|
302
|
+
),
|
|
303
|
+
toolCount: recordedCalls.length,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function parseToolFailureDiagnostic(
|
|
308
|
+
transcript: string,
|
|
309
|
+
expectedTool?: string,
|
|
310
|
+
terminalError?: string,
|
|
311
|
+
allowCompletionProof = true,
|
|
312
|
+
): ToolFailureDiagnostic | undefined {
|
|
313
|
+
const calls = new Map<string, RecordedToolCall>();
|
|
314
|
+
const recordedCalls: RecordedToolCall[] = [];
|
|
315
|
+
const diagnostics: RecordedToolFailure[] = [];
|
|
316
|
+
const successfulResults: RecordedToolSuccess[] = [];
|
|
317
|
+
const successfulCompletions: RecordedCompletion[] = [];
|
|
318
|
+
const recordedMessages: RecordedMessage[] = [];
|
|
319
|
+
const resultCallIds = new Set<string>();
|
|
320
|
+
let falsePositiveProofValid = true;
|
|
321
|
+
let lastInteractionOrder = 0;
|
|
322
|
+
let order = 0;
|
|
323
|
+
|
|
324
|
+
for (const line of transcript.split('\n')) {
|
|
325
|
+
order += 1;
|
|
326
|
+
if (!line.trim()) continue;
|
|
327
|
+
let entry: unknown;
|
|
328
|
+
try {
|
|
329
|
+
entry = JSON.parse(line);
|
|
330
|
+
} catch {
|
|
331
|
+
falsePositiveProofValid = false;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (!isRecord(entry) || entry.type !== 'message') continue;
|
|
335
|
+
const message = entry.message;
|
|
336
|
+
if (!isRecord(message)) {
|
|
337
|
+
falsePositiveProofValid = false;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
recordedMessages.push({ order, value: message });
|
|
341
|
+
if (
|
|
342
|
+
message.role === 'assistant' &&
|
|
343
|
+
((typeof message.errorMessage === 'string' &&
|
|
344
|
+
message.errorMessage.trim().length > 0) ||
|
|
345
|
+
message.stopReason === 'error' ||
|
|
346
|
+
message.stopReason === 'aborted')
|
|
347
|
+
) {
|
|
348
|
+
falsePositiveProofValid = false;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (message.role === 'assistant') {
|
|
352
|
+
if (!Array.isArray(message.content)) {
|
|
353
|
+
falsePositiveProofValid = false;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
lastInteractionOrder = order;
|
|
357
|
+
const toolCalls = message.content.filter(
|
|
358
|
+
(item): item is Record<string, unknown> =>
|
|
359
|
+
isRecord(item) &&
|
|
360
|
+
item.type === 'toolCall' &&
|
|
361
|
+
typeof item.id === 'string' &&
|
|
362
|
+
typeof item.name === 'string',
|
|
363
|
+
);
|
|
364
|
+
for (const item of message.content) {
|
|
365
|
+
if (
|
|
366
|
+
!isRecord(item) ||
|
|
367
|
+
item.type !== 'toolCall' ||
|
|
368
|
+
typeof item.id !== 'string' ||
|
|
369
|
+
typeof item.name !== 'string'
|
|
370
|
+
) {
|
|
371
|
+
if (isRecord(item) && item.type === 'toolCall') {
|
|
372
|
+
falsePositiveProofValid = false;
|
|
373
|
+
}
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (calls.has(item.id)) falsePositiveProofValid = false;
|
|
377
|
+
const call = toolCallText(item.name, item.arguments);
|
|
378
|
+
const completionIsExclusive =
|
|
379
|
+
toolCalls.length === 1 &&
|
|
380
|
+
message.content.every(
|
|
381
|
+
(contentItem) =>
|
|
382
|
+
isRecord(contentItem) &&
|
|
383
|
+
(contentItem.type === 'thinking' ||
|
|
384
|
+
contentItem.type === 'toolCall'),
|
|
385
|
+
);
|
|
386
|
+
const completionValue =
|
|
387
|
+
item.name === 'structured_output' && completionIsExclusive
|
|
388
|
+
? structuredCompletionValue(item.arguments)
|
|
389
|
+
: undefined;
|
|
390
|
+
const recordedCall: RecordedToolCall = {
|
|
391
|
+
id: item.id,
|
|
392
|
+
order,
|
|
393
|
+
tool: item.name,
|
|
394
|
+
...(call ? { call } : {}),
|
|
395
|
+
...(completionValue ? { completionValue } : {}),
|
|
396
|
+
};
|
|
397
|
+
calls.set(item.id, recordedCall);
|
|
398
|
+
recordedCalls.push(recordedCall);
|
|
399
|
+
}
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (message.role !== 'toolResult' || typeof message.toolName !== 'string') {
|
|
404
|
+
if (message.role === 'toolResult') falsePositiveProofValid = false;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
lastInteractionOrder = order;
|
|
408
|
+
if (
|
|
409
|
+
typeof message.toolCallId !== 'string' ||
|
|
410
|
+
typeof message.isError !== 'boolean' ||
|
|
411
|
+
!Array.isArray(message.content) ||
|
|
412
|
+
resultCallIds.has(message.toolCallId)
|
|
413
|
+
) {
|
|
414
|
+
falsePositiveProofValid = false;
|
|
415
|
+
}
|
|
416
|
+
if (typeof message.toolCallId === 'string') {
|
|
417
|
+
resultCallIds.add(message.toolCallId);
|
|
418
|
+
}
|
|
419
|
+
const recorded =
|
|
420
|
+
typeof message.toolCallId === 'string'
|
|
421
|
+
? calls.get(message.toolCallId)
|
|
422
|
+
: undefined;
|
|
423
|
+
const callMatchesResult = recorded?.tool === message.toolName;
|
|
424
|
+
if (!callMatchesResult) falsePositiveProofValid = false;
|
|
425
|
+
if (
|
|
426
|
+
message.toolName === 'structured_output' &&
|
|
427
|
+
message.isError === false &&
|
|
428
|
+
callMatchesResult &&
|
|
429
|
+
recorded?.completionValue
|
|
430
|
+
) {
|
|
431
|
+
successfulCompletions.push({
|
|
432
|
+
order,
|
|
433
|
+
value: recorded.completionValue,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
if (
|
|
437
|
+
message.isError === false &&
|
|
438
|
+
message.toolName !== 'structured_output' &&
|
|
439
|
+
callMatchesResult
|
|
440
|
+
) {
|
|
441
|
+
const output = textContent(message.content);
|
|
442
|
+
const detectorOutput = firstTextContent(message.content);
|
|
443
|
+
successfulResults.push({
|
|
444
|
+
order,
|
|
445
|
+
tool: message.toolName,
|
|
446
|
+
...(recorded.call ? { call: recorded.call } : {}),
|
|
447
|
+
...(output ? { output } : {}),
|
|
448
|
+
...(detectorOutput !== undefined ? { detectorOutput } : {}),
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
if (message.isError !== true) continue;
|
|
452
|
+
const output = textContent(message.content);
|
|
453
|
+
const diagnostic: ToolFailureDiagnostic = {
|
|
454
|
+
tool: message.toolName,
|
|
455
|
+
...(callMatchesResult && recorded.call ? { call: recorded.call } : {}),
|
|
456
|
+
...(output ? { output } : {}),
|
|
457
|
+
};
|
|
458
|
+
diagnostics.push({
|
|
459
|
+
...diagnostic,
|
|
460
|
+
...(callMatchesResult && typeof message.toolCallId === 'string'
|
|
461
|
+
? { callId: message.toolCallId }
|
|
462
|
+
: {}),
|
|
463
|
+
order,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const matchingTool = (diagnostic: { tool: string }): boolean =>
|
|
468
|
+
expectedTool === undefined ||
|
|
469
|
+
diagnostic.tool.toLowerCase() === expectedTool.toLowerCase();
|
|
470
|
+
let selected: RecordedToolFailure | undefined;
|
|
471
|
+
if (terminalError) {
|
|
472
|
+
selected = latestMatching(
|
|
473
|
+
diagnostics,
|
|
474
|
+
(diagnostic) =>
|
|
475
|
+
matchingTool(diagnostic) &&
|
|
476
|
+
diagnosticMatchesTerminalError(diagnostic, terminalError),
|
|
477
|
+
);
|
|
478
|
+
if (!selected) {
|
|
479
|
+
const fallback = latestMatching(diagnostics, matchingTool);
|
|
480
|
+
const latestFailureOrder = diagnostics.at(-1)?.order;
|
|
481
|
+
if (
|
|
482
|
+
fallback &&
|
|
483
|
+
latestFailureOrder !== undefined &&
|
|
484
|
+
finalCompletion(
|
|
485
|
+
successfulCompletions,
|
|
486
|
+
latestFailureOrder,
|
|
487
|
+
lastInteractionOrder,
|
|
488
|
+
allowCompletionProof,
|
|
489
|
+
)
|
|
490
|
+
) {
|
|
491
|
+
return publicDiagnostic(
|
|
492
|
+
fallback,
|
|
493
|
+
diagnostics,
|
|
494
|
+
recordedCalls,
|
|
495
|
+
successfulCompletions,
|
|
496
|
+
lastInteractionOrder,
|
|
497
|
+
allowCompletionProof,
|
|
498
|
+
'latest-before-completion',
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (!selected && diagnostics.length === 0) {
|
|
503
|
+
const falsePositive = reproduceHiddenBashFalsePositive(
|
|
504
|
+
recordedMessages,
|
|
505
|
+
successfulResults,
|
|
506
|
+
);
|
|
507
|
+
const completion = falsePositive
|
|
508
|
+
? finalCompletion(
|
|
509
|
+
successfulCompletions,
|
|
510
|
+
falsePositive.result.order,
|
|
511
|
+
lastInteractionOrder,
|
|
512
|
+
allowCompletionProof,
|
|
513
|
+
)
|
|
514
|
+
: undefined;
|
|
515
|
+
if (
|
|
516
|
+
falsePositiveProofValid &&
|
|
517
|
+
recordedCalls.length === resultCallIds.size &&
|
|
518
|
+
recordedCalls.every((call) => resultCallIds.has(call.id)) &&
|
|
519
|
+
recordedCalls.filter((call) => call.tool === 'structured_output')
|
|
520
|
+
.length === 1 &&
|
|
521
|
+
expectedTool?.toLowerCase() === 'bash' &&
|
|
522
|
+
falsePositive &&
|
|
523
|
+
terminalError === falsePositive.terminalError &&
|
|
524
|
+
completion
|
|
525
|
+
) {
|
|
526
|
+
const successfulOutput = falsePositive.result;
|
|
527
|
+
return {
|
|
528
|
+
tool: successfulOutput.tool,
|
|
529
|
+
...(successfulOutput.call ? { call: successfulOutput.call } : {}),
|
|
530
|
+
...(successfulOutput.output
|
|
531
|
+
? { output: successfulOutput.output }
|
|
532
|
+
: {}),
|
|
533
|
+
completionAfterFailure: true,
|
|
534
|
+
completionValue: completion.value,
|
|
535
|
+
transcriptToolCount: recordedCalls.length,
|
|
536
|
+
transcriptTurnCount: recordedMessages.filter(
|
|
537
|
+
({ value }) => value.role === 'assistant',
|
|
538
|
+
).length,
|
|
539
|
+
correlation: 'successful-output-before-completion',
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
} else {
|
|
544
|
+
selected = latestMatching(diagnostics, matchingTool);
|
|
545
|
+
}
|
|
546
|
+
return selected
|
|
547
|
+
? publicDiagnostic(
|
|
548
|
+
selected,
|
|
549
|
+
diagnostics,
|
|
550
|
+
recordedCalls,
|
|
551
|
+
successfulCompletions,
|
|
552
|
+
lastInteractionOrder,
|
|
553
|
+
allowCompletionProof,
|
|
554
|
+
)
|
|
555
|
+
: undefined;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function reproduceHiddenBashFalsePositive(
|
|
559
|
+
messages: RecordedMessage[],
|
|
560
|
+
successfulResults: RecordedToolSuccess[],
|
|
561
|
+
):
|
|
562
|
+
| {
|
|
563
|
+
result: RecordedToolSuccess;
|
|
564
|
+
terminalError: string;
|
|
565
|
+
}
|
|
566
|
+
| undefined {
|
|
567
|
+
let lastAssistantTextIndex = -1;
|
|
568
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
569
|
+
const message = messages[index]?.value;
|
|
570
|
+
if (
|
|
571
|
+
message?.role === 'assistant' &&
|
|
572
|
+
Array.isArray(message.content) &&
|
|
573
|
+
message.content.some(
|
|
574
|
+
(item) =>
|
|
575
|
+
isRecord(item) &&
|
|
576
|
+
item.type === 'text' &&
|
|
577
|
+
typeof item.text === 'string' &&
|
|
578
|
+
item.text.trim().length > 0,
|
|
579
|
+
)
|
|
580
|
+
) {
|
|
581
|
+
lastAssistantTextIndex = index;
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const scanStart =
|
|
587
|
+
lastAssistantTextIndex >= 0 ? lastAssistantTextIndex + 1 : 0;
|
|
588
|
+
for (let index = messages.length - 1; index >= scanStart; index -= 1) {
|
|
589
|
+
const recordedMessage = messages[index];
|
|
590
|
+
const message = recordedMessage?.value;
|
|
591
|
+
if (
|
|
592
|
+
!recordedMessage ||
|
|
593
|
+
message?.role !== 'toolResult' ||
|
|
594
|
+
message.toolName !== 'bash' ||
|
|
595
|
+
message.isError !== false
|
|
596
|
+
) {
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
const output = firstTextContent(message.content);
|
|
600
|
+
if (output === undefined) continue;
|
|
601
|
+
|
|
602
|
+
const exitMatch = output.match(HIDDEN_BASH_EXIT_PATTERN);
|
|
603
|
+
const exitCode = exitMatch ? Number.parseInt(exitMatch[1]!, 10) : undefined;
|
|
604
|
+
const detectedExitCode =
|
|
605
|
+
exitCode !== undefined && exitCode !== 0
|
|
606
|
+
? exitCode
|
|
607
|
+
: HIDDEN_BASH_FATAL_PATTERNS.some((pattern) => pattern.test(output))
|
|
608
|
+
? 1
|
|
609
|
+
: undefined;
|
|
610
|
+
if (detectedExitCode === undefined) continue;
|
|
611
|
+
|
|
612
|
+
const result = successfulResults.find(
|
|
613
|
+
(candidate) =>
|
|
614
|
+
candidate.order === recordedMessage.order &&
|
|
615
|
+
candidate.tool === 'bash' &&
|
|
616
|
+
candidate.detectorOutput === output,
|
|
617
|
+
);
|
|
618
|
+
if (!result) return undefined;
|
|
619
|
+
const details = output.slice(0, 200);
|
|
620
|
+
return {
|
|
621
|
+
result,
|
|
622
|
+
terminalError: `bash failed (exit ${detectedExitCode}): ${details}`,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
return undefined;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function structuredCompletionValue(
|
|
629
|
+
argumentsValue: unknown,
|
|
630
|
+
): Record<string, unknown> | undefined {
|
|
631
|
+
if (!isRecord(argumentsValue)) return undefined;
|
|
632
|
+
if (
|
|
633
|
+
Object.keys(argumentsValue).length !== 1 ||
|
|
634
|
+
!Object.hasOwn(argumentsValue, 'value') ||
|
|
635
|
+
!isRecord(argumentsValue.value)
|
|
636
|
+
) {
|
|
637
|
+
return undefined;
|
|
638
|
+
}
|
|
639
|
+
return argumentsValue.value;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function normalized(value: string): string {
|
|
643
|
+
return value.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function comparableFragments(value: string): string[] {
|
|
647
|
+
const normalizedValue = normalized(value);
|
|
648
|
+
const lines = value
|
|
649
|
+
.split(/\r?\n/)
|
|
650
|
+
.map(normalized)
|
|
651
|
+
.filter((line) => line.length >= 8);
|
|
652
|
+
return [...new Set([normalizedValue, ...lines])].filter(
|
|
653
|
+
(fragment) => fragment.length >= 8,
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function diagnosticMatchesTerminalError(
|
|
658
|
+
diagnostic: ToolFailureDiagnostic,
|
|
659
|
+
terminalError: string,
|
|
660
|
+
): boolean {
|
|
661
|
+
if (!diagnostic.output) return false;
|
|
662
|
+
const detail =
|
|
663
|
+
terminalError.match(
|
|
664
|
+
/\b[a-z][\w-]* failed(?:\s*\([^)]*\))?\s*:\s*([\s\S]+)/i,
|
|
665
|
+
)?.[1] ?? terminalError;
|
|
666
|
+
const outputFragments = comparableFragments(diagnostic.output);
|
|
667
|
+
const errorFragments = comparableFragments(`${terminalError}\n${detail}`);
|
|
668
|
+
return outputFragments.some((output) =>
|
|
669
|
+
errorFragments.some(
|
|
670
|
+
(error) => output.includes(error) || error.includes(output),
|
|
671
|
+
),
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function latestMatching(
|
|
676
|
+
diagnostics: RecordedToolFailure[],
|
|
677
|
+
predicate: (diagnostic: RecordedToolFailure) => boolean,
|
|
678
|
+
): RecordedToolFailure | undefined {
|
|
679
|
+
for (let index = diagnostics.length - 1; index >= 0; index -= 1) {
|
|
680
|
+
const diagnostic = diagnostics[index];
|
|
681
|
+
if (diagnostic && predicate(diagnostic)) return diagnostic;
|
|
682
|
+
}
|
|
683
|
+
return undefined;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function preExecutionBashFailure(output: string | undefined): boolean {
|
|
687
|
+
if (!output) return false;
|
|
688
|
+
const normalizedOutput = output.toLowerCase();
|
|
689
|
+
return PRE_EXECUTION_BASH_FAILURES.some((fragment) =>
|
|
690
|
+
normalizedOutput.includes(fragment),
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function replaySafeToolCall(
|
|
695
|
+
call: RecordedToolCall,
|
|
696
|
+
diagnostics: RecordedToolFailure[],
|
|
697
|
+
bashPermission?: BashPermission,
|
|
698
|
+
approvedBashCommands: readonly string[] = [],
|
|
699
|
+
): boolean {
|
|
700
|
+
const tool = call.tool.toLowerCase();
|
|
701
|
+
if (REPLAY_SAFE_TOOLS.has(tool)) return true;
|
|
702
|
+
if (tool !== 'bash' || !call.call) return false;
|
|
703
|
+
if (
|
|
704
|
+
authorizeBash(call.call, { mode: 'read-only', allow: [] }).allowed === true
|
|
705
|
+
) {
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
if (
|
|
709
|
+
!bashPermission ||
|
|
710
|
+
authorizeBash(call.call, bashPermission, approvedBashCommands).allowed ===
|
|
711
|
+
true
|
|
712
|
+
) {
|
|
713
|
+
return false;
|
|
714
|
+
}
|
|
715
|
+
const failure = diagnostics.find(
|
|
716
|
+
(diagnostic) => diagnostic.callId === call.id,
|
|
717
|
+
);
|
|
718
|
+
return preExecutionBashFailure(failure?.output);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function transcriptReplaySafe(
|
|
722
|
+
calls: RecordedToolCall[],
|
|
723
|
+
diagnostics: RecordedToolFailure[],
|
|
724
|
+
completeTranscript: boolean,
|
|
725
|
+
): boolean {
|
|
726
|
+
return (
|
|
727
|
+
completeTranscript &&
|
|
728
|
+
calls.length > 0 &&
|
|
729
|
+
calls.every((call) => replaySafeToolCall(call, diagnostics))
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function publicDiagnostic(
|
|
734
|
+
diagnostic: RecordedToolFailure,
|
|
735
|
+
diagnostics: RecordedToolFailure[],
|
|
736
|
+
recordedCalls: RecordedToolCall[],
|
|
737
|
+
successfulCompletions: RecordedCompletion[],
|
|
738
|
+
lastInteractionOrder: number,
|
|
739
|
+
allowCompletionProof: boolean,
|
|
740
|
+
correlation?: ToolFailureDiagnostic['correlation'],
|
|
741
|
+
): ToolFailureDiagnostic {
|
|
742
|
+
const result: ToolFailureDiagnostic = {
|
|
743
|
+
tool: diagnostic.tool,
|
|
744
|
+
...(diagnostic.call ? { call: diagnostic.call } : {}),
|
|
745
|
+
...(diagnostic.output ? { output: diagnostic.output } : {}),
|
|
746
|
+
};
|
|
747
|
+
const { order } = diagnostic;
|
|
748
|
+
const latestFailureOrder = diagnostics.at(-1)?.order ?? order;
|
|
749
|
+
const completion = finalCompletion(
|
|
750
|
+
successfulCompletions,
|
|
751
|
+
latestFailureOrder,
|
|
752
|
+
lastInteractionOrder,
|
|
753
|
+
allowCompletionProof,
|
|
754
|
+
);
|
|
755
|
+
return {
|
|
756
|
+
...result,
|
|
757
|
+
...(transcriptReplaySafe(recordedCalls, diagnostics, allowCompletionProof)
|
|
758
|
+
? { replaySafe: true as const }
|
|
759
|
+
: {}),
|
|
760
|
+
...(completion
|
|
761
|
+
? {
|
|
762
|
+
completionAfterFailure: true as const,
|
|
763
|
+
completionValue: completion.value,
|
|
764
|
+
}
|
|
765
|
+
: {}),
|
|
766
|
+
...(correlation ? { correlation } : {}),
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function finalCompletion(
|
|
771
|
+
completions: RecordedCompletion[],
|
|
772
|
+
latestFailureOrder: number,
|
|
773
|
+
lastInteractionOrder: number,
|
|
774
|
+
allowCompletionProof: boolean,
|
|
775
|
+
): RecordedCompletion | undefined {
|
|
776
|
+
if (!allowCompletionProof || completions.length !== 1) return undefined;
|
|
777
|
+
const completion = completions[0];
|
|
778
|
+
return completion &&
|
|
779
|
+
completion.order > latestFailureOrder &&
|
|
780
|
+
completion.order === lastInteractionOrder
|
|
781
|
+
? completion
|
|
782
|
+
: undefined;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function isSessionFilePath(path: string): boolean {
|
|
786
|
+
return (
|
|
787
|
+
isAbsolute(path) &&
|
|
788
|
+
!path.includes('\0') &&
|
|
789
|
+
basename(path) === SESSION_FILE_NAME &&
|
|
790
|
+
SESSION_RUN_DIRECTORY.test(basename(dirname(path)))
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function pathWithin(root: string, candidate: string): boolean {
|
|
795
|
+
const fromRoot = relative(resolve(root), resolve(candidate));
|
|
796
|
+
return (
|
|
797
|
+
fromRoot !== '' &&
|
|
798
|
+
fromRoot !== '..' &&
|
|
799
|
+
!fromRoot.startsWith(`..${sep}`) &&
|
|
800
|
+
!isAbsolute(fromRoot)
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
async function readContainedSessionTail(
|
|
805
|
+
sessionFile: string,
|
|
806
|
+
trustedRoot: string,
|
|
807
|
+
identity: SubagentSessionIdentity,
|
|
808
|
+
): Promise<SessionTail | undefined> {
|
|
809
|
+
if (
|
|
810
|
+
!isSessionFilePath(sessionFile) ||
|
|
811
|
+
!isAbsolute(trustedRoot) ||
|
|
812
|
+
trustedRoot.includes('\0') ||
|
|
813
|
+
!pathWithin(trustedRoot, sessionFile) ||
|
|
814
|
+
!isValidSessionIdentity(identity) ||
|
|
815
|
+
resolve(sessionFile) !==
|
|
816
|
+
resolve(
|
|
817
|
+
trustedRoot,
|
|
818
|
+
identity.runId,
|
|
819
|
+
`run-${identity.childIndex}`,
|
|
820
|
+
SESSION_FILE_NAME,
|
|
821
|
+
)
|
|
822
|
+
) {
|
|
823
|
+
return undefined;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const resolvedSessionFile = resolve(sessionFile);
|
|
827
|
+
const inspected = await lstat(resolvedSessionFile);
|
|
828
|
+
if (inspected.isSymbolicLink() || !inspected.isFile()) return undefined;
|
|
829
|
+
|
|
830
|
+
const [canonicalRoot, canonicalSessionFile] = await Promise.all([
|
|
831
|
+
realpath(trustedRoot),
|
|
832
|
+
realpath(resolvedSessionFile),
|
|
833
|
+
]);
|
|
834
|
+
if (!pathWithin(canonicalRoot, canonicalSessionFile)) return undefined;
|
|
835
|
+
|
|
836
|
+
const handle = await open(
|
|
837
|
+
canonicalSessionFile,
|
|
838
|
+
constants.O_RDONLY | constants.O_NOFOLLOW,
|
|
839
|
+
);
|
|
840
|
+
try {
|
|
841
|
+
const opened = await handle.stat();
|
|
842
|
+
if (!opened.isFile()) return undefined;
|
|
843
|
+
const bytesToRead = Math.min(opened.size, MAX_SESSION_TAIL_BYTES);
|
|
844
|
+
if (bytesToRead === 0) return { content: '', truncated: false };
|
|
845
|
+
const start = opened.size - bytesToRead;
|
|
846
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
847
|
+
const { bytesRead } = await handle.read(buffer, 0, bytesToRead, start);
|
|
848
|
+
const afterRead = await handle.stat();
|
|
849
|
+
if (
|
|
850
|
+
afterRead.dev !== opened.dev ||
|
|
851
|
+
afterRead.ino !== opened.ino ||
|
|
852
|
+
afterRead.size !== opened.size ||
|
|
853
|
+
afterRead.mtimeMs !== opened.mtimeMs
|
|
854
|
+
) {
|
|
855
|
+
return undefined;
|
|
856
|
+
}
|
|
857
|
+
let content = buffer.subarray(0, bytesRead).toString('utf8');
|
|
858
|
+
if (start > 0) {
|
|
859
|
+
const firstNewline = content.indexOf('\n');
|
|
860
|
+
content = firstNewline === -1 ? '' : content.slice(firstNewline + 1);
|
|
861
|
+
}
|
|
862
|
+
return { content, truncated: start > 0 };
|
|
863
|
+
} finally {
|
|
864
|
+
await handle.close();
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function isValidSessionIdentity(identity: SubagentSessionIdentity): boolean {
|
|
869
|
+
return (
|
|
870
|
+
identity.runId.length > 0 &&
|
|
871
|
+
!identity.runId.includes('\0') &&
|
|
872
|
+
basename(identity.runId) === identity.runId &&
|
|
873
|
+
identity.runId !== '.' &&
|
|
874
|
+
identity.runId !== '..' &&
|
|
875
|
+
Number.isSafeInteger(identity.childIndex) &&
|
|
876
|
+
identity.childIndex >= 0
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
export function deriveSubagentSessionRoot(
|
|
881
|
+
parentSessionFile: string | undefined,
|
|
882
|
+
): string | undefined {
|
|
883
|
+
if (
|
|
884
|
+
!parentSessionFile ||
|
|
885
|
+
!isAbsolute(parentSessionFile) ||
|
|
886
|
+
parentSessionFile.includes('\0')
|
|
887
|
+
) {
|
|
888
|
+
return undefined;
|
|
889
|
+
}
|
|
890
|
+
const parentName = basename(parentSessionFile);
|
|
891
|
+
if (
|
|
892
|
+
!parentName.endsWith(SESSION_FILE_SUFFIX) ||
|
|
893
|
+
parentName === SESSION_FILE_SUFFIX
|
|
894
|
+
) {
|
|
895
|
+
return undefined;
|
|
896
|
+
}
|
|
897
|
+
return join(
|
|
898
|
+
dirname(parentSessionFile),
|
|
899
|
+
parentName.slice(0, -SESSION_FILE_SUFFIX.length),
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
export async function readToolFailureDiagnostic(
|
|
904
|
+
sessionFile: string | undefined,
|
|
905
|
+
trustedRoot: string | undefined,
|
|
906
|
+
identity: SubagentSessionIdentity | undefined,
|
|
907
|
+
expectedTool?: string,
|
|
908
|
+
terminalError?: string,
|
|
909
|
+
): Promise<ToolFailureDiagnostic | undefined> {
|
|
910
|
+
if (!sessionFile || !trustedRoot || !identity) return undefined;
|
|
911
|
+
try {
|
|
912
|
+
const tail = await readContainedSessionTail(
|
|
913
|
+
sessionFile,
|
|
914
|
+
trustedRoot,
|
|
915
|
+
identity,
|
|
916
|
+
);
|
|
917
|
+
return parseToolFailureDiagnostic(
|
|
918
|
+
tail?.content ?? '',
|
|
919
|
+
expectedTool,
|
|
920
|
+
terminalError,
|
|
921
|
+
tail?.truncated !== true,
|
|
922
|
+
);
|
|
923
|
+
} catch {
|
|
924
|
+
return undefined;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
export async function readDelegationReplayAudit(
|
|
929
|
+
sessionFile: string | undefined,
|
|
930
|
+
trustedRoot: string | undefined,
|
|
931
|
+
identity: SubagentSessionIdentity | undefined,
|
|
932
|
+
expectation: DelegationReplayExpectation,
|
|
933
|
+
): Promise<DelegationReplayAudit | undefined> {
|
|
934
|
+
if (!sessionFile || !trustedRoot || !identity) return undefined;
|
|
935
|
+
try {
|
|
936
|
+
const tail = await readContainedSessionTail(
|
|
937
|
+
sessionFile,
|
|
938
|
+
trustedRoot,
|
|
939
|
+
identity,
|
|
940
|
+
);
|
|
941
|
+
return tail
|
|
942
|
+
? parseDelegationReplayAudit(tail.content, expectation, !tail.truncated)
|
|
943
|
+
: undefined;
|
|
944
|
+
} catch {
|
|
945
|
+
return undefined;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
export function formatToolFailureDiagnostic(
|
|
950
|
+
diagnostic: ToolFailureDiagnostic,
|
|
951
|
+
): string[] {
|
|
952
|
+
const successfulOutputCorrelation =
|
|
953
|
+
diagnostic.correlation === 'successful-output-before-completion';
|
|
954
|
+
return [
|
|
955
|
+
`${successfulOutputCorrelation ? 'Terminal-reported tool' : 'Failed tool'}: ${diagnostic.tool}`,
|
|
956
|
+
...(diagnostic.call
|
|
957
|
+
? [
|
|
958
|
+
`${diagnostic.tool === 'bash' ? 'Command' : 'Arguments'}: ${diagnostic.call}`,
|
|
959
|
+
]
|
|
960
|
+
: []),
|
|
961
|
+
...(diagnostic.output
|
|
962
|
+
? [
|
|
963
|
+
`${successfulOutputCorrelation ? 'Successful tool output' : 'Tool error'}: ${diagnostic.output}`,
|
|
964
|
+
]
|
|
965
|
+
: []),
|
|
966
|
+
...(diagnostic.correlation === 'latest-before-completion'
|
|
967
|
+
? [
|
|
968
|
+
'Correlation: latest failed tool call before successful structured_output; terminal text did not identify the call',
|
|
969
|
+
]
|
|
970
|
+
: []),
|
|
971
|
+
...(successfulOutputCorrelation
|
|
972
|
+
? [
|
|
973
|
+
'Correlation: terminal error text came from a successful tool result before the final structured_output',
|
|
974
|
+
]
|
|
975
|
+
: []),
|
|
976
|
+
];
|
|
977
|
+
}
|