@wichayutdew/pi-workflows 0.3.0 → 1.0.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 +53 -39
- package/dist/index.js +5754 -4794
- package/package.json +1 -1
- package/src/command-names.ts +7 -1
- package/src/commands.ts +126 -52
- package/src/config/catalog.ts +162 -0
- package/src/config/ceiling.ts +116 -116
- package/src/config/command-conflicts.ts +4 -4
- package/src/config/diagnostics.ts +28 -0
- package/src/config/load-settings.ts +37 -0
- package/src/config/load-types.ts +53 -0
- package/src/config/load-workflows.ts +135 -0
- package/src/config/load.ts +43 -311
- package/src/config/types.ts +137 -135
- package/src/config/validate.ts +12 -1261
- package/src/config/validation/permissions.ts +291 -0
- package/src/config/validation/prompt.ts +19 -0
- package/src/config/validation/settings.ts +122 -0
- package/src/config/validation/shared.ts +129 -0
- package/src/config/validation/shortcut.ts +118 -0
- package/src/config/validation/step.ts +228 -0
- package/src/config/validation/subagent.ts +288 -0
- package/src/config/validation/workflow.ts +156 -0
- package/src/config/yaml.ts +29 -0
- package/src/digest.ts +38 -6
- package/src/engine/checkpoint.ts +13 -9
- package/src/engine/create-run.ts +42 -0
- package/src/engine/gate-transitions.ts +184 -0
- package/src/engine/reconciliation-history.ts +88 -0
- package/src/engine/resume.ts +19 -7
- package/src/engine/run-advance.ts +122 -0
- package/src/engine/run-lifecycle.ts +124 -0
- package/src/engine/run-reconciliation.ts +116 -0
- package/src/engine/run-validation.ts +128 -0
- package/src/engine/state-types.ts +64 -0
- package/src/engine/state.ts +10 -193
- package/src/engine/transition-helpers.ts +28 -0
- package/src/engine/transition-types.ts +8 -0
- package/src/engine/transitions.ts +17 -471
- package/src/harness/action-context.ts +181 -0
- package/src/harness/catalog.ts +45 -0
- package/src/harness/context-idle.ts +21 -0
- package/src/harness/core-actions.ts +299 -0
- package/src/harness/delegation-control-actions.ts +250 -0
- package/src/harness/delegation-failure.ts +215 -0
- package/src/harness/delegation-plan.ts +219 -0
- package/src/harness/delegation-recovery-validation.ts +159 -0
- package/src/harness/delegation-response-actions.ts +295 -0
- package/src/harness/delegation-retry-policy.ts +118 -0
- package/src/harness/dependencies.ts +165 -0
- package/src/harness/gate-submission-action.ts +154 -0
- package/src/harness/lifecycle-actions.ts +154 -0
- package/src/harness/pause-actions.ts +120 -0
- package/src/harness/plannotator-result-actions.ts +123 -0
- package/src/harness/prompt-gate-actions.ts +270 -0
- package/src/harness/resume-action.ts +261 -0
- package/src/harness/start-actions.ts +183 -0
- package/src/harness/status-actions.ts +191 -0
- package/src/harness/step-execution-actions.ts +283 -0
- package/src/harness/types.ts +76 -0
- package/src/harness.ts +236 -2303
- package/src/index.ts +73 -11
- package/src/integrations/plannotator-requests.ts +127 -0
- package/src/integrations/plannotator-responses.ts +141 -0
- package/src/integrations/plannotator-types.ts +44 -0
- package/src/integrations/plannotator.ts +16 -235
- package/src/integrations/prompt-gate.ts +17 -10
- package/src/integrations/subagents/child-policy-envelope.ts +119 -0
- package/src/integrations/subagents/child-policy-paths.ts +66 -0
- package/src/integrations/subagents/child-policy-sections.ts +174 -0
- package/src/integrations/subagents/child-policy-types.ts +32 -0
- package/src/integrations/subagents/child-policy-validation.ts +163 -0
- package/src/integrations/subagents/child-runtime-completion.ts +56 -0
- package/src/integrations/subagents/child-runtime-dependencies.ts +47 -0
- package/src/integrations/subagents/child-runtime-files.ts +156 -0
- package/src/integrations/subagents/child-runtime-policy.ts +83 -0
- package/src/integrations/subagents/child-runtime-types.ts +30 -0
- package/src/integrations/subagents/child-runtime.ts +140 -301
- package/src/integrations/subagents/client-delegation.ts +181 -0
- package/src/integrations/subagents/client-messages.ts +66 -0
- package/src/integrations/subagents/client-types.ts +36 -0
- package/src/integrations/subagents/client.ts +127 -219
- package/src/integrations/subagents/delegated-result.ts +31 -0
- package/src/integrations/subagents/diagnostic-format.ts +40 -0
- package/src/integrations/subagents/diagnostic-text.ts +114 -0
- package/src/integrations/subagents/diagnostic-types.ts +77 -0
- package/src/integrations/subagents/diagnostics.ts +24 -977
- package/src/integrations/subagents/failure-correlation.ts +235 -0
- package/src/integrations/subagents/failure-transcript.ts +192 -0
- package/src/integrations/subagents/hidden-bash-failure.ts +98 -0
- package/src/integrations/subagents/protocol-events.ts +27 -0
- package/src/integrations/subagents/protocol.ts +30 -375
- package/src/integrations/subagents/replay-audit.ts +147 -0
- package/src/integrations/subagents/replay-safety.ts +82 -0
- package/src/integrations/subagents/session-diagnostics.ts +258 -0
- package/src/policy/approved-command-extraction.ts +71 -0
- package/src/policy/approved-commands.ts +15 -432
- package/src/policy/bash-authorization.ts +86 -0
- package/src/policy/bash-read-only.ts +161 -0
- package/src/policy/bash-types.ts +20 -0
- package/src/policy/bash.ts +4 -346
- package/src/policy/completion-batch.ts +21 -16
- package/src/policy/immutable-input.ts +5 -2
- package/src/policy/mcp-authorization.ts +71 -0
- package/src/policy/restricted-command.ts +141 -0
- package/src/policy/restricted-git.ts +30 -0
- package/src/policy/reviewed-artifact.ts +97 -0
- package/src/policy/reviewed-command-safety.ts +150 -0
- package/src/policy/reviewed-command-shape.ts +49 -0
- package/src/policy/reviewed-repository-contract.ts +138 -0
- package/src/policy/reviewed-repository-cwd.ts +102 -0
- package/src/policy/tool-call-authorization.ts +58 -0
- package/src/policy/tool-selection.ts +72 -0
- package/src/policy/tool-types.ts +14 -0
- package/src/policy/tools.ts +11 -150
- package/src/preflight.ts +96 -61
- package/src/prompt/main-workflow-notice.ts +41 -0
- package/src/prompt/retry-task.ts +83 -0
- package/src/prompt/step-contract.ts +101 -0
- package/src/prompt/step-sections.ts +75 -0
- package/src/prompt/step-task.ts +156 -0
- package/src/prompt/template.ts +81 -0
- package/src/prompt.ts +10 -254
- package/src/runtime/completion-tool.ts +3 -0
- package/src/runtime/main-step-completion.ts +78 -0
- package/src/runtime/main-step-lifecycle.ts +37 -0
- package/src/runtime/main-step-policy.ts +84 -0
- package/src/runtime/main-step-runtime-types.ts +64 -0
- package/src/runtime/main-step-runtime.ts +129 -200
- package/src/runtime/main-step-state.ts +85 -0
- package/src/runtime/serial-task-queue.ts +48 -13
- package/src/runtime/step-result.ts +48 -27
- package/src/workflow-list.ts +15 -6
- package/src/workflow-status/format-status.ts +52 -0
- package/src/workflow-status/formatting.ts +178 -0
- package/src/workflow-status/layout.ts +109 -0
- package/src/workflow-status/render-board.ts +101 -0
- package/src/workflow-status/render-path.ts +123 -0
- package/src/workflow-status/render-summary.ts +144 -0
- package/src/workflow-status/types.ts +40 -0
- package/src/workflow-status/view.ts +273 -0
- package/src/workflow-status.ts +20 -820
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseDelegatedStepResult,
|
|
3
|
+
type SubagentDelegationResponse,
|
|
4
|
+
type SubagentDelegationUpdate,
|
|
5
|
+
} from '../integrations/subagents/protocol.ts';
|
|
6
|
+
import { advanceRun } from '../engine/transitions.ts';
|
|
7
|
+
import type { WorkflowStepResult } from '../runtime/step-result.ts';
|
|
8
|
+
import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
|
|
9
|
+
import type { ActiveDelegation, DelegationFailureDetails } from './types.ts';
|
|
10
|
+
|
|
11
|
+
type HarnessActionContext = Pick<
|
|
12
|
+
FullHarnessActionContext,
|
|
13
|
+
| 'activeDelegation'
|
|
14
|
+
| 'catalog'
|
|
15
|
+
| 'cleanupDelegation'
|
|
16
|
+
| 'delegationFailures'
|
|
17
|
+
| 'dependencies'
|
|
18
|
+
| 'finishDelegation'
|
|
19
|
+
| 'isSessionActive'
|
|
20
|
+
| 'latestContext'
|
|
21
|
+
| 'mutationQueue'
|
|
22
|
+
| 'pauseForDelegationFailure'
|
|
23
|
+
| 'releaseMainAfterCancellation'
|
|
24
|
+
| 'retainUnconfirmedDelegation'
|
|
25
|
+
| 'retryDelegationAfterFailure'
|
|
26
|
+
| 'run'
|
|
27
|
+
| 'sessionEpoch'
|
|
28
|
+
| 'settleAfterTransition'
|
|
29
|
+
| 'subagents'
|
|
30
|
+
| 'submitGate'
|
|
31
|
+
| 'updateStatus'
|
|
32
|
+
>;
|
|
33
|
+
|
|
34
|
+
export type DelegationResponseActions = {
|
|
35
|
+
handleDelegationUpdate: (
|
|
36
|
+
this: HarnessActionContext,
|
|
37
|
+
active: ActiveDelegation,
|
|
38
|
+
update: SubagentDelegationUpdate,
|
|
39
|
+
) => void;
|
|
40
|
+
queueDelegationResponse: (
|
|
41
|
+
this: HarnessActionContext,
|
|
42
|
+
active: ActiveDelegation,
|
|
43
|
+
response: SubagentDelegationResponse,
|
|
44
|
+
) => void;
|
|
45
|
+
queueDelegationFailure: (
|
|
46
|
+
this: HarnessActionContext,
|
|
47
|
+
active: ActiveDelegation,
|
|
48
|
+
reason: string,
|
|
49
|
+
) => void;
|
|
50
|
+
finishDelegation: (
|
|
51
|
+
this: HarnessActionContext,
|
|
52
|
+
active: ActiveDelegation,
|
|
53
|
+
response: SubagentDelegationResponse,
|
|
54
|
+
) => Promise<void>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function hasErrorCode(error: unknown, code: string): boolean {
|
|
58
|
+
return error instanceof Error && 'code' in error && error.code === code;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function handleDelegationUpdate(
|
|
62
|
+
this: HarnessActionContext,
|
|
63
|
+
active: ActiveDelegation,
|
|
64
|
+
update: SubagentDelegationUpdate,
|
|
65
|
+
): void {
|
|
66
|
+
if (this.activeDelegation !== active) return;
|
|
67
|
+
const progress = [
|
|
68
|
+
update.currentTool ? `tool ${update.currentTool}` : undefined,
|
|
69
|
+
update.toolCount !== undefined ? `${update.toolCount} calls` : undefined,
|
|
70
|
+
update.tokens !== undefined ? `${update.tokens} tokens` : undefined,
|
|
71
|
+
].filter((part): part is string => part !== undefined);
|
|
72
|
+
active.progress = progress.join(', ') || 'running';
|
|
73
|
+
this.updateStatus();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function queueDelegationResponse(
|
|
77
|
+
this: HarnessActionContext,
|
|
78
|
+
active: ActiveDelegation,
|
|
79
|
+
response: SubagentDelegationResponse,
|
|
80
|
+
): void {
|
|
81
|
+
void this.mutationQueue
|
|
82
|
+
.run(() => this.finishDelegation(active, response))
|
|
83
|
+
.catch((error: unknown) => {
|
|
84
|
+
this.pauseForDelegationFailure(
|
|
85
|
+
error instanceof Error ? error.message : String(error),
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function queueDelegationFailure(
|
|
91
|
+
this: HarnessActionContext,
|
|
92
|
+
active: ActiveDelegation,
|
|
93
|
+
reason: string,
|
|
94
|
+
): void {
|
|
95
|
+
void this.mutationQueue
|
|
96
|
+
.run(async () => {
|
|
97
|
+
if (this.activeDelegation !== active) {
|
|
98
|
+
await this.cleanupDelegation(active);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (this.subagents.activeRequestId === active.requestId) {
|
|
102
|
+
this.retainUnconfirmedDelegation(active, reason);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
this.activeDelegation = undefined;
|
|
106
|
+
await this.cleanupDelegation(active);
|
|
107
|
+
this.pauseForDelegationFailure(reason);
|
|
108
|
+
})
|
|
109
|
+
.catch((error: unknown) => {
|
|
110
|
+
this.pauseForDelegationFailure(
|
|
111
|
+
error instanceof Error ? error.message : String(error),
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function finishDelegation(
|
|
117
|
+
this: HarnessActionContext,
|
|
118
|
+
active: ActiveDelegation,
|
|
119
|
+
response: SubagentDelegationResponse,
|
|
120
|
+
): Promise<void> {
|
|
121
|
+
if (this.activeDelegation !== active) {
|
|
122
|
+
await this.cleanupDelegation(active);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
this.activeDelegation = undefined;
|
|
126
|
+
let terminalFailure: DelegationFailureDetails | undefined;
|
|
127
|
+
let cleanupAttempted = false;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
if (
|
|
131
|
+
!this.isSessionActive ||
|
|
132
|
+
this.sessionEpoch !== active.sessionEpoch ||
|
|
133
|
+
!this.run ||
|
|
134
|
+
this.run.status !== 'running' ||
|
|
135
|
+
this.run.runId !== active.runId ||
|
|
136
|
+
this.run.currentStepId !== active.stepId ||
|
|
137
|
+
this.run.currentStepDigest !== active.stepDigest
|
|
138
|
+
) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const workflow = this.catalog.workflows.get(this.run.workflowId);
|
|
142
|
+
const step = workflow?.definition.steps[this.run.currentStepId];
|
|
143
|
+
if (!workflow || !step) {
|
|
144
|
+
throw new Error('Active workflow configuration is unavailable');
|
|
145
|
+
}
|
|
146
|
+
let recoveredTerminalFailure: DelegationFailureDetails | undefined;
|
|
147
|
+
if (
|
|
148
|
+
response.status !== 'completed' ||
|
|
149
|
+
this.delegationFailures.hasContradictoryCompletion(response)
|
|
150
|
+
) {
|
|
151
|
+
const failure = await this.delegationFailures.describeDelegationFailure(
|
|
152
|
+
active,
|
|
153
|
+
response,
|
|
154
|
+
);
|
|
155
|
+
terminalFailure = failure;
|
|
156
|
+
if (
|
|
157
|
+
response.status !== 'failed' ||
|
|
158
|
+
failure.diagnostic?.completionAfterFailure !== true
|
|
159
|
+
) {
|
|
160
|
+
throw new Error(failure.reason);
|
|
161
|
+
}
|
|
162
|
+
const projectionError = this.delegationFailures.recoveredProjectionError(
|
|
163
|
+
active,
|
|
164
|
+
response,
|
|
165
|
+
failure.diagnostic,
|
|
166
|
+
);
|
|
167
|
+
if (projectionError) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
this.delegationFailures.rejectedRecoveryReason(
|
|
170
|
+
failure,
|
|
171
|
+
projectionError,
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
recoveredTerminalFailure = failure;
|
|
176
|
+
}
|
|
177
|
+
const requiredSkillWarning =
|
|
178
|
+
step.requires.skills.length > 0
|
|
179
|
+
? response.warnings?.find((warning) => /skill/i.test(warning))
|
|
180
|
+
: undefined;
|
|
181
|
+
if (requiredSkillWarning) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Subagent skill preflight failed: ${requiredSkillWarning}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let serializedResult: string;
|
|
188
|
+
try {
|
|
189
|
+
serializedResult = await this.dependencies.readDelegatedResult(active);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (recoveredTerminalFailure) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
this.delegationFailures.rejectedRecoveryReason(
|
|
194
|
+
recoveredTerminalFailure,
|
|
195
|
+
error,
|
|
196
|
+
),
|
|
197
|
+
{ cause: error },
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
if (hasErrorCode(error, 'ENOENT')) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`Subagent "${active.agent}" completed without producing the required correlated structured_output result`,
|
|
203
|
+
{ cause: error },
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let result: WorkflowStepResult;
|
|
210
|
+
try {
|
|
211
|
+
const rawResult: unknown = JSON.parse(serializedResult);
|
|
212
|
+
result = parseDelegatedStepResult(rawResult, active.policy);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (recoveredTerminalFailure) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
this.delegationFailures.rejectedRecoveryReason(
|
|
217
|
+
recoveredTerminalFailure,
|
|
218
|
+
error,
|
|
219
|
+
),
|
|
220
|
+
{ cause: error },
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
if (
|
|
226
|
+
recoveredTerminalFailure?.diagnostic &&
|
|
227
|
+
!this.delegationFailures.completionMatchesResult(
|
|
228
|
+
recoveredTerminalFailure.diagnostic,
|
|
229
|
+
result,
|
|
230
|
+
active.policy,
|
|
231
|
+
)
|
|
232
|
+
) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
this.delegationFailures.rejectedRecoveryReason(
|
|
235
|
+
recoveredTerminalFailure,
|
|
236
|
+
'structured_output transcript value does not match the correlated result',
|
|
237
|
+
),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
if (recoveredTerminalFailure) {
|
|
241
|
+
const isFalsePositive =
|
|
242
|
+
recoveredTerminalFailure.diagnostic?.correlation ===
|
|
243
|
+
'successful-output-before-completion';
|
|
244
|
+
this.latestContext?.ui.notify(
|
|
245
|
+
isFalsePositive
|
|
246
|
+
? `Accepted "${active.stepId}" because the trusted child transcript proved the terminal tool error was a false positive and produced a matching structured result`
|
|
247
|
+
: `Accepted "${active.stepId}" because the child resolved an earlier tool failure and produced a valid structured result`,
|
|
248
|
+
'warning',
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (step.gate?.submitOutcome === result.outcome) {
|
|
252
|
+
await this.submitGate(
|
|
253
|
+
workflow,
|
|
254
|
+
this.run,
|
|
255
|
+
result.outcome,
|
|
256
|
+
result.artifact ?? '',
|
|
257
|
+
);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this.run = advanceRun(
|
|
262
|
+
workflow,
|
|
263
|
+
this.run,
|
|
264
|
+
result.outcome,
|
|
265
|
+
result.summary,
|
|
266
|
+
this.dependencies.now(),
|
|
267
|
+
);
|
|
268
|
+
this.settleAfterTransition(workflow);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
271
|
+
cleanupAttempted = true;
|
|
272
|
+
await this.cleanupDelegation(active);
|
|
273
|
+
if (!this.retryDelegationAfterFailure(active, terminalFailure, reason)) {
|
|
274
|
+
this.pauseForDelegationFailure(reason);
|
|
275
|
+
}
|
|
276
|
+
} finally {
|
|
277
|
+
try {
|
|
278
|
+
if (!cleanupAttempted) await this.cleanupDelegation(active);
|
|
279
|
+
} finally {
|
|
280
|
+
if (active.cancelling) this.releaseMainAfterCancellation(active);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Returns delegation response and result-processing actions for composition.
|
|
287
|
+
*/
|
|
288
|
+
export function createDelegationResponseActions(): DelegationResponseActions {
|
|
289
|
+
return {
|
|
290
|
+
handleDelegationUpdate,
|
|
291
|
+
queueDelegationResponse,
|
|
292
|
+
queueDelegationFailure,
|
|
293
|
+
finishDelegation,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { digest } from '../digest.ts';
|
|
2
|
+
import type { ChildStepPolicy } from '../integrations/subagents/protocol.ts';
|
|
3
|
+
import type { DelegationFailureDetails } from './types.ts';
|
|
4
|
+
|
|
5
|
+
const MAX_FAILURE_FIELD_CHARS = 1_600;
|
|
6
|
+
export const MAX_DELEGATION_RECOVERY_ATTEMPTS = 2;
|
|
7
|
+
|
|
8
|
+
const normalizedFingerprintField = (
|
|
9
|
+
value: string | undefined,
|
|
10
|
+
): string | undefined => value?.trim().replaceAll(/\s+/g, ' ') || undefined;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates a stable identity for the semantic evidence behind one failure.
|
|
14
|
+
*
|
|
15
|
+
* Runtime request identifiers and session paths are deliberately absent so a
|
|
16
|
+
* fresh child repeating the same failed approach is recognized.
|
|
17
|
+
*
|
|
18
|
+
* @param failure - Normalized terminal failure evidence.
|
|
19
|
+
* @returns Stable digest used to stop duplicate automatic recovery attempts.
|
|
20
|
+
*/
|
|
21
|
+
export function delegationFailureFingerprint(
|
|
22
|
+
failure: DelegationFailureDetails,
|
|
23
|
+
): string {
|
|
24
|
+
return digest({
|
|
25
|
+
status: failure.status,
|
|
26
|
+
exitCode: failure.exitCode,
|
|
27
|
+
error: normalizedFingerprintField(failure.error),
|
|
28
|
+
recoveryBlocker: failure.recoveryBlocker,
|
|
29
|
+
replayToolCount: failure.replayAudit?.toolCount,
|
|
30
|
+
diagnostic: failure.diagnostic
|
|
31
|
+
? {
|
|
32
|
+
tool: normalizedFingerprintField(failure.diagnostic.tool),
|
|
33
|
+
call: normalizedFingerprintField(failure.diagnostic.call),
|
|
34
|
+
output: normalizedFingerprintField(failure.diagnostic.output),
|
|
35
|
+
}
|
|
36
|
+
: undefined,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Bounds a failure field while retaining evidence from both ends.
|
|
42
|
+
*
|
|
43
|
+
* @param value - Untrusted diagnostic text.
|
|
44
|
+
* @returns Text safe for inclusion in a workflow notification.
|
|
45
|
+
*/
|
|
46
|
+
export function boundedFailureField(value: string): string {
|
|
47
|
+
if (value.length <= MAX_FAILURE_FIELD_CHARS) return value;
|
|
48
|
+
const marker = '… [truncated] …';
|
|
49
|
+
const available = MAX_FAILURE_FIELD_CHARS - marker.length - 2;
|
|
50
|
+
const startLength = Math.ceil(available / 2);
|
|
51
|
+
const endLength = Math.floor(available / 2);
|
|
52
|
+
return `${value.slice(0, startLength)}\n${marker}\n${value.slice(-endLength)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Returns whether a terminal failure contains evidence suitable for recovery.
|
|
57
|
+
*
|
|
58
|
+
* @param failure - Normalized terminal failure.
|
|
59
|
+
* @returns Whether diagnostic recovery may be attempted.
|
|
60
|
+
*/
|
|
61
|
+
export function isRetryableTerminalFailure(
|
|
62
|
+
failure: DelegationFailureDetails,
|
|
63
|
+
): boolean {
|
|
64
|
+
if (failure.recoveryBlocker !== undefined) return false;
|
|
65
|
+
if (
|
|
66
|
+
failure.status === 'timed_out' ||
|
|
67
|
+
failure.status === 'turn_budget_exhausted' ||
|
|
68
|
+
failure.status === 'tool_budget_exhausted'
|
|
69
|
+
) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (
|
|
73
|
+
failure.status !== 'failed' &&
|
|
74
|
+
failure.status !== 'structured_output_failed'
|
|
75
|
+
) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return (
|
|
79
|
+
failure.error !== undefined ||
|
|
80
|
+
(Number.isSafeInteger(failure.exitCode) && failure.exitCode !== 0)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Applies replay-safety policy to a proposed delegation retry.
|
|
86
|
+
*
|
|
87
|
+
* @param policy - Child policy from the failed delegation.
|
|
88
|
+
* @param isReplayExplicitlyAuthorized - Workflow-level retry authorization.
|
|
89
|
+
* @param replayAudit - Transcript-derived replay evidence.
|
|
90
|
+
* @returns Whether retrying the delegation is safe.
|
|
91
|
+
*/
|
|
92
|
+
export function isSafeToRetryDelegation(
|
|
93
|
+
policy: ChildStepPolicy,
|
|
94
|
+
isReplayExplicitlyAuthorized: boolean,
|
|
95
|
+
replayAudit: DelegationFailureDetails['replayAudit'],
|
|
96
|
+
): boolean {
|
|
97
|
+
return (
|
|
98
|
+
replayAudit?.replaySafe === true &&
|
|
99
|
+
(isReplayExplicitlyAuthorized ||
|
|
100
|
+
policy.permissions.bash.mode === 'deny' ||
|
|
101
|
+
policy.permissions.bash.mode === 'read-only')
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Extends a failure reason with bounded recovery-rejection evidence.
|
|
107
|
+
*
|
|
108
|
+
* @param failure - Original normalized failure.
|
|
109
|
+
* @param error - Error raised while attempting recovery.
|
|
110
|
+
* @returns User-facing rejection reason.
|
|
111
|
+
*/
|
|
112
|
+
export function rejectedRecoveryReason(
|
|
113
|
+
failure: DelegationFailureDetails,
|
|
114
|
+
error: unknown,
|
|
115
|
+
): string {
|
|
116
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
117
|
+
return `${failure.reason}\nRecovery rejected: ${boundedFailureField(detail)}`;
|
|
118
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
import { constants, mkdtempSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { lstat, open, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
7
|
+
import { loadCatalog } from '../config/load.ts';
|
|
8
|
+
import {
|
|
9
|
+
requestPlannotatorReview,
|
|
10
|
+
requestPlannotatorReviewStatus,
|
|
11
|
+
} from '../integrations/plannotator.ts';
|
|
12
|
+
import { requestPromptGateReview } from '../integrations/prompt-gate.ts';
|
|
13
|
+
import {
|
|
14
|
+
createSubagentDelegationClient,
|
|
15
|
+
type SubagentDelegationClientController,
|
|
16
|
+
} from '../integrations/subagents/client.ts';
|
|
17
|
+
import {
|
|
18
|
+
readDelegationReplayAudit,
|
|
19
|
+
readToolFailureDiagnostic,
|
|
20
|
+
} from '../integrations/subagents/diagnostics.ts';
|
|
21
|
+
import {
|
|
22
|
+
createMainStepRuntime,
|
|
23
|
+
type MainStepRuntimeController,
|
|
24
|
+
} from '../runtime/main-step-runtime.ts';
|
|
25
|
+
import {
|
|
26
|
+
createSerialTaskQueue,
|
|
27
|
+
type SerialTaskQueueController,
|
|
28
|
+
} from '../runtime/serial-task-queue.ts';
|
|
29
|
+
import { showWorkflowStatus } from '../workflow-status.ts';
|
|
30
|
+
import type { ActiveDelegation } from './types.ts';
|
|
31
|
+
|
|
32
|
+
const MAX_DELEGATED_RESULT_BYTES = 1024 * 1024;
|
|
33
|
+
|
|
34
|
+
export type DelegationWorkspace = {
|
|
35
|
+
resultDirectory: string;
|
|
36
|
+
capabilityPath: string;
|
|
37
|
+
capabilityToken: string;
|
|
38
|
+
resultPath: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type WorkflowHarnessDependencies = {
|
|
42
|
+
readonly now: () => number;
|
|
43
|
+
readonly createRequestId: () => string;
|
|
44
|
+
readonly createAbortController: () => AbortController;
|
|
45
|
+
readonly createDelegationWorkspace: () => DelegationWorkspace;
|
|
46
|
+
readonly readDelegatedResult: (active: ActiveDelegation) => Promise<string>;
|
|
47
|
+
readonly removeDelegationWorkspace: (
|
|
48
|
+
resultDirectory: string,
|
|
49
|
+
) => Promise<void>;
|
|
50
|
+
readonly currentWorkingDirectory: () => string;
|
|
51
|
+
readonly waitForDelay: (delayMs: number) => Promise<void>;
|
|
52
|
+
readonly loadCatalog: typeof loadCatalog;
|
|
53
|
+
readonly requestPlannotatorReview: typeof requestPlannotatorReview;
|
|
54
|
+
readonly requestPlannotatorReviewStatus: typeof requestPlannotatorReviewStatus;
|
|
55
|
+
readonly requestPromptGateReview: typeof requestPromptGateReview;
|
|
56
|
+
readonly readDelegationReplayAudit: typeof readDelegationReplayAudit;
|
|
57
|
+
readonly readToolFailureDiagnostic: typeof readToolFailureDiagnostic;
|
|
58
|
+
readonly showWorkflowStatus: typeof showWorkflowStatus;
|
|
59
|
+
readonly createSubagentClient: (
|
|
60
|
+
pi: ExtensionAPI,
|
|
61
|
+
) => SubagentDelegationClientController;
|
|
62
|
+
readonly createMainStepRuntime: (
|
|
63
|
+
pi: ExtensionAPI,
|
|
64
|
+
) => MainStepRuntimeController;
|
|
65
|
+
readonly createMutationQueue: () => SerialTaskQueueController;
|
|
66
|
+
readonly scheduleInterval: (
|
|
67
|
+
operation: () => void,
|
|
68
|
+
intervalMs: number,
|
|
69
|
+
) => ReturnType<typeof setInterval>;
|
|
70
|
+
readonly cancelInterval: (timer: ReturnType<typeof setInterval>) => void;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function createDelegationWorkspace(): DelegationWorkspace {
|
|
74
|
+
const resultDirectory = mkdtempSync(join(tmpdir(), 'pi-workflows-step-'));
|
|
75
|
+
const capabilityPath = join(resultDirectory, 'capability');
|
|
76
|
+
const capabilityToken = randomBytes(32).toString('hex');
|
|
77
|
+
const resultPath = join(resultDirectory, 'result.json');
|
|
78
|
+
writeFileSync(capabilityPath, capabilityToken, {
|
|
79
|
+
encoding: 'utf8',
|
|
80
|
+
flag: 'wx',
|
|
81
|
+
mode: 0o600,
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
resultDirectory,
|
|
85
|
+
capabilityPath,
|
|
86
|
+
capabilityToken,
|
|
87
|
+
resultPath,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function readDelegatedResult(active: ActiveDelegation): Promise<string> {
|
|
92
|
+
const expectedPath = join(active.resultDirectory, 'result.json');
|
|
93
|
+
if (active.policy.resultPath !== expectedPath) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
'delegated result path does not match its private directory',
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
const inspected = await lstat(expectedPath);
|
|
99
|
+
if (inspected.isSymbolicLink() || !inspected.isFile()) {
|
|
100
|
+
throw new Error('delegated result is not a regular file');
|
|
101
|
+
}
|
|
102
|
+
const handle = await open(
|
|
103
|
+
expectedPath,
|
|
104
|
+
constants.O_RDONLY | constants.O_NOFOLLOW,
|
|
105
|
+
);
|
|
106
|
+
try {
|
|
107
|
+
const beforeRead = await handle.stat();
|
|
108
|
+
if (!beforeRead.isFile() || beforeRead.size > MAX_DELEGATED_RESULT_BYTES) {
|
|
109
|
+
throw new Error('delegated result is not a bounded regular file');
|
|
110
|
+
}
|
|
111
|
+
const value = await handle.readFile({ encoding: 'utf8' });
|
|
112
|
+
const afterRead = await handle.stat();
|
|
113
|
+
if (
|
|
114
|
+
afterRead.dev !== beforeRead.dev ||
|
|
115
|
+
afterRead.ino !== beforeRead.ino ||
|
|
116
|
+
afterRead.size !== beforeRead.size ||
|
|
117
|
+
afterRead.mtimeMs !== beforeRead.mtimeMs
|
|
118
|
+
) {
|
|
119
|
+
throw new Error('delegated result changed while it was being read');
|
|
120
|
+
}
|
|
121
|
+
return value;
|
|
122
|
+
} finally {
|
|
123
|
+
await handle.close();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const DEFAULT_DEPENDENCIES: WorkflowHarnessDependencies = {
|
|
128
|
+
now: () => Date.now(),
|
|
129
|
+
createRequestId: randomUUID,
|
|
130
|
+
createAbortController: () => new AbortController(),
|
|
131
|
+
createDelegationWorkspace,
|
|
132
|
+
readDelegatedResult,
|
|
133
|
+
removeDelegationWorkspace: async (resultDirectory) =>
|
|
134
|
+
rm(resultDirectory, { recursive: true, force: true }),
|
|
135
|
+
currentWorkingDirectory: () => process.cwd(),
|
|
136
|
+
waitForDelay: async (delayMs) =>
|
|
137
|
+
new Promise((resolve) => setTimeout(resolve, delayMs)),
|
|
138
|
+
loadCatalog,
|
|
139
|
+
requestPlannotatorReview,
|
|
140
|
+
requestPlannotatorReviewStatus,
|
|
141
|
+
requestPromptGateReview,
|
|
142
|
+
readDelegationReplayAudit,
|
|
143
|
+
readToolFailureDiagnostic,
|
|
144
|
+
showWorkflowStatus,
|
|
145
|
+
createSubagentClient: (pi) => createSubagentDelegationClient(pi.events),
|
|
146
|
+
createMainStepRuntime: (pi) => createMainStepRuntime({ pi }),
|
|
147
|
+
createMutationQueue: createSerialTaskQueue,
|
|
148
|
+
scheduleInterval: (operation, intervalMs) =>
|
|
149
|
+
setInterval(operation, intervalMs),
|
|
150
|
+
cancelInterval: (timer) => {
|
|
151
|
+
clearInterval(timer);
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Builds the effect boundary used by the workflow harness.
|
|
157
|
+
*
|
|
158
|
+
* Callers can replace individual dependencies for deterministic tests or
|
|
159
|
+
* alternate runtime integrations without changing workflow logic.
|
|
160
|
+
*/
|
|
161
|
+
export function createWorkflowHarnessDependencies(
|
|
162
|
+
overrides: Partial<WorkflowHarnessDependencies> = {},
|
|
163
|
+
): WorkflowHarnessDependencies {
|
|
164
|
+
return { ...DEFAULT_DEPENDENCIES, ...overrides };
|
|
165
|
+
}
|