@wichayutdew/pi-workflows 2.5.1 → 2.6.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/README.md +4 -1
- package/dist/index.js +1133 -2409
- package/examples/starter-kit/agents/planner.md +7 -0
- package/examples/starter-kit/agents/reviewer.md +6 -0
- package/examples/starter-kit/agents/scout.md +6 -0
- package/examples/starter-kit/agents/worker.md +7 -0
- package/examples/starter-kit/agents/workspace-preparer.md +6 -0
- package/examples/starter-kit/investigate.workflow.yaml +114 -0
- package/examples/starter-kit/mr-comment.workflow.yaml +114 -54
- package/examples/starter-kit/mr-review.workflow.yaml +77 -56
- package/examples/starter-kit/settings.yaml +3 -0
- package/examples/starter-kit/steps/investigate/investigate.md +77 -0
- package/examples/starter-kit/steps/investigate/retrieve.md +63 -0
- package/examples/starter-kit/steps/investigate/validate.md +46 -0
- package/examples/starter-kit/steps/mr-comment/checkout-source.md +67 -0
- package/examples/starter-kit/steps/mr-comment/fetch.md +34 -26
- package/examples/starter-kit/steps/mr-comment/implement.md +34 -27
- package/examples/starter-kit/steps/mr-comment/plan.md +54 -40
- package/examples/starter-kit/steps/mr-comment/publish.md +28 -27
- package/examples/starter-kit/steps/mr-comment/verify.md +42 -35
- package/examples/starter-kit/steps/mr-review/fetch.md +52 -22
- package/examples/starter-kit/steps/mr-review/publish-approved.md +49 -0
- package/examples/starter-kit/steps/mr-review/review-for-approval.md +142 -0
- package/examples/starter-kit/steps/mr-review/verify-published.md +37 -0
- package/examples/starter-kit/steps/shared/prepare-workspace.md +91 -73
- package/examples/starter-kit/steps/shared/publish-remote.md +45 -0
- package/examples/starter-kit/steps/ticket/implement.md +59 -25
- package/examples/starter-kit/steps/ticket/plan.md +145 -72
- package/examples/starter-kit/steps/ticket/verify.md +80 -45
- package/examples/starter-kit/steps/work/implement.md +60 -24
- package/examples/starter-kit/steps/work/plan.md +129 -57
- package/examples/starter-kit/steps/work/verify.md +58 -22
- package/examples/starter-kit/ticket.workflow.yaml +75 -36
- package/examples/starter-kit/work.workflow.yaml +63 -33
- package/package.json +3 -16
- package/schemas/workflow.schema.json +2 -15
- package/src/agents/profile.ts +98 -0
- package/src/config/ceiling.ts +0 -82
- package/src/config/types.ts +6 -53
- package/src/config/validation/settings.ts +2 -14
- package/src/config/validation/step.ts +12 -8
- package/src/config/validation/workflow.ts +1 -10
- package/src/engine/run-workflow-validation.ts +0 -3
- package/src/engine/state-types.ts +1 -1
- package/src/harness/action-context.ts +1 -13
- package/src/harness/delegation-control-actions.ts +4 -63
- package/src/harness/delegation-plan.ts +26 -80
- package/src/harness/delegation-response-actions.ts +22 -135
- package/src/harness/dependencies.ts +1 -12
- package/src/harness/status-actions.ts +1 -0
- package/src/harness/step-execution-actions.ts +2 -9
- package/src/harness/types.ts +2 -40
- package/src/harness.ts +2 -17
- package/src/index.ts +4 -9
- package/src/integrations/subagents/child-policy-validation.ts +6 -7
- package/src/integrations/subagents/child-runtime-dependencies.ts +1 -1
- package/src/integrations/subagents/child-runtime-policy.ts +1 -7
- package/src/integrations/subagents/child-runtime.ts +18 -9
- package/src/integrations/subagents/client.ts +240 -98
- package/src/integrations/subagents/protocol-events.ts +32 -15
- package/src/integrations/subagents/protocol.ts +1 -1
- package/src/preflight.ts +0 -8
- package/src/prompt/main-workflow-notice.ts +8 -15
- package/src/prompt/step-task.ts +7 -6
- package/src/workflow-status/format-status.ts +5 -1
- package/src/workflow-status/render-step-detail.ts +94 -0
- package/src/workflow-status/types.ts +1 -0
- package/src/workflow-status/view.ts +49 -14
- package/agents/step.md +0 -32
- package/examples/mr-comments.workflow.yaml +0 -125
- package/examples/prompts/mr-comments/implement.md +0 -17
- package/examples/prompts/mr-comments/inspect.md +0 -5
- package/examples/prompts/mr-comments/plan.md +0 -54
- package/examples/prompts/mr-comments/verify.md +0 -9
- package/examples/settings.yaml +0 -27
- package/examples/starter-kit/steps/mr-review/publish.md +0 -48
- package/examples/starter-kit/steps/mr-review/review.md +0 -76
- package/examples/starter-kit/steps/mr-review/verify.md +0 -35
- package/src/config/validation/subagent.ts +0 -288
- package/src/harness/delegation-failure.ts +0 -248
- package/src/harness/delegation-recovery-validation.ts +0 -161
- package/src/harness/delegation-retry-policy.ts +0 -120
- package/src/integrations/subagents/client-delegation.ts +0 -181
- package/src/integrations/subagents/client-messages.ts +0 -66
- package/src/integrations/subagents/client-types.ts +0 -36
- package/src/integrations/subagents/diagnostic-format.ts +0 -45
- package/src/integrations/subagents/diagnostic-text.ts +0 -114
- package/src/integrations/subagents/diagnostic-types.ts +0 -83
- package/src/integrations/subagents/diagnostics.ts +0 -26
- package/src/integrations/subagents/failure-correlation.ts +0 -285
- package/src/integrations/subagents/failure-transcript.ts +0 -252
- package/src/integrations/subagents/hidden-bash-failure.ts +0 -98
- package/src/integrations/subagents/replay-audit.ts +0 -146
- package/src/integrations/subagents/replay-safety.ts +0 -67
- package/src/integrations/subagents/session-diagnostics.ts +0 -357
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
import { comparableDiagnosticFragments } from './diagnostic-text.ts';
|
|
2
|
-
import type {
|
|
3
|
-
RecordedCompletion,
|
|
4
|
-
RecordedToolCall,
|
|
5
|
-
RecordedToolFailure,
|
|
6
|
-
RecordedTranscriptWarning,
|
|
7
|
-
ToolFailureDiagnostic,
|
|
8
|
-
} from './diagnostic-types.ts';
|
|
9
|
-
import { parseFailureTranscript } from './failure-transcript.ts';
|
|
10
|
-
import { reproduceHiddenBashFalsePositive } from './hidden-bash-failure.ts';
|
|
11
|
-
import { isFailureTranscriptReplaySafe } from './replay-safety.ts';
|
|
12
|
-
|
|
13
|
-
const diagnosticMatchesTerminalError = (
|
|
14
|
-
diagnostic: ToolFailureDiagnostic,
|
|
15
|
-
terminalError: string,
|
|
16
|
-
): boolean => {
|
|
17
|
-
if (!diagnostic.output) return false;
|
|
18
|
-
const detail =
|
|
19
|
-
terminalError.match(
|
|
20
|
-
/\b[a-z][\w-]* failed(?:\s*\([^)]*\))?\s*:\s*([\s\S]+)/i,
|
|
21
|
-
)?.[1] ?? terminalError;
|
|
22
|
-
const outputFragments = comparableDiagnosticFragments(diagnostic.output);
|
|
23
|
-
const errorFragments = comparableDiagnosticFragments(
|
|
24
|
-
`${terminalError}\n${detail}`,
|
|
25
|
-
);
|
|
26
|
-
return outputFragments.some((output) =>
|
|
27
|
-
errorFragments.some(
|
|
28
|
-
(error) => output.includes(error) || error.includes(output),
|
|
29
|
-
),
|
|
30
|
-
);
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const latestMatching = (
|
|
34
|
-
diagnostics: ReadonlyArray<RecordedToolFailure>,
|
|
35
|
-
predicate: (diagnostic: RecordedToolFailure) => boolean,
|
|
36
|
-
): RecordedToolFailure | undefined => {
|
|
37
|
-
for (let index = diagnostics.length - 1; index >= 0; index -= 1) {
|
|
38
|
-
const diagnostic = diagnostics[index];
|
|
39
|
-
if (diagnostic && predicate(diagnostic)) return diagnostic;
|
|
40
|
-
}
|
|
41
|
-
return undefined;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const latestWarningAfter = (
|
|
45
|
-
warnings: ReadonlyArray<RecordedTranscriptWarning>,
|
|
46
|
-
order: number,
|
|
47
|
-
): RecordedTranscriptWarning | undefined => {
|
|
48
|
-
for (let index = warnings.length - 1; index >= 0; index -= 1) {
|
|
49
|
-
const warning = warnings[index];
|
|
50
|
-
if (warning && warning.order > order) return warning;
|
|
51
|
-
}
|
|
52
|
-
return undefined;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const finalCompletion = (
|
|
56
|
-
completions: ReadonlyArray<RecordedCompletion>,
|
|
57
|
-
latestFailureOrder: number,
|
|
58
|
-
lastInteractionOrder: number,
|
|
59
|
-
allowCompletionProof: boolean,
|
|
60
|
-
): RecordedCompletion | undefined => {
|
|
61
|
-
if (!allowCompletionProof || completions.length !== 1) return undefined;
|
|
62
|
-
const completion = completions[0];
|
|
63
|
-
return completion &&
|
|
64
|
-
completion.order > latestFailureOrder &&
|
|
65
|
-
completion.order === lastInteractionOrder
|
|
66
|
-
? completion
|
|
67
|
-
: undefined;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const publicDiagnostic = (
|
|
71
|
-
diagnostic: RecordedToolFailure,
|
|
72
|
-
diagnostics: ReadonlyArray<RecordedToolFailure>,
|
|
73
|
-
recordedCalls: ReadonlyArray<RecordedToolCall>,
|
|
74
|
-
successfulCompletions: ReadonlyArray<RecordedCompletion>,
|
|
75
|
-
postCompletionWarning: RecordedTranscriptWarning | undefined,
|
|
76
|
-
lastInteractionOrder: number,
|
|
77
|
-
allowCompletionProof: boolean,
|
|
78
|
-
correlation?: ToolFailureDiagnostic['correlation'],
|
|
79
|
-
): ToolFailureDiagnostic => {
|
|
80
|
-
const result: ToolFailureDiagnostic = {
|
|
81
|
-
tool: diagnostic.tool,
|
|
82
|
-
...(diagnostic.call ? { call: diagnostic.call } : {}),
|
|
83
|
-
...(diagnostic.output ? { output: diagnostic.output } : {}),
|
|
84
|
-
};
|
|
85
|
-
const latestFailureOrder = diagnostics.at(-1)?.order ?? diagnostic.order;
|
|
86
|
-
const completion = finalCompletion(
|
|
87
|
-
successfulCompletions,
|
|
88
|
-
latestFailureOrder,
|
|
89
|
-
lastInteractionOrder,
|
|
90
|
-
allowCompletionProof,
|
|
91
|
-
);
|
|
92
|
-
return {
|
|
93
|
-
...result,
|
|
94
|
-
...(postCompletionWarning
|
|
95
|
-
? { postCompletionWarning: postCompletionWarning.content }
|
|
96
|
-
: {}),
|
|
97
|
-
...(isFailureTranscriptReplaySafe({
|
|
98
|
-
calls: recordedCalls,
|
|
99
|
-
diagnostics,
|
|
100
|
-
isCompleteTranscript: allowCompletionProof,
|
|
101
|
-
})
|
|
102
|
-
? { replaySafe: true as const }
|
|
103
|
-
: {}),
|
|
104
|
-
...(completion
|
|
105
|
-
? {
|
|
106
|
-
completionAfterFailure: true as const,
|
|
107
|
-
completionValue: completion.value,
|
|
108
|
-
}
|
|
109
|
-
: {}),
|
|
110
|
-
...(correlation ? { correlation } : {}),
|
|
111
|
-
};
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const hiddenFalsePositiveDiagnostic = (
|
|
115
|
-
expectedTool: string | undefined,
|
|
116
|
-
terminalError: string,
|
|
117
|
-
allowCompletionProof: boolean,
|
|
118
|
-
transcript: ReturnType<typeof parseFailureTranscript>,
|
|
119
|
-
): ToolFailureDiagnostic | undefined => {
|
|
120
|
-
const {
|
|
121
|
-
diagnostics,
|
|
122
|
-
hasValidFalsePositiveProof,
|
|
123
|
-
lastInteractionOrder,
|
|
124
|
-
recordedCalls,
|
|
125
|
-
recordedMessages,
|
|
126
|
-
resultCallIds,
|
|
127
|
-
successfulCompletions,
|
|
128
|
-
successfulResults,
|
|
129
|
-
transcriptWarnings,
|
|
130
|
-
} = transcript;
|
|
131
|
-
const completionOrder = successfulCompletions.at(-1)?.order ?? 0;
|
|
132
|
-
const postCompletionWarning = latestWarningAfter(
|
|
133
|
-
transcriptWarnings,
|
|
134
|
-
completionOrder,
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
const falsePositive = reproduceHiddenBashFalsePositive(
|
|
138
|
-
recordedMessages,
|
|
139
|
-
successfulResults,
|
|
140
|
-
);
|
|
141
|
-
const hasLaterFailure =
|
|
142
|
-
falsePositive !== undefined &&
|
|
143
|
-
diagnostics.some(
|
|
144
|
-
(diagnostic) => diagnostic.order > falsePositive.result.order,
|
|
145
|
-
);
|
|
146
|
-
const completion = falsePositive
|
|
147
|
-
? finalCompletion(
|
|
148
|
-
successfulCompletions,
|
|
149
|
-
Math.max(
|
|
150
|
-
falsePositive.result.order,
|
|
151
|
-
diagnostics.at(-1)?.order ?? falsePositive.result.order,
|
|
152
|
-
),
|
|
153
|
-
lastInteractionOrder,
|
|
154
|
-
allowCompletionProof,
|
|
155
|
-
)
|
|
156
|
-
: undefined;
|
|
157
|
-
if (
|
|
158
|
-
!hasValidFalsePositiveProof ||
|
|
159
|
-
recordedCalls.length !== resultCallIds.size ||
|
|
160
|
-
!recordedCalls.every((call) => resultCallIds.has(call.id)) ||
|
|
161
|
-
recordedCalls.filter((call) => call.tool === 'structured_output').length !==
|
|
162
|
-
1 ||
|
|
163
|
-
expectedTool?.toLowerCase() !== 'bash' ||
|
|
164
|
-
!falsePositive ||
|
|
165
|
-
hasLaterFailure ||
|
|
166
|
-
postCompletionWarning !== undefined ||
|
|
167
|
-
terminalError !== falsePositive.terminalError ||
|
|
168
|
-
!completion
|
|
169
|
-
) {
|
|
170
|
-
return undefined;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const successfulOutput = falsePositive.result;
|
|
174
|
-
return {
|
|
175
|
-
tool: successfulOutput.tool,
|
|
176
|
-
...(successfulOutput.call ? { call: successfulOutput.call } : {}),
|
|
177
|
-
...(successfulOutput.output ? { output: successfulOutput.output } : {}),
|
|
178
|
-
completionAfterFailure: true,
|
|
179
|
-
completionValue: completion.value,
|
|
180
|
-
transcriptToolCount: recordedCalls.length,
|
|
181
|
-
transcriptTurnCount: recordedMessages.filter(
|
|
182
|
-
({ value }) => value.role === 'assistant',
|
|
183
|
-
).length,
|
|
184
|
-
correlation: 'successful-output-before-completion',
|
|
185
|
-
};
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Correlates a terminal tool failure with trusted evidence from a child
|
|
190
|
-
* transcript.
|
|
191
|
-
*/
|
|
192
|
-
export const parseToolFailureDiagnostic = (
|
|
193
|
-
transcript: string,
|
|
194
|
-
expectedTool?: string,
|
|
195
|
-
terminalError?: string,
|
|
196
|
-
allowCompletionProof = true,
|
|
197
|
-
): ToolFailureDiagnostic | undefined => {
|
|
198
|
-
const parsed = parseFailureTranscript(transcript);
|
|
199
|
-
const {
|
|
200
|
-
diagnostics,
|
|
201
|
-
lastInteractionOrder,
|
|
202
|
-
recordedCalls,
|
|
203
|
-
successfulCompletions,
|
|
204
|
-
transcriptWarnings,
|
|
205
|
-
} = parsed;
|
|
206
|
-
const completionOrder = successfulCompletions.at(-1)?.order ?? 0;
|
|
207
|
-
const postCompletionWarning = latestWarningAfter(
|
|
208
|
-
transcriptWarnings,
|
|
209
|
-
completionOrder,
|
|
210
|
-
);
|
|
211
|
-
const matchesExpectedTool = (
|
|
212
|
-
diagnostic: Pick<RecordedToolFailure, 'tool'>,
|
|
213
|
-
): boolean =>
|
|
214
|
-
expectedTool === undefined ||
|
|
215
|
-
diagnostic.tool.toLowerCase() === expectedTool.toLowerCase();
|
|
216
|
-
|
|
217
|
-
let selected: RecordedToolFailure | undefined;
|
|
218
|
-
if (terminalError) {
|
|
219
|
-
selected = latestMatching(
|
|
220
|
-
diagnostics,
|
|
221
|
-
(diagnostic) =>
|
|
222
|
-
matchesExpectedTool(diagnostic) &&
|
|
223
|
-
diagnosticMatchesTerminalError(diagnostic, terminalError),
|
|
224
|
-
);
|
|
225
|
-
if (!selected) {
|
|
226
|
-
const fallback = latestMatching(diagnostics, matchesExpectedTool);
|
|
227
|
-
const latestFailureOrder = diagnostics.at(-1)?.order;
|
|
228
|
-
if (
|
|
229
|
-
fallback &&
|
|
230
|
-
latestFailureOrder !== undefined &&
|
|
231
|
-
finalCompletion(
|
|
232
|
-
successfulCompletions,
|
|
233
|
-
latestFailureOrder,
|
|
234
|
-
lastInteractionOrder,
|
|
235
|
-
allowCompletionProof,
|
|
236
|
-
)
|
|
237
|
-
) {
|
|
238
|
-
return publicDiagnostic(
|
|
239
|
-
fallback,
|
|
240
|
-
diagnostics,
|
|
241
|
-
recordedCalls,
|
|
242
|
-
successfulCompletions,
|
|
243
|
-
postCompletionWarning,
|
|
244
|
-
lastInteractionOrder,
|
|
245
|
-
allowCompletionProof,
|
|
246
|
-
'latest-before-completion',
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
if (!selected) {
|
|
251
|
-
const falsePositive = hiddenFalsePositiveDiagnostic(
|
|
252
|
-
expectedTool,
|
|
253
|
-
terminalError,
|
|
254
|
-
allowCompletionProof,
|
|
255
|
-
parsed,
|
|
256
|
-
);
|
|
257
|
-
if (falsePositive) return falsePositive;
|
|
258
|
-
}
|
|
259
|
-
} else {
|
|
260
|
-
selected = latestMatching(diagnostics, matchesExpectedTool);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (!selected && postCompletionWarning) {
|
|
264
|
-
selected =
|
|
265
|
-
latestMatching(diagnostics, matchesExpectedTool) ?? diagnostics.at(-1);
|
|
266
|
-
if (!selected) {
|
|
267
|
-
return {
|
|
268
|
-
tool: expectedTool ?? 'subagent',
|
|
269
|
-
postCompletionWarning: postCompletionWarning.content,
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return selected
|
|
275
|
-
? publicDiagnostic(
|
|
276
|
-
selected,
|
|
277
|
-
diagnostics,
|
|
278
|
-
recordedCalls,
|
|
279
|
-
successfulCompletions,
|
|
280
|
-
postCompletionWarning,
|
|
281
|
-
lastInteractionOrder,
|
|
282
|
-
allowCompletionProof,
|
|
283
|
-
)
|
|
284
|
-
: undefined;
|
|
285
|
-
};
|
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
diagnosticTextContent,
|
|
3
|
-
diagnosticToolCallText,
|
|
4
|
-
firstDiagnosticTextContent,
|
|
5
|
-
isDiagnosticRecord,
|
|
6
|
-
structuredCompletionValue,
|
|
7
|
-
} from './diagnostic-text.ts';
|
|
8
|
-
import type {
|
|
9
|
-
ParsedFailureTranscript,
|
|
10
|
-
RecordedCompletion,
|
|
11
|
-
RecordedMessage,
|
|
12
|
-
RecordedToolCall,
|
|
13
|
-
RecordedToolFailure,
|
|
14
|
-
RecordedToolSuccess,
|
|
15
|
-
RecordedTranscriptWarning,
|
|
16
|
-
} from './diagnostic-types.ts';
|
|
17
|
-
|
|
18
|
-
const WATCHDOG_WARNING_TYPE = 'subagent_watchdog_warning';
|
|
19
|
-
const MAX_WARNING_FIELD_CHARS = 600;
|
|
20
|
-
|
|
21
|
-
const boundedWarningField = (value: string): string => {
|
|
22
|
-
const normalized = value.trim().replaceAll(/\s+/g, ' ');
|
|
23
|
-
return normalized.length <= MAX_WARNING_FIELD_CHARS
|
|
24
|
-
? normalized
|
|
25
|
-
: `${normalized.slice(0, MAX_WARNING_FIELD_CHARS - 1)}…`;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const transcriptWarningContent = (
|
|
29
|
-
entry: Readonly<Record<string, unknown>>,
|
|
30
|
-
): string | undefined => {
|
|
31
|
-
if (
|
|
32
|
-
entry.type !== 'custom_message' ||
|
|
33
|
-
entry.customType !== WATCHDOG_WARNING_TYPE
|
|
34
|
-
) {
|
|
35
|
-
return undefined;
|
|
36
|
-
}
|
|
37
|
-
const details = entry.details;
|
|
38
|
-
if (isDiagnosticRecord(details)) {
|
|
39
|
-
const fields = [
|
|
40
|
-
typeof details.summary === 'string' ? details.summary : undefined,
|
|
41
|
-
typeof details.evidence === 'string' ? details.evidence : undefined,
|
|
42
|
-
typeof details.recommendedAction === 'string'
|
|
43
|
-
? `Recommended action: ${details.recommendedAction}`
|
|
44
|
-
: undefined,
|
|
45
|
-
].filter((field): field is string => Boolean(field?.trim()));
|
|
46
|
-
if (fields.length > 0) return boundedWarningField(fields.join(' '));
|
|
47
|
-
}
|
|
48
|
-
return typeof entry.content === 'string' && entry.content.trim()
|
|
49
|
-
? boundedWarningField(entry.content)
|
|
50
|
-
: 'The child emitted an unresolved watchdog warning after completion.';
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const isBenignTerminalAssistant = (
|
|
54
|
-
message: Readonly<Record<string, unknown>>,
|
|
55
|
-
): boolean =>
|
|
56
|
-
message.stopReason === 'stop' &&
|
|
57
|
-
message.errorMessage === undefined &&
|
|
58
|
-
Array.isArray(message.content) &&
|
|
59
|
-
message.content.length === 1 &&
|
|
60
|
-
isDiagnosticRecord(message.content[0]) &&
|
|
61
|
-
message.content[0].type === 'text' &&
|
|
62
|
-
message.content[0].text === '';
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Parses the tool calls, results, completions, and structural evidence needed
|
|
66
|
-
* to correlate a terminal failure.
|
|
67
|
-
*/
|
|
68
|
-
export const parseFailureTranscript = (
|
|
69
|
-
transcript: string,
|
|
70
|
-
): ParsedFailureTranscript => {
|
|
71
|
-
const calls = new Map<string, RecordedToolCall>();
|
|
72
|
-
const recordedCalls: Array<RecordedToolCall> = [];
|
|
73
|
-
const diagnostics: Array<RecordedToolFailure> = [];
|
|
74
|
-
const successfulResults: Array<RecordedToolSuccess> = [];
|
|
75
|
-
const successfulCompletions: Array<RecordedCompletion> = [];
|
|
76
|
-
const transcriptWarnings: Array<RecordedTranscriptWarning> = [];
|
|
77
|
-
const recordedMessages: Array<RecordedMessage> = [];
|
|
78
|
-
const resultCallIds = new Set<string>();
|
|
79
|
-
let hasValidFalsePositiveProof = true;
|
|
80
|
-
let lastInteractionOrder = 0;
|
|
81
|
-
let order = 0;
|
|
82
|
-
|
|
83
|
-
for (const line of transcript.split('\n')) {
|
|
84
|
-
order += 1;
|
|
85
|
-
if (!line.trim()) continue;
|
|
86
|
-
|
|
87
|
-
let entry: unknown;
|
|
88
|
-
try {
|
|
89
|
-
entry = JSON.parse(line);
|
|
90
|
-
} catch {
|
|
91
|
-
hasValidFalsePositiveProof = false;
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
if (isDiagnosticRecord(entry)) {
|
|
95
|
-
const warning = transcriptWarningContent(entry);
|
|
96
|
-
if (warning) {
|
|
97
|
-
transcriptWarnings.push({ order, content: warning });
|
|
98
|
-
lastInteractionOrder = order;
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (!isDiagnosticRecord(entry) || entry.type !== 'message') continue;
|
|
103
|
-
const message = entry.message;
|
|
104
|
-
if (!isDiagnosticRecord(message)) {
|
|
105
|
-
hasValidFalsePositiveProof = false;
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
recordedMessages.push({ order, value: message });
|
|
109
|
-
if (
|
|
110
|
-
message.role === 'assistant' &&
|
|
111
|
-
((typeof message.errorMessage === 'string' &&
|
|
112
|
-
message.errorMessage.trim().length > 0) ||
|
|
113
|
-
message.stopReason === 'error' ||
|
|
114
|
-
message.stopReason === 'aborted')
|
|
115
|
-
) {
|
|
116
|
-
hasValidFalsePositiveProof = false;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (message.role === 'assistant') {
|
|
120
|
-
if (!Array.isArray(message.content)) {
|
|
121
|
-
hasValidFalsePositiveProof = false;
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
if (!isBenignTerminalAssistant(message)) {
|
|
125
|
-
lastInteractionOrder = order;
|
|
126
|
-
}
|
|
127
|
-
const toolCalls = message.content.filter(
|
|
128
|
-
(item): item is Record<string, unknown> =>
|
|
129
|
-
isDiagnosticRecord(item) &&
|
|
130
|
-
item.type === 'toolCall' &&
|
|
131
|
-
typeof item.id === 'string' &&
|
|
132
|
-
typeof item.name === 'string',
|
|
133
|
-
);
|
|
134
|
-
for (const item of message.content) {
|
|
135
|
-
if (
|
|
136
|
-
!isDiagnosticRecord(item) ||
|
|
137
|
-
item.type !== 'toolCall' ||
|
|
138
|
-
typeof item.id !== 'string' ||
|
|
139
|
-
typeof item.name !== 'string'
|
|
140
|
-
) {
|
|
141
|
-
if (isDiagnosticRecord(item) && item.type === 'toolCall') {
|
|
142
|
-
hasValidFalsePositiveProof = false;
|
|
143
|
-
}
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
if (calls.has(item.id)) hasValidFalsePositiveProof = false;
|
|
147
|
-
|
|
148
|
-
const call = diagnosticToolCallText({
|
|
149
|
-
tool: item.name,
|
|
150
|
-
argumentsValue: item.arguments,
|
|
151
|
-
});
|
|
152
|
-
const isExclusiveCompletion =
|
|
153
|
-
toolCalls.length === 1 &&
|
|
154
|
-
message.content.every(
|
|
155
|
-
(contentItem) =>
|
|
156
|
-
isDiagnosticRecord(contentItem) &&
|
|
157
|
-
(contentItem.type === 'thinking' ||
|
|
158
|
-
contentItem.type === 'text' ||
|
|
159
|
-
contentItem.type === 'toolCall'),
|
|
160
|
-
);
|
|
161
|
-
const completionValue =
|
|
162
|
-
item.name === 'structured_output' && isExclusiveCompletion
|
|
163
|
-
? structuredCompletionValue(item.arguments)
|
|
164
|
-
: undefined;
|
|
165
|
-
const recordedCall: RecordedToolCall = {
|
|
166
|
-
id: item.id,
|
|
167
|
-
order,
|
|
168
|
-
tool: item.name,
|
|
169
|
-
...(call ? { call } : {}),
|
|
170
|
-
...(completionValue ? { completionValue } : {}),
|
|
171
|
-
};
|
|
172
|
-
calls.set(item.id, recordedCall);
|
|
173
|
-
recordedCalls.push(recordedCall);
|
|
174
|
-
}
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (message.role !== 'toolResult' || typeof message.toolName !== 'string') {
|
|
179
|
-
if (message.role === 'toolResult') hasValidFalsePositiveProof = false;
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
lastInteractionOrder = order;
|
|
183
|
-
if (
|
|
184
|
-
typeof message.toolCallId !== 'string' ||
|
|
185
|
-
typeof message.isError !== 'boolean' ||
|
|
186
|
-
!Array.isArray(message.content) ||
|
|
187
|
-
resultCallIds.has(message.toolCallId)
|
|
188
|
-
) {
|
|
189
|
-
hasValidFalsePositiveProof = false;
|
|
190
|
-
}
|
|
191
|
-
if (typeof message.toolCallId === 'string') {
|
|
192
|
-
resultCallIds.add(message.toolCallId);
|
|
193
|
-
}
|
|
194
|
-
const recorded =
|
|
195
|
-
typeof message.toolCallId === 'string'
|
|
196
|
-
? calls.get(message.toolCallId)
|
|
197
|
-
: undefined;
|
|
198
|
-
const doesCallMatchResult = recorded?.tool === message.toolName;
|
|
199
|
-
if (!doesCallMatchResult) hasValidFalsePositiveProof = false;
|
|
200
|
-
|
|
201
|
-
if (
|
|
202
|
-
message.toolName === 'structured_output' &&
|
|
203
|
-
message.isError === false &&
|
|
204
|
-
doesCallMatchResult &&
|
|
205
|
-
recorded.completionValue
|
|
206
|
-
) {
|
|
207
|
-
successfulCompletions.push({
|
|
208
|
-
order,
|
|
209
|
-
value: recorded.completionValue,
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
if (
|
|
213
|
-
message.isError === false &&
|
|
214
|
-
message.toolName !== 'structured_output' &&
|
|
215
|
-
doesCallMatchResult
|
|
216
|
-
) {
|
|
217
|
-
const output = diagnosticTextContent(message.content);
|
|
218
|
-
const detectorOutput = firstDiagnosticTextContent(message.content);
|
|
219
|
-
successfulResults.push({
|
|
220
|
-
order,
|
|
221
|
-
tool: message.toolName,
|
|
222
|
-
...(recorded.call ? { call: recorded.call } : {}),
|
|
223
|
-
...(output ? { output } : {}),
|
|
224
|
-
...(detectorOutput !== undefined ? { detectorOutput } : {}),
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
if (message.isError !== true) continue;
|
|
228
|
-
|
|
229
|
-
const output = diagnosticTextContent(message.content);
|
|
230
|
-
diagnostics.push({
|
|
231
|
-
tool: message.toolName,
|
|
232
|
-
...(doesCallMatchResult && recorded.call ? { call: recorded.call } : {}),
|
|
233
|
-
...(output ? { output } : {}),
|
|
234
|
-
...(doesCallMatchResult && typeof message.toolCallId === 'string'
|
|
235
|
-
? { callId: message.toolCallId }
|
|
236
|
-
: {}),
|
|
237
|
-
order,
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return {
|
|
242
|
-
recordedCalls,
|
|
243
|
-
diagnostics,
|
|
244
|
-
successfulResults,
|
|
245
|
-
successfulCompletions,
|
|
246
|
-
transcriptWarnings,
|
|
247
|
-
recordedMessages,
|
|
248
|
-
resultCallIds,
|
|
249
|
-
hasValidFalsePositiveProof,
|
|
250
|
-
lastInteractionOrder,
|
|
251
|
-
};
|
|
252
|
-
};
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
firstDiagnosticTextContent,
|
|
3
|
-
isDiagnosticRecord,
|
|
4
|
-
} from './diagnostic-text.ts';
|
|
5
|
-
import type {
|
|
6
|
-
RecordedMessage,
|
|
7
|
-
RecordedToolSuccess,
|
|
8
|
-
} from './diagnostic-types.ts';
|
|
9
|
-
|
|
10
|
-
const HIDDEN_BASH_FATAL_PATTERNS = [
|
|
11
|
-
/command not found/i,
|
|
12
|
-
/permission denied/i,
|
|
13
|
-
/no such file or directory/i,
|
|
14
|
-
/segmentation fault/i,
|
|
15
|
-
/killed|terminated/i,
|
|
16
|
-
/out of memory/i,
|
|
17
|
-
/connection refused/i,
|
|
18
|
-
/timeout/i,
|
|
19
|
-
] as const;
|
|
20
|
-
const HIDDEN_BASH_EXIT_PATTERN =
|
|
21
|
-
/exit(?:ed)?\s*(?:with\s*)?(?:code|status)?\s*[:\s]?\s*(\d+)/i;
|
|
22
|
-
|
|
23
|
-
export type HiddenBashFalsePositive = {
|
|
24
|
-
readonly result: RecordedToolSuccess;
|
|
25
|
-
readonly terminalError: string;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const lastAssistantTextIndex = (
|
|
29
|
-
messages: ReadonlyArray<RecordedMessage>,
|
|
30
|
-
): number => {
|
|
31
|
-
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
32
|
-
const message = messages[index]?.value;
|
|
33
|
-
const hasAssistantText =
|
|
34
|
-
message?.role === 'assistant' &&
|
|
35
|
-
Array.isArray(message.content) &&
|
|
36
|
-
message.content.some(
|
|
37
|
-
(item) =>
|
|
38
|
-
isDiagnosticRecord(item) &&
|
|
39
|
-
item.type === 'text' &&
|
|
40
|
-
typeof item.text === 'string' &&
|
|
41
|
-
item.text.trim().length > 0,
|
|
42
|
-
);
|
|
43
|
-
if (hasAssistantText) return index;
|
|
44
|
-
}
|
|
45
|
-
return -1;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const hiddenExitCode = (output: string): number | undefined => {
|
|
49
|
-
const exitCodeText = output.match(HIDDEN_BASH_EXIT_PATTERN)?.[1];
|
|
50
|
-
const exitCode =
|
|
51
|
-
exitCodeText === undefined ? undefined : Number.parseInt(exitCodeText, 10);
|
|
52
|
-
if (exitCode !== undefined && exitCode !== 0) return exitCode;
|
|
53
|
-
return HIDDEN_BASH_FATAL_PATTERNS.some((pattern) => pattern.test(output))
|
|
54
|
-
? 1
|
|
55
|
-
: undefined;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Reproduces pi-subagents' hidden Bash failure detection from successful tool
|
|
60
|
-
* output.
|
|
61
|
-
*/
|
|
62
|
-
export const reproduceHiddenBashFalsePositive = (
|
|
63
|
-
messages: ReadonlyArray<RecordedMessage>,
|
|
64
|
-
successfulResults: ReadonlyArray<RecordedToolSuccess>,
|
|
65
|
-
): HiddenBashFalsePositive | undefined => {
|
|
66
|
-
const assistantTextIndex = lastAssistantTextIndex(messages);
|
|
67
|
-
const scanStart = assistantTextIndex >= 0 ? assistantTextIndex + 1 : 0;
|
|
68
|
-
|
|
69
|
-
for (let index = messages.length - 1; index >= scanStart; index -= 1) {
|
|
70
|
-
const recordedMessage = messages[index];
|
|
71
|
-
const message = recordedMessage?.value;
|
|
72
|
-
if (
|
|
73
|
-
!recordedMessage ||
|
|
74
|
-
message?.role !== 'toolResult' ||
|
|
75
|
-
message.toolName !== 'bash' ||
|
|
76
|
-
message.isError !== false
|
|
77
|
-
) {
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
const output = firstDiagnosticTextContent(message.content);
|
|
81
|
-
if (output === undefined) continue;
|
|
82
|
-
|
|
83
|
-
const detectedExitCode = hiddenExitCode(output);
|
|
84
|
-
if (detectedExitCode === undefined) continue;
|
|
85
|
-
const result = successfulResults.find(
|
|
86
|
-
(candidate) =>
|
|
87
|
-
candidate.order === recordedMessage.order &&
|
|
88
|
-
candidate.tool === 'bash' &&
|
|
89
|
-
candidate.detectorOutput === output,
|
|
90
|
-
);
|
|
91
|
-
if (!result) return undefined;
|
|
92
|
-
return {
|
|
93
|
-
result,
|
|
94
|
-
terminalError: `bash failed (exit ${detectedExitCode}): ${output.slice(0, 200)}`,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
return undefined;
|
|
98
|
-
};
|