@wichayutdew/pi-workflows 2.5.0 → 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 -251
- 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,288 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DEFAULT_STEP_SUBAGENT,
|
|
3
|
-
SUBAGENT_RUNTIME_NAME_PATTERN,
|
|
4
|
-
type StepSubagent,
|
|
5
|
-
type SubagentContext,
|
|
6
|
-
type SubagentPermissionCeiling,
|
|
7
|
-
type SubagentToolBudget,
|
|
8
|
-
type SubagentTurnBudget,
|
|
9
|
-
} from '../types.ts';
|
|
10
|
-
import {
|
|
11
|
-
isJsonObject,
|
|
12
|
-
readBoolean,
|
|
13
|
-
readInteger,
|
|
14
|
-
readString,
|
|
15
|
-
readStringList,
|
|
16
|
-
rejectUnknownKeys,
|
|
17
|
-
RESOURCE_SELECTOR_PATTERN,
|
|
18
|
-
TOOL_PATTERN,
|
|
19
|
-
type ValidationErrors,
|
|
20
|
-
} from './shared.ts';
|
|
21
|
-
|
|
22
|
-
function parseSubagentTurnBudget(
|
|
23
|
-
value: unknown,
|
|
24
|
-
path: string,
|
|
25
|
-
errors: ValidationErrors,
|
|
26
|
-
): SubagentTurnBudget | undefined {
|
|
27
|
-
if (value === undefined) return undefined;
|
|
28
|
-
if (!isJsonObject(value)) {
|
|
29
|
-
errors.push(`${path}: expected an object`);
|
|
30
|
-
return undefined;
|
|
31
|
-
}
|
|
32
|
-
rejectUnknownKeys(value, ['maxTurns', 'graceTurns'], path, errors);
|
|
33
|
-
const maxTurns = readInteger(value.maxTurns, 0, `${path}.maxTurns`, errors, {
|
|
34
|
-
min: 1,
|
|
35
|
-
max: 1_000,
|
|
36
|
-
});
|
|
37
|
-
const graceTurns =
|
|
38
|
-
value.graceTurns === undefined
|
|
39
|
-
? undefined
|
|
40
|
-
: readInteger(value.graceTurns, 0, `${path}.graceTurns`, errors, {
|
|
41
|
-
min: 0,
|
|
42
|
-
max: 100,
|
|
43
|
-
});
|
|
44
|
-
if (maxTurns === 0) return undefined;
|
|
45
|
-
return {
|
|
46
|
-
maxTurns,
|
|
47
|
-
...(graceTurns !== undefined ? { graceTurns } : {}),
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function parseSubagentToolBudget(
|
|
52
|
-
value: unknown,
|
|
53
|
-
path: string,
|
|
54
|
-
errors: ValidationErrors,
|
|
55
|
-
): SubagentToolBudget | undefined {
|
|
56
|
-
if (value === undefined) return undefined;
|
|
57
|
-
if (!isJsonObject(value)) {
|
|
58
|
-
errors.push(`${path}: expected an object`);
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
61
|
-
rejectUnknownKeys(value, ['soft', 'hard', 'block'], path, errors);
|
|
62
|
-
const hard = readInteger(value.hard, 0, `${path}.hard`, errors, {
|
|
63
|
-
min: 1,
|
|
64
|
-
max: 100_000,
|
|
65
|
-
});
|
|
66
|
-
const soft =
|
|
67
|
-
value.soft === undefined
|
|
68
|
-
? undefined
|
|
69
|
-
: readInteger(value.soft, 0, `${path}.soft`, errors, {
|
|
70
|
-
min: 1,
|
|
71
|
-
max: 100_000,
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
let block: Array<string> | '*' | undefined;
|
|
75
|
-
if (value.block === '*') {
|
|
76
|
-
block = '*';
|
|
77
|
-
} else if (value.block !== undefined) {
|
|
78
|
-
block = readStringList(value.block, `${path}.block`, errors, TOOL_PATTERN);
|
|
79
|
-
if (block.length === 0) {
|
|
80
|
-
errors.push(`${path}.block: expected "*" or at least one tool name`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
if (soft !== undefined && hard > 0 && soft > hard) {
|
|
84
|
-
errors.push(`${path}.soft: must not exceed hard`);
|
|
85
|
-
}
|
|
86
|
-
if (hard === 0) return undefined;
|
|
87
|
-
return {
|
|
88
|
-
hard,
|
|
89
|
-
...(soft !== undefined ? { soft } : {}),
|
|
90
|
-
...(block !== undefined ? { block } : {}),
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Parse an optional isolated-step subagent configuration. */
|
|
95
|
-
export function parseStepSubagent(
|
|
96
|
-
value: unknown,
|
|
97
|
-
path: string,
|
|
98
|
-
errors: ValidationErrors,
|
|
99
|
-
): StepSubagent | undefined {
|
|
100
|
-
if (value === undefined) return undefined;
|
|
101
|
-
if (typeof value === 'string') {
|
|
102
|
-
const agent =
|
|
103
|
-
readString(value, path, errors, {
|
|
104
|
-
pattern: SUBAGENT_RUNTIME_NAME_PATTERN,
|
|
105
|
-
}) ?? DEFAULT_STEP_SUBAGENT.agent;
|
|
106
|
-
return { ...DEFAULT_STEP_SUBAGENT, agent };
|
|
107
|
-
}
|
|
108
|
-
if (!isJsonObject(value)) {
|
|
109
|
-
errors.push(`${path}: expected an agent profile name or object`);
|
|
110
|
-
return undefined;
|
|
111
|
-
}
|
|
112
|
-
rejectUnknownKeys(
|
|
113
|
-
value,
|
|
114
|
-
[
|
|
115
|
-
'agent',
|
|
116
|
-
'context',
|
|
117
|
-
'model',
|
|
118
|
-
'timeoutMs',
|
|
119
|
-
'turnBudget',
|
|
120
|
-
'toolBudget',
|
|
121
|
-
'artifacts',
|
|
122
|
-
'retryToolFailures',
|
|
123
|
-
],
|
|
124
|
-
path,
|
|
125
|
-
errors,
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
const agent =
|
|
129
|
-
value.agent === undefined
|
|
130
|
-
? DEFAULT_STEP_SUBAGENT.agent
|
|
131
|
-
: (readString(value.agent, `${path}.agent`, errors, {
|
|
132
|
-
pattern: SUBAGENT_RUNTIME_NAME_PATTERN,
|
|
133
|
-
}) ?? DEFAULT_STEP_SUBAGENT.agent);
|
|
134
|
-
const contextValue =
|
|
135
|
-
value.context === undefined
|
|
136
|
-
? DEFAULT_STEP_SUBAGENT.context
|
|
137
|
-
: readString(value.context, `${path}.context`, errors);
|
|
138
|
-
const context: SubagentContext = DEFAULT_STEP_SUBAGENT.context;
|
|
139
|
-
if (contextValue !== 'fresh') {
|
|
140
|
-
errors.push(`${path}.context: expected fresh`);
|
|
141
|
-
}
|
|
142
|
-
const model =
|
|
143
|
-
value.model === undefined
|
|
144
|
-
? undefined
|
|
145
|
-
: readString(value.model, `${path}.model`, errors, {
|
|
146
|
-
pattern: RESOURCE_SELECTOR_PATTERN,
|
|
147
|
-
});
|
|
148
|
-
const timeoutMs = readInteger(
|
|
149
|
-
value.timeoutMs,
|
|
150
|
-
DEFAULT_STEP_SUBAGENT.timeoutMs,
|
|
151
|
-
`${path}.timeoutMs`,
|
|
152
|
-
errors,
|
|
153
|
-
{ min: 1_000, max: 86_400_000 },
|
|
154
|
-
);
|
|
155
|
-
const turnBudget = parseSubagentTurnBudget(
|
|
156
|
-
value.turnBudget,
|
|
157
|
-
`${path}.turnBudget`,
|
|
158
|
-
errors,
|
|
159
|
-
);
|
|
160
|
-
const toolBudget = parseSubagentToolBudget(
|
|
161
|
-
value.toolBudget,
|
|
162
|
-
`${path}.toolBudget`,
|
|
163
|
-
errors,
|
|
164
|
-
);
|
|
165
|
-
const artifacts = readBoolean(
|
|
166
|
-
value.artifacts,
|
|
167
|
-
DEFAULT_STEP_SUBAGENT.artifacts,
|
|
168
|
-
`${path}.artifacts`,
|
|
169
|
-
errors,
|
|
170
|
-
);
|
|
171
|
-
const retryToolFailures = readBoolean(
|
|
172
|
-
value.retryToolFailures,
|
|
173
|
-
DEFAULT_STEP_SUBAGENT.retryToolFailures,
|
|
174
|
-
`${path}.retryToolFailures`,
|
|
175
|
-
errors,
|
|
176
|
-
);
|
|
177
|
-
return {
|
|
178
|
-
agent,
|
|
179
|
-
context,
|
|
180
|
-
...(model ? { model } : {}),
|
|
181
|
-
timeoutMs,
|
|
182
|
-
...(turnBudget ? { turnBudget } : {}),
|
|
183
|
-
...(toolBudget ? { toolBudget } : {}),
|
|
184
|
-
artifacts,
|
|
185
|
-
retryToolFailures,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** Parse the subagent limits inside the user permission ceiling. */
|
|
190
|
-
export function parseSubagentPermissionCeiling(
|
|
191
|
-
value: unknown,
|
|
192
|
-
path: string,
|
|
193
|
-
errors: ValidationErrors,
|
|
194
|
-
): SubagentPermissionCeiling | undefined {
|
|
195
|
-
if (!isJsonObject(value)) {
|
|
196
|
-
errors.push(`${path}: expected an object`);
|
|
197
|
-
return undefined;
|
|
198
|
-
}
|
|
199
|
-
rejectUnknownKeys(
|
|
200
|
-
value,
|
|
201
|
-
[
|
|
202
|
-
'agents',
|
|
203
|
-
'contexts',
|
|
204
|
-
'models',
|
|
205
|
-
'maxTimeoutMs',
|
|
206
|
-
'maxTurns',
|
|
207
|
-
'maxGraceTurns',
|
|
208
|
-
'maxToolCalls',
|
|
209
|
-
'artifacts',
|
|
210
|
-
'retryToolFailures',
|
|
211
|
-
],
|
|
212
|
-
path,
|
|
213
|
-
errors,
|
|
214
|
-
);
|
|
215
|
-
const agents = readStringList(
|
|
216
|
-
value.agents,
|
|
217
|
-
`${path}.agents`,
|
|
218
|
-
errors,
|
|
219
|
-
SUBAGENT_RUNTIME_NAME_PATTERN,
|
|
220
|
-
);
|
|
221
|
-
const contexts = readStringList(
|
|
222
|
-
value.contexts,
|
|
223
|
-
`${path}.contexts`,
|
|
224
|
-
errors,
|
|
225
|
-
/^fresh$/,
|
|
226
|
-
).filter((context): context is SubagentContext => context === 'fresh');
|
|
227
|
-
const models = readStringList(
|
|
228
|
-
value.models,
|
|
229
|
-
`${path}.models`,
|
|
230
|
-
errors,
|
|
231
|
-
RESOURCE_SELECTOR_PATTERN,
|
|
232
|
-
);
|
|
233
|
-
if (agents.length === 0) {
|
|
234
|
-
errors.push(`${path}.agents: at least one subagent is required`);
|
|
235
|
-
}
|
|
236
|
-
if (contexts.length === 0) {
|
|
237
|
-
errors.push(`${path}.contexts: at least one context mode is required`);
|
|
238
|
-
}
|
|
239
|
-
(
|
|
240
|
-
[
|
|
241
|
-
'maxTimeoutMs',
|
|
242
|
-
'maxTurns',
|
|
243
|
-
'maxGraceTurns',
|
|
244
|
-
'maxToolCalls',
|
|
245
|
-
'artifacts',
|
|
246
|
-
] as const
|
|
247
|
-
)
|
|
248
|
-
.filter((field) => value[field] === undefined)
|
|
249
|
-
.forEach((field) => errors.push(`${path}.${field}: required`));
|
|
250
|
-
|
|
251
|
-
return {
|
|
252
|
-
agents,
|
|
253
|
-
contexts,
|
|
254
|
-
models,
|
|
255
|
-
maxTimeoutMs: readInteger(
|
|
256
|
-
value.maxTimeoutMs,
|
|
257
|
-
0,
|
|
258
|
-
`${path}.maxTimeoutMs`,
|
|
259
|
-
errors,
|
|
260
|
-
{ min: 1_000, max: 86_400_000 },
|
|
261
|
-
),
|
|
262
|
-
maxTurns: readInteger(value.maxTurns, 0, `${path}.maxTurns`, errors, {
|
|
263
|
-
min: 1,
|
|
264
|
-
max: 1_000,
|
|
265
|
-
}),
|
|
266
|
-
maxGraceTurns: readInteger(
|
|
267
|
-
value.maxGraceTurns,
|
|
268
|
-
0,
|
|
269
|
-
`${path}.maxGraceTurns`,
|
|
270
|
-
errors,
|
|
271
|
-
{ min: 0, max: 100 },
|
|
272
|
-
),
|
|
273
|
-
maxToolCalls: readInteger(
|
|
274
|
-
value.maxToolCalls,
|
|
275
|
-
0,
|
|
276
|
-
`${path}.maxToolCalls`,
|
|
277
|
-
errors,
|
|
278
|
-
{ min: 1, max: 100_000 },
|
|
279
|
-
),
|
|
280
|
-
artifacts: readBoolean(value.artifacts, false, `${path}.artifacts`, errors),
|
|
281
|
-
retryToolFailures: readBoolean(
|
|
282
|
-
value.retryToolFailures,
|
|
283
|
-
false,
|
|
284
|
-
`${path}.retryToolFailures`,
|
|
285
|
-
errors,
|
|
286
|
-
),
|
|
287
|
-
};
|
|
288
|
-
}
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
import type { WorkflowStepResult } from '../runtime/step-result.ts';
|
|
2
|
-
import {
|
|
3
|
-
failedToolName,
|
|
4
|
-
formatToolFailureDiagnostic,
|
|
5
|
-
type CompletedDelegationTranscriptAudit,
|
|
6
|
-
} from '../integrations/subagents/diagnostics.ts';
|
|
7
|
-
import type {
|
|
8
|
-
ChildStepPolicy,
|
|
9
|
-
SubagentDelegationResponse,
|
|
10
|
-
} from '../integrations/subagents/protocol.ts';
|
|
11
|
-
import type { WorkflowHarnessDependencies } from './dependencies.ts';
|
|
12
|
-
import {
|
|
13
|
-
completionMatchesResult,
|
|
14
|
-
recoveredProjectionError,
|
|
15
|
-
} from './delegation-recovery-validation.ts';
|
|
16
|
-
import {
|
|
17
|
-
boundedFailureField,
|
|
18
|
-
delegationFailureFingerprint,
|
|
19
|
-
isRetryableTerminalFailure,
|
|
20
|
-
isSafeToRetryDelegation,
|
|
21
|
-
rejectedRecoveryReason,
|
|
22
|
-
} from './delegation-retry-policy.ts';
|
|
23
|
-
import type {
|
|
24
|
-
ActiveDelegation,
|
|
25
|
-
DelegationFailureDetails,
|
|
26
|
-
DelegationRecoveryBlocker,
|
|
27
|
-
} from './types.ts';
|
|
28
|
-
|
|
29
|
-
export type DelegationFailureActions = {
|
|
30
|
-
delegationFailureFingerprint: (failure: DelegationFailureDetails) => string;
|
|
31
|
-
hasContradictoryCompletion: (response: SubagentDelegationResponse) => boolean;
|
|
32
|
-
completedResponseAudit: (
|
|
33
|
-
active: ActiveDelegation,
|
|
34
|
-
response: SubagentDelegationResponse,
|
|
35
|
-
) => Promise<CompletedDelegationTranscriptAudit>;
|
|
36
|
-
isRetryableTerminalFailure: (failure: DelegationFailureDetails) => boolean;
|
|
37
|
-
isSafeToRetryDelegation: (
|
|
38
|
-
policy: ChildStepPolicy,
|
|
39
|
-
isReplayExplicitlyAuthorized: boolean,
|
|
40
|
-
replayAudit: DelegationFailureDetails['replayAudit'],
|
|
41
|
-
) => boolean;
|
|
42
|
-
rejectedRecoveryReason: (
|
|
43
|
-
failure: DelegationFailureDetails,
|
|
44
|
-
error: unknown,
|
|
45
|
-
) => string;
|
|
46
|
-
completionMatchesResult: (
|
|
47
|
-
diagnostic: NonNullable<DelegationFailureDetails['diagnostic']>,
|
|
48
|
-
result: WorkflowStepResult,
|
|
49
|
-
policy: ChildStepPolicy,
|
|
50
|
-
) => boolean;
|
|
51
|
-
recoveredProjectionError: (
|
|
52
|
-
active: ActiveDelegation,
|
|
53
|
-
response: SubagentDelegationResponse,
|
|
54
|
-
diagnostic: NonNullable<DelegationFailureDetails['diagnostic']>,
|
|
55
|
-
) => string | undefined;
|
|
56
|
-
describeDelegationFailure: (
|
|
57
|
-
active: ActiveDelegation,
|
|
58
|
-
response: SubagentDelegationResponse,
|
|
59
|
-
) => Promise<DelegationFailureDetails>;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
function nonEmptyTerminalError(
|
|
63
|
-
response: SubagentDelegationResponse,
|
|
64
|
-
): string | undefined {
|
|
65
|
-
return [response.error, response.execution?.error].find(
|
|
66
|
-
(error): error is string =>
|
|
67
|
-
typeof error === 'string' && error.trim().length > 0,
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function nonzeroTerminalExitCode(
|
|
72
|
-
response: SubagentDelegationResponse,
|
|
73
|
-
): number | undefined {
|
|
74
|
-
return [response.exitCode, response.execution?.exitCode].find(
|
|
75
|
-
(exitCode): exitCode is number =>
|
|
76
|
-
typeof exitCode === 'number' &&
|
|
77
|
-
Number.isSafeInteger(exitCode) &&
|
|
78
|
-
exitCode !== 0,
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function hasContradictoryCompletion(
|
|
83
|
-
response: SubagentDelegationResponse,
|
|
84
|
-
): boolean {
|
|
85
|
-
return (
|
|
86
|
-
response.status === 'completed' &&
|
|
87
|
-
(nonEmptyTerminalError(response) !== undefined ||
|
|
88
|
-
nonzeroTerminalExitCode(response) !== undefined)
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function recoveryBlocker(
|
|
93
|
-
response: SubagentDelegationResponse,
|
|
94
|
-
): DelegationRecoveryBlocker | undefined {
|
|
95
|
-
const execution = response.execution;
|
|
96
|
-
const fileMutation = response.effects?.fileMutation;
|
|
97
|
-
if (fileMutation?.attempted === true || fileMutation?.status === 'observed') {
|
|
98
|
-
return 'reported-mutation';
|
|
99
|
-
}
|
|
100
|
-
if (execution?.detached === true || execution?.status === 'detached') {
|
|
101
|
-
return 'detached';
|
|
102
|
-
}
|
|
103
|
-
if (execution?.stopped === true || execution?.status === 'stopped') {
|
|
104
|
-
return 'stopped';
|
|
105
|
-
}
|
|
106
|
-
if (
|
|
107
|
-
response.status === 'interrupted' ||
|
|
108
|
-
execution?.interrupted === true ||
|
|
109
|
-
execution?.status === 'paused'
|
|
110
|
-
) {
|
|
111
|
-
return 'interrupted';
|
|
112
|
-
}
|
|
113
|
-
if (response.status === 'cancelled') return 'cancelled';
|
|
114
|
-
if (execution?.timedOut === true && response.status !== 'timed_out') {
|
|
115
|
-
return 'inconsistent-timeout';
|
|
116
|
-
}
|
|
117
|
-
return undefined;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function validateReplayAudit(
|
|
121
|
-
response: SubagentDelegationResponse,
|
|
122
|
-
replayAudit: DelegationFailureDetails['replayAudit'],
|
|
123
|
-
): DelegationFailureDetails['replayAudit'] {
|
|
124
|
-
if (!replayAudit) return undefined;
|
|
125
|
-
if (
|
|
126
|
-
response.toolCount !== undefined &&
|
|
127
|
-
response.toolCount !== replayAudit.toolCount
|
|
128
|
-
) {
|
|
129
|
-
return { ...replayAudit, replaySafe: false };
|
|
130
|
-
}
|
|
131
|
-
return replayAudit;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Creates pure delegation-failure evaluators plus the injected diagnostic
|
|
136
|
-
* reader used at the transcript I/O boundary.
|
|
137
|
-
*/
|
|
138
|
-
export function createDelegationFailureActions(
|
|
139
|
-
dependencies: Pick<
|
|
140
|
-
WorkflowHarnessDependencies,
|
|
141
|
-
'readDelegationReplayAudit' | 'readToolFailureDiagnostic'
|
|
142
|
-
> &
|
|
143
|
-
Partial<
|
|
144
|
-
Pick<WorkflowHarnessDependencies, 'auditCompletedDelegationTranscript'>
|
|
145
|
-
>,
|
|
146
|
-
): DelegationFailureActions {
|
|
147
|
-
const responseIdentity = (
|
|
148
|
-
active: ActiveDelegation,
|
|
149
|
-
response: SubagentDelegationResponse,
|
|
150
|
-
): { readonly runId: string; readonly childIndex: 0 } | undefined => {
|
|
151
|
-
if (
|
|
152
|
-
response.childIndex !== 0 ||
|
|
153
|
-
typeof response.runId !== 'string' ||
|
|
154
|
-
(response.agent !== undefined && response.agent !== active.agent)
|
|
155
|
-
) {
|
|
156
|
-
return undefined;
|
|
157
|
-
}
|
|
158
|
-
return { runId: response.runId, childIndex: 0 };
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
const completedResponseAudit = async (
|
|
162
|
-
active: ActiveDelegation,
|
|
163
|
-
response: SubagentDelegationResponse,
|
|
164
|
-
): Promise<CompletedDelegationTranscriptAudit> => {
|
|
165
|
-
if (!dependencies.auditCompletedDelegationTranscript) {
|
|
166
|
-
return {
|
|
167
|
-
verified: false,
|
|
168
|
-
reason: 'completed transcript auditing is unavailable',
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
return dependencies.auditCompletedDelegationTranscript(
|
|
172
|
-
response.sessionFile,
|
|
173
|
-
active.trustedSessionRoot,
|
|
174
|
-
responseIdentity(active, response),
|
|
175
|
-
);
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const describeDelegationFailure = async (
|
|
179
|
-
active: ActiveDelegation,
|
|
180
|
-
response: SubagentDelegationResponse,
|
|
181
|
-
): Promise<DelegationFailureDetails> => {
|
|
182
|
-
const blocker = recoveryBlocker(response);
|
|
183
|
-
const terminalError = nonEmptyTerminalError(response);
|
|
184
|
-
const error =
|
|
185
|
-
terminalError ?? 'The subagent returned no terminal error details.';
|
|
186
|
-
const identity = responseIdentity(active, response);
|
|
187
|
-
const [diagnostic, replayAudit] = await Promise.all([
|
|
188
|
-
dependencies.readToolFailureDiagnostic(
|
|
189
|
-
response.sessionFile,
|
|
190
|
-
active.trustedSessionRoot,
|
|
191
|
-
identity,
|
|
192
|
-
failedToolName(terminalError),
|
|
193
|
-
terminalError,
|
|
194
|
-
),
|
|
195
|
-
dependencies.readDelegationReplayAudit(
|
|
196
|
-
response.sessionFile,
|
|
197
|
-
active.trustedSessionRoot,
|
|
198
|
-
identity,
|
|
199
|
-
{
|
|
200
|
-
task: active.transcriptTask,
|
|
201
|
-
bashPermission: active.policy.permissions.bash,
|
|
202
|
-
},
|
|
203
|
-
),
|
|
204
|
-
]);
|
|
205
|
-
const validatedReplayAudit = validateReplayAudit(response, replayAudit);
|
|
206
|
-
const exitCode =
|
|
207
|
-
nonzeroTerminalExitCode(response) ??
|
|
208
|
-
response.exitCode ??
|
|
209
|
-
response.execution?.exitCode;
|
|
210
|
-
const reason = [
|
|
211
|
-
hasContradictoryCompletion(response)
|
|
212
|
-
? `Subagent "${active.agent}" reported terminal failure signals with completed status.`
|
|
213
|
-
: `Subagent "${active.agent}" ${response.status.replaceAll('_', ' ')}.`,
|
|
214
|
-
...(diagnostic ? formatToolFailureDiagnostic(diagnostic) : []),
|
|
215
|
-
...(exitCode !== undefined ? [`Subagent exit code: ${exitCode}`] : []),
|
|
216
|
-
...(blocker
|
|
217
|
-
? [`Automatic recovery blocked by: ${blocker.replaceAll('-', ' ')}`]
|
|
218
|
-
: []),
|
|
219
|
-
`Terminal error: ${boundedFailureField(error)}`,
|
|
220
|
-
...(diagnostic && response.sessionFile
|
|
221
|
-
? [
|
|
222
|
-
`Diagnostic session: ${boundedFailureField(response.sessionFile.replaceAll(/\s+/g, ' '))}`,
|
|
223
|
-
]
|
|
224
|
-
: []),
|
|
225
|
-
].join('\n');
|
|
226
|
-
return {
|
|
227
|
-
reason,
|
|
228
|
-
status: response.status,
|
|
229
|
-
...(terminalError ? { error: terminalError } : {}),
|
|
230
|
-
...(exitCode !== undefined ? { exitCode } : {}),
|
|
231
|
-
...(blocker ? { recoveryBlocker: blocker } : {}),
|
|
232
|
-
...(diagnostic ? { diagnostic } : {}),
|
|
233
|
-
...(validatedReplayAudit ? { replayAudit: validatedReplayAudit } : {}),
|
|
234
|
-
};
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
return {
|
|
238
|
-
delegationFailureFingerprint,
|
|
239
|
-
hasContradictoryCompletion,
|
|
240
|
-
completedResponseAudit,
|
|
241
|
-
isRetryableTerminalFailure,
|
|
242
|
-
isSafeToRetryDelegation,
|
|
243
|
-
rejectedRecoveryReason,
|
|
244
|
-
completionMatchesResult,
|
|
245
|
-
recoveredProjectionError,
|
|
246
|
-
describeDelegationFailure,
|
|
247
|
-
};
|
|
248
|
-
}
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { parseDelegatedStepResult } from '../integrations/subagents/protocol.ts';
|
|
2
|
-
import type {
|
|
3
|
-
ChildStepPolicy,
|
|
4
|
-
SubagentDelegationResponse,
|
|
5
|
-
} from '../integrations/subagents/protocol.ts';
|
|
6
|
-
import type { WorkflowStepResult } from '../runtime/step-result.ts';
|
|
7
|
-
import type { ActiveDelegation, DelegationFailureDetails } from './types.ts';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Verifies that transcript completion data exactly matches a recovered result.
|
|
11
|
-
*
|
|
12
|
-
* @param diagnostic - Correlated tool-failure diagnostic.
|
|
13
|
-
* @param result - Result recovered from the delegated artifact.
|
|
14
|
-
* @param policy - Child policy used to validate the result.
|
|
15
|
-
* @returns Whether the transcript and recovered artifact agree.
|
|
16
|
-
*/
|
|
17
|
-
export function completionMatchesResult(
|
|
18
|
-
diagnostic: NonNullable<DelegationFailureDetails['diagnostic']>,
|
|
19
|
-
result: WorkflowStepResult,
|
|
20
|
-
policy: ChildStepPolicy,
|
|
21
|
-
): boolean {
|
|
22
|
-
const value = diagnostic.completionValue;
|
|
23
|
-
if (!value) return false;
|
|
24
|
-
const expectedKeys = [
|
|
25
|
-
'outcome',
|
|
26
|
-
'summary',
|
|
27
|
-
...(result.artifact === undefined ? [] : ['artifact']),
|
|
28
|
-
...(result.workspace === undefined ? [] : ['workspace']),
|
|
29
|
-
].sort();
|
|
30
|
-
const actualKeys = Object.keys(value).sort();
|
|
31
|
-
if (
|
|
32
|
-
actualKeys.length !== expectedKeys.length ||
|
|
33
|
-
!actualKeys.every((key, index) => key === expectedKeys[index])
|
|
34
|
-
) {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
try {
|
|
38
|
-
const completion = parseDelegatedStepResult(
|
|
39
|
-
{
|
|
40
|
-
...value,
|
|
41
|
-
version: 1,
|
|
42
|
-
policyDigest: policy.policyDigest,
|
|
43
|
-
},
|
|
44
|
-
policy,
|
|
45
|
-
);
|
|
46
|
-
return (
|
|
47
|
-
completion.outcome === result.outcome &&
|
|
48
|
-
completion.summary === result.summary &&
|
|
49
|
-
completion.artifact === result.artifact &&
|
|
50
|
-
completion.workspace?.cwd === result.workspace?.cwd
|
|
51
|
-
);
|
|
52
|
-
} catch {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Finds contradictory or unsafe fields in a recovered terminal projection.
|
|
59
|
-
*
|
|
60
|
-
* @param active - Active delegation identity.
|
|
61
|
-
* @param response - Terminal response received from the child.
|
|
62
|
-
* @param diagnostic - Transcript evidence correlated to the failure.
|
|
63
|
-
* @returns A rejection reason, or undefined for a valid projection.
|
|
64
|
-
*/
|
|
65
|
-
export function recoveredProjectionError(
|
|
66
|
-
active: ActiveDelegation,
|
|
67
|
-
response: SubagentDelegationResponse,
|
|
68
|
-
diagnostic: NonNullable<DelegationFailureDetails['diagnostic']>,
|
|
69
|
-
): string | undefined {
|
|
70
|
-
if (response.agent !== active.agent) {
|
|
71
|
-
return `terminal agent identity is ${JSON.stringify(response.agent)}; expected ${JSON.stringify(active.agent)}`;
|
|
72
|
-
}
|
|
73
|
-
if (response.childIndex !== 0) {
|
|
74
|
-
return `terminal child index is ${JSON.stringify(response.childIndex)}; expected 0`;
|
|
75
|
-
}
|
|
76
|
-
if (
|
|
77
|
-
typeof response.exitCode !== 'number' ||
|
|
78
|
-
!Number.isSafeInteger(response.exitCode) ||
|
|
79
|
-
response.exitCode <= 0
|
|
80
|
-
) {
|
|
81
|
-
return `terminal exit code is ${JSON.stringify(response.exitCode)}; expected a positive safe integer`;
|
|
82
|
-
}
|
|
83
|
-
const execution = response.execution;
|
|
84
|
-
if (!execution) return 'terminal response has no execution projection';
|
|
85
|
-
if (execution.status !== 'failed' || execution.success) {
|
|
86
|
-
return `execution projection is ${JSON.stringify({
|
|
87
|
-
status: execution.status,
|
|
88
|
-
success: execution.success,
|
|
89
|
-
})}; expected failed/false`;
|
|
90
|
-
}
|
|
91
|
-
if (execution.exitCode !== response.exitCode) {
|
|
92
|
-
return `execution exit code ${JSON.stringify(execution.exitCode)} does not match terminal exit code ${JSON.stringify(response.exitCode)}`;
|
|
93
|
-
}
|
|
94
|
-
if (
|
|
95
|
-
typeof response.error !== 'string' ||
|
|
96
|
-
!response.error ||
|
|
97
|
-
typeof execution.error !== 'string' ||
|
|
98
|
-
execution.error !== response.error
|
|
99
|
-
) {
|
|
100
|
-
return 'terminal and execution errors are missing or do not match exactly';
|
|
101
|
-
}
|
|
102
|
-
const warnings = response.warnings as unknown;
|
|
103
|
-
if (
|
|
104
|
-
warnings !== undefined &&
|
|
105
|
-
(!Array.isArray(warnings) ||
|
|
106
|
-
warnings.some(
|
|
107
|
-
(warning) => typeof warning !== 'string' || warning.trim().length > 0,
|
|
108
|
-
))
|
|
109
|
-
) {
|
|
110
|
-
return `terminal response contains warning evidence: ${JSON.stringify(warnings)}`;
|
|
111
|
-
}
|
|
112
|
-
if (
|
|
113
|
-
diagnostic.transcriptToolCount !== undefined &&
|
|
114
|
-
response.toolCount !== undefined &&
|
|
115
|
-
response.toolCount !== diagnostic.transcriptToolCount
|
|
116
|
-
) {
|
|
117
|
-
return `terminal tool count ${response.toolCount} does not match transcript tool count ${diagnostic.transcriptToolCount}`;
|
|
118
|
-
}
|
|
119
|
-
if (
|
|
120
|
-
diagnostic.transcriptTurnCount !== undefined &&
|
|
121
|
-
response.turns !== undefined &&
|
|
122
|
-
response.turns !== diagnostic.transcriptTurnCount
|
|
123
|
-
) {
|
|
124
|
-
return `terminal turn count ${response.turns} does not match transcript turn count ${diagnostic.transcriptTurnCount}`;
|
|
125
|
-
}
|
|
126
|
-
const toolFailure = response.error.match(
|
|
127
|
-
/^\s*([a-z][\w-]*) failed\s*\(exit\s+(\d+)\)\s*:/i,
|
|
128
|
-
);
|
|
129
|
-
if (!toolFailure) {
|
|
130
|
-
return 'terminal error is not a recognized "<tool> failed (exit N): <detail>" failure';
|
|
131
|
-
}
|
|
132
|
-
const terminalTool = toolFailure[1];
|
|
133
|
-
const terminalExitCodeText = toolFailure[2];
|
|
134
|
-
if (!terminalTool || !terminalExitCodeText) {
|
|
135
|
-
return 'terminal error does not contain a complete tool failure projection';
|
|
136
|
-
}
|
|
137
|
-
const terminalExitCode = Number(terminalExitCodeText);
|
|
138
|
-
if (
|
|
139
|
-
terminalTool.toLowerCase() !== diagnostic.tool.toLowerCase() ||
|
|
140
|
-
terminalExitCode !== response.exitCode
|
|
141
|
-
) {
|
|
142
|
-
return `terminal tool/exit ${JSON.stringify({
|
|
143
|
-
tool: terminalTool,
|
|
144
|
-
exitCode: terminalExitCode,
|
|
145
|
-
})} does not match the correlated failure ${JSON.stringify({
|
|
146
|
-
tool: diagnostic.tool,
|
|
147
|
-
exitCode: response.exitCode,
|
|
148
|
-
})}`;
|
|
149
|
-
}
|
|
150
|
-
const unsafeFlag = (
|
|
151
|
-
[
|
|
152
|
-
['interrupted', execution.interrupted],
|
|
153
|
-
['timedOut', execution.timedOut],
|
|
154
|
-
['stopped', execution.stopped],
|
|
155
|
-
['detached', execution.detached],
|
|
156
|
-
] as const
|
|
157
|
-
).find(([, isEnabled]) => isEnabled === true)?.[0];
|
|
158
|
-
return unsafeFlag
|
|
159
|
-
? `execution projection reports ${unsafeFlag}=true`
|
|
160
|
-
: undefined;
|
|
161
|
-
}
|