@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
package/src/harness.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
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
1
|
import type {
|
|
7
2
|
ExtensionAPI,
|
|
8
3
|
ExtensionCommandContext,
|
|
@@ -13,2394 +8,332 @@ import {
|
|
|
13
8
|
registerHarnessCommands,
|
|
14
9
|
type WorkflowCommandController,
|
|
15
10
|
} from './commands.ts';
|
|
16
|
-
import { loadCatalog } from './config/load.ts';
|
|
17
|
-
import { hasRuntimeCommandConflict } from './config/command-conflicts.ts';
|
|
18
11
|
import {
|
|
19
|
-
DEFAULT_SETTINGS,
|
|
20
12
|
DEFAULT_STATUS_SHORTCUT,
|
|
21
13
|
type LoadedWorkflow,
|
|
22
14
|
type WorkflowCatalog,
|
|
23
15
|
type WorkflowStep,
|
|
24
16
|
} from './config/types.ts';
|
|
25
|
-
import {
|
|
17
|
+
import type { WorkflowRun } from './engine/state.ts';
|
|
18
|
+
import { type PromptGateReviewResult } from './integrations/prompt-gate.ts';
|
|
19
|
+
import type { SubagentDelegationClientController } from './integrations/subagents/client.ts';
|
|
26
20
|
import {
|
|
27
|
-
abortRun,
|
|
28
|
-
advanceRun,
|
|
29
|
-
allowedOutcomes,
|
|
30
|
-
attachGateReviewId,
|
|
31
|
-
beginGate,
|
|
32
|
-
failGate,
|
|
33
|
-
failRun,
|
|
34
|
-
pauseRun,
|
|
35
|
-
reconcileRun,
|
|
36
|
-
resolveGate,
|
|
37
|
-
resumeRun,
|
|
38
|
-
storeGateResolution,
|
|
39
|
-
} from './engine/transitions.ts';
|
|
40
|
-
import {
|
|
41
|
-
createRun,
|
|
42
|
-
type GateResolution,
|
|
43
|
-
type WorkflowRun,
|
|
44
|
-
} from './engine/state.ts';
|
|
45
|
-
import { readLatestCheckpoint } from './engine/checkpoint.ts';
|
|
46
|
-
import {
|
|
47
|
-
captureResumeCheckpoint,
|
|
48
|
-
matchesResumeCheckpoint,
|
|
49
|
-
} from './engine/resume.ts';
|
|
50
|
-
import {
|
|
51
|
-
parsePlannotatorResult,
|
|
52
|
-
PLANNOTATOR_RESULT_CHANNEL,
|
|
53
|
-
requestPlannotatorReview,
|
|
54
|
-
requestPlannotatorReviewStatus,
|
|
55
|
-
} from './integrations/plannotator.ts';
|
|
56
|
-
import {
|
|
57
|
-
requestPromptGateReview,
|
|
58
|
-
type PromptGateReviewResult,
|
|
59
|
-
} from './integrations/prompt-gate.ts';
|
|
60
|
-
import { SubagentDelegationClient } from './integrations/subagents/client.ts';
|
|
61
|
-
import {
|
|
62
|
-
encodeChildPolicy,
|
|
63
|
-
parseDelegatedStepResult,
|
|
64
|
-
type ChildStepPolicy,
|
|
65
|
-
type SubagentDelegationRequest,
|
|
66
21
|
type SubagentDelegationResponse,
|
|
67
|
-
type SubagentDelegationStatus,
|
|
68
22
|
type SubagentDelegationUpdate,
|
|
69
23
|
} from './integrations/subagents/protocol.ts';
|
|
24
|
+
import type { MainStepRuntimeController } from './runtime/main-step-runtime.ts';
|
|
25
|
+
import type { SerialTaskQueueController } from './runtime/serial-task-queue.ts';
|
|
26
|
+
import type { WorkflowStepResult } from './runtime/step-result.ts';
|
|
70
27
|
import {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
readToolFailureDiagnostic,
|
|
76
|
-
type DelegationReplayAudit,
|
|
77
|
-
} from './integrations/subagents/diagnostics.ts';
|
|
78
|
-
import { preflightStep } from './preflight.ts';
|
|
79
|
-
import {
|
|
80
|
-
buildDelegatedStepTask,
|
|
81
|
-
buildMainStepTask,
|
|
82
|
-
buildMainWorkflowNotice,
|
|
83
|
-
reinforcementRetryTask,
|
|
84
|
-
} from './prompt.ts';
|
|
85
|
-
import {
|
|
86
|
-
narrowApprovedBashCommands,
|
|
87
|
-
resolveReviewedRepositoryCwd,
|
|
88
|
-
reviewedCommandShapeError,
|
|
89
|
-
} from './policy/approved-commands.ts';
|
|
28
|
+
createDelegationFailureActions,
|
|
29
|
+
type DelegationFailureActions,
|
|
30
|
+
} from './harness/delegation-failure.ts';
|
|
31
|
+
import { createEmptyCatalog } from './harness/catalog.ts';
|
|
90
32
|
import {
|
|
91
|
-
|
|
92
|
-
type
|
|
93
|
-
} from './
|
|
94
|
-
import {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
33
|
+
createWorkflowHarnessDependencies,
|
|
34
|
+
type WorkflowHarnessDependencies,
|
|
35
|
+
} from './harness/dependencies.ts';
|
|
36
|
+
import type {
|
|
37
|
+
ActiveDelegation,
|
|
38
|
+
ActivePromptReview,
|
|
39
|
+
DelegationRecovery,
|
|
40
|
+
DelegationFailureDetails,
|
|
41
|
+
MainStepIdentity,
|
|
42
|
+
WorkflowStartContext,
|
|
43
|
+
} from './harness/types.ts';
|
|
44
|
+
import { createStatusActions } from './harness/status-actions.ts';
|
|
45
|
+
import { createStartActions } from './harness/start-actions.ts';
|
|
46
|
+
import { createPauseActions } from './harness/pause-actions.ts';
|
|
47
|
+
import { createResumeAction } from './harness/resume-action.ts';
|
|
48
|
+
import { createLifecycleActions } from './harness/lifecycle-actions.ts';
|
|
49
|
+
import { createStepExecutionActions } from './harness/step-execution-actions.ts';
|
|
50
|
+
import { createDelegationResponseActions } from './harness/delegation-response-actions.ts';
|
|
51
|
+
import { createDelegationControlActions } from './harness/delegation-control-actions.ts';
|
|
52
|
+
import { createGateSubmissionAction } from './harness/gate-submission-action.ts';
|
|
53
|
+
import { createPromptGateActions } from './harness/prompt-gate-actions.ts';
|
|
54
|
+
import { createPlannotatorResultActions } from './harness/plannotator-result-actions.ts';
|
|
55
|
+
import { createCoreActions } from './harness/core-actions.ts';
|
|
98
56
|
import {
|
|
99
57
|
formatShortcutLabel,
|
|
100
|
-
showWorkflowStatus as showWorkflowStatusOverlay,
|
|
101
|
-
workflowStatusIcon,
|
|
102
|
-
type WorkflowStatusExecution,
|
|
103
58
|
type WorkflowStatusSnapshot,
|
|
104
59
|
} from './workflow-status.ts';
|
|
105
60
|
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
(error): error is string =>
|
|
124
|
-
typeof error === 'string' && error.trim().length > 0,
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function nonzeroTerminalExitCode(
|
|
129
|
-
response: SubagentDelegationResponse,
|
|
130
|
-
): number | undefined {
|
|
131
|
-
return [response.exitCode, response.execution?.exitCode].find(
|
|
132
|
-
(exitCode): exitCode is number =>
|
|
133
|
-
typeof exitCode === 'number' &&
|
|
134
|
-
Number.isSafeInteger(exitCode) &&
|
|
135
|
-
exitCode !== 0,
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function hasContradictoryCompletion(
|
|
140
|
-
response: SubagentDelegationResponse,
|
|
141
|
-
): boolean {
|
|
142
|
-
return (
|
|
143
|
-
response.status === 'completed' &&
|
|
144
|
-
(nonEmptyTerminalError(response) !== undefined ||
|
|
145
|
-
nonzeroTerminalExitCode(response) !== undefined)
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function isRetryableTerminalFailure(
|
|
150
|
-
failure: DelegationFailureDetails,
|
|
151
|
-
): boolean {
|
|
152
|
-
return (
|
|
153
|
-
(failure.status === 'failed' ||
|
|
154
|
-
failure.status === 'structured_output_failed') &&
|
|
155
|
-
(failure.error !== undefined ||
|
|
156
|
-
(Number.isSafeInteger(failure.exitCode) && failure.exitCode !== 0))
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function validateReplayAudit(
|
|
161
|
-
response: SubagentDelegationResponse,
|
|
162
|
-
replayAudit: DelegationReplayAudit | undefined,
|
|
163
|
-
): DelegationReplayAudit | undefined {
|
|
164
|
-
if (!replayAudit) return undefined;
|
|
165
|
-
if (
|
|
166
|
-
response.toolCount !== undefined &&
|
|
167
|
-
response.toolCount !== replayAudit.toolCount
|
|
168
|
-
) {
|
|
169
|
-
return { ...replayAudit, replaySafe: false };
|
|
170
|
-
}
|
|
171
|
-
return replayAudit;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function isSafeToRetryDelegation(
|
|
175
|
-
policy: ChildStepPolicy,
|
|
176
|
-
replayExplicitlyAuthorized: boolean,
|
|
177
|
-
replayAudit: DelegationReplayAudit | undefined,
|
|
178
|
-
): boolean {
|
|
179
|
-
const tools = new Set(policy.permissions.tools);
|
|
180
|
-
return (
|
|
181
|
-
replayAudit?.replaySafe === true &&
|
|
182
|
-
!tools.has('edit') &&
|
|
183
|
-
!tools.has('write') &&
|
|
184
|
-
(replayExplicitlyAuthorized ||
|
|
185
|
-
policy.permissions.bash.mode === 'deny' ||
|
|
186
|
-
policy.permissions.bash.mode === 'read-only')
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
interface ActiveDelegation {
|
|
191
|
-
requestId: string;
|
|
192
|
-
runId: string;
|
|
193
|
-
stepId: string;
|
|
194
|
-
stepDigest: string;
|
|
195
|
-
sessionEpoch: number;
|
|
196
|
-
resultDirectory: string;
|
|
197
|
-
policy: ChildStepPolicy;
|
|
198
|
-
transcriptTask: string;
|
|
199
|
-
agent: string;
|
|
200
|
-
trustedSessionRoot?: string;
|
|
201
|
-
reinforcementReplayAuthorized: boolean;
|
|
202
|
-
reinforcementRetryCount: number;
|
|
203
|
-
progress?: string;
|
|
204
|
-
cancelling?: boolean;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const MAX_FAILURE_FIELD_CHARS = 1_600;
|
|
208
|
-
const MAX_DELEGATED_RESULT_BYTES = 1024 * 1024;
|
|
209
|
-
|
|
210
|
-
function boundedFailureField(value: string): string {
|
|
211
|
-
if (value.length <= MAX_FAILURE_FIELD_CHARS) return value;
|
|
212
|
-
const marker = '… [truncated] …';
|
|
213
|
-
const available = MAX_FAILURE_FIELD_CHARS - marker.length - 2;
|
|
214
|
-
const startLength = Math.ceil(available / 2);
|
|
215
|
-
const endLength = Math.floor(available / 2);
|
|
216
|
-
return `${value.slice(0, startLength)}\n${marker}\n${value.slice(-endLength)}`;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async function readStableDelegatedResult(
|
|
220
|
-
active: ActiveDelegation,
|
|
221
|
-
): Promise<string> {
|
|
222
|
-
const expectedPath = join(active.resultDirectory, 'result.json');
|
|
223
|
-
if (active.policy.resultPath !== expectedPath) {
|
|
224
|
-
throw new Error(
|
|
225
|
-
'delegated result path does not match its private directory',
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
const inspected = await lstat(expectedPath);
|
|
229
|
-
if (inspected.isSymbolicLink() || !inspected.isFile()) {
|
|
230
|
-
throw new Error('delegated result is not a regular file');
|
|
231
|
-
}
|
|
232
|
-
const handle = await open(
|
|
233
|
-
expectedPath,
|
|
234
|
-
constants.O_RDONLY | constants.O_NOFOLLOW,
|
|
235
|
-
);
|
|
236
|
-
try {
|
|
237
|
-
const beforeRead = await handle.stat();
|
|
238
|
-
if (!beforeRead.isFile() || beforeRead.size > MAX_DELEGATED_RESULT_BYTES) {
|
|
239
|
-
throw new Error('delegated result is not a bounded regular file');
|
|
240
|
-
}
|
|
241
|
-
const value = await handle.readFile({ encoding: 'utf8' });
|
|
242
|
-
const afterRead = await handle.stat();
|
|
243
|
-
if (
|
|
244
|
-
afterRead.dev !== beforeRead.dev ||
|
|
245
|
-
afterRead.ino !== beforeRead.ino ||
|
|
246
|
-
afterRead.size !== beforeRead.size ||
|
|
247
|
-
afterRead.mtimeMs !== beforeRead.mtimeMs
|
|
248
|
-
) {
|
|
249
|
-
throw new Error('delegated result changed while it was being read');
|
|
250
|
-
}
|
|
251
|
-
return value;
|
|
252
|
-
} finally {
|
|
253
|
-
await handle.close();
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function rejectedRecoveryReason(
|
|
258
|
-
failure: DelegationFailureDetails,
|
|
259
|
-
error: unknown,
|
|
260
|
-
): string {
|
|
261
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
262
|
-
return `${failure.reason}\nRecovery rejected: ${boundedFailureField(detail)}`;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function completionMatchesResult(
|
|
266
|
-
diagnostic: NonNullable<DelegationFailureDetails['diagnostic']>,
|
|
267
|
-
result: WorkflowStepResult,
|
|
268
|
-
policy: ChildStepPolicy,
|
|
269
|
-
): boolean {
|
|
270
|
-
const value = diagnostic.completionValue;
|
|
271
|
-
if (!value) return false;
|
|
272
|
-
const expectedKeys = [
|
|
273
|
-
'outcome',
|
|
274
|
-
'summary',
|
|
275
|
-
...(result.artifact === undefined ? [] : ['artifact']),
|
|
276
|
-
].sort();
|
|
277
|
-
const actualKeys = Object.keys(value).sort();
|
|
278
|
-
if (
|
|
279
|
-
actualKeys.length !== expectedKeys.length ||
|
|
280
|
-
!actualKeys.every((key, index) => key === expectedKeys[index])
|
|
281
|
-
) {
|
|
282
|
-
return false;
|
|
283
|
-
}
|
|
284
|
-
try {
|
|
285
|
-
const completion = parseDelegatedStepResult(
|
|
286
|
-
{
|
|
287
|
-
...value,
|
|
288
|
-
version: 1,
|
|
289
|
-
policyDigest: policy.policyDigest,
|
|
290
|
-
},
|
|
291
|
-
policy,
|
|
292
|
-
);
|
|
293
|
-
return (
|
|
294
|
-
completion.outcome === result.outcome &&
|
|
295
|
-
completion.summary === result.summary &&
|
|
296
|
-
completion.artifact === result.artifact
|
|
297
|
-
);
|
|
298
|
-
} catch {
|
|
299
|
-
return false;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function recoveredProjectionError(
|
|
304
|
-
active: ActiveDelegation,
|
|
305
|
-
response: SubagentDelegationResponse,
|
|
306
|
-
diagnostic: NonNullable<DelegationFailureDetails['diagnostic']>,
|
|
307
|
-
): string | undefined {
|
|
308
|
-
if (response.agent !== active.agent) {
|
|
309
|
-
return `terminal agent identity is ${JSON.stringify(response.agent)}; expected ${JSON.stringify(active.agent)}`;
|
|
310
|
-
}
|
|
311
|
-
if (response.childIndex !== 0) {
|
|
312
|
-
return `terminal child index is ${JSON.stringify(response.childIndex)}; expected 0`;
|
|
313
|
-
}
|
|
314
|
-
if (
|
|
315
|
-
typeof response.exitCode !== 'number' ||
|
|
316
|
-
!Number.isSafeInteger(response.exitCode) ||
|
|
317
|
-
response.exitCode <= 0
|
|
318
|
-
) {
|
|
319
|
-
return `terminal exit code is ${JSON.stringify(response.exitCode)}; expected a positive safe integer`;
|
|
320
|
-
}
|
|
321
|
-
const execution = response.execution;
|
|
322
|
-
if (!execution) return 'terminal response has no execution projection';
|
|
323
|
-
if (execution.status !== 'failed' || execution.success !== false) {
|
|
324
|
-
return `execution projection is ${JSON.stringify({
|
|
325
|
-
status: execution.status,
|
|
326
|
-
success: execution.success,
|
|
327
|
-
})}; expected failed/false`;
|
|
328
|
-
}
|
|
329
|
-
if (execution.exitCode !== response.exitCode) {
|
|
330
|
-
return `execution exit code ${JSON.stringify(execution.exitCode)} does not match terminal exit code ${JSON.stringify(response.exitCode)}`;
|
|
331
|
-
}
|
|
332
|
-
if (
|
|
333
|
-
typeof response.error !== 'string' ||
|
|
334
|
-
!response.error ||
|
|
335
|
-
typeof execution.error !== 'string' ||
|
|
336
|
-
execution.error !== response.error
|
|
337
|
-
) {
|
|
338
|
-
return 'terminal and execution errors are missing or do not match exactly';
|
|
339
|
-
}
|
|
340
|
-
const warnings = response.warnings as unknown;
|
|
341
|
-
if (
|
|
342
|
-
warnings !== undefined &&
|
|
343
|
-
(!Array.isArray(warnings) ||
|
|
344
|
-
warnings.some(
|
|
345
|
-
(warning) => typeof warning !== 'string' || warning.trim().length > 0,
|
|
346
|
-
))
|
|
347
|
-
) {
|
|
348
|
-
return `terminal response contains warning evidence: ${JSON.stringify(warnings)}`;
|
|
349
|
-
}
|
|
350
|
-
if (
|
|
351
|
-
diagnostic.transcriptToolCount !== undefined &&
|
|
352
|
-
response.toolCount !== undefined &&
|
|
353
|
-
response.toolCount !== diagnostic.transcriptToolCount
|
|
354
|
-
) {
|
|
355
|
-
return `terminal tool count ${response.toolCount} does not match transcript tool count ${diagnostic.transcriptToolCount}`;
|
|
356
|
-
}
|
|
357
|
-
if (
|
|
358
|
-
diagnostic.transcriptTurnCount !== undefined &&
|
|
359
|
-
response.turns !== undefined &&
|
|
360
|
-
response.turns !== diagnostic.transcriptTurnCount
|
|
361
|
-
) {
|
|
362
|
-
return `terminal turn count ${response.turns} does not match transcript turn count ${diagnostic.transcriptTurnCount}`;
|
|
363
|
-
}
|
|
364
|
-
const toolFailure = response.error.match(
|
|
365
|
-
/^\s*([a-z][\w-]*) failed\s*\(exit\s+(\d+)\)\s*:/i,
|
|
366
|
-
);
|
|
367
|
-
if (!toolFailure) {
|
|
368
|
-
return 'terminal error is not a recognized "<tool> failed (exit N): <detail>" failure';
|
|
369
|
-
}
|
|
370
|
-
const terminalTool = toolFailure[1]!;
|
|
371
|
-
const terminalExitCode = Number(toolFailure[2]);
|
|
372
|
-
if (
|
|
373
|
-
terminalTool.toLowerCase() !== diagnostic.tool.toLowerCase() ||
|
|
374
|
-
terminalExitCode !== response.exitCode
|
|
375
|
-
) {
|
|
376
|
-
return `terminal tool/exit ${JSON.stringify({
|
|
377
|
-
tool: terminalTool,
|
|
378
|
-
exitCode: terminalExitCode,
|
|
379
|
-
})} does not match the correlated failure ${JSON.stringify({
|
|
380
|
-
tool: diagnostic.tool,
|
|
381
|
-
exitCode: response.exitCode,
|
|
382
|
-
})}`;
|
|
383
|
-
}
|
|
384
|
-
const unsafeFlag = (
|
|
385
|
-
[
|
|
386
|
-
['interrupted', execution.interrupted],
|
|
387
|
-
['timedOut', execution.timedOut],
|
|
388
|
-
['stopped', execution.stopped],
|
|
389
|
-
['detached', execution.detached],
|
|
390
|
-
] as const
|
|
391
|
-
).find(([, enabled]) => enabled === true)?.[0];
|
|
392
|
-
return unsafeFlag
|
|
393
|
-
? `execution projection reports ${unsafeFlag}=true`
|
|
394
|
-
: undefined;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
interface DelegationFailureDetails {
|
|
398
|
-
reason: string;
|
|
399
|
-
status: SubagentDelegationStatus;
|
|
400
|
-
error?: string;
|
|
401
|
-
exitCode?: number;
|
|
402
|
-
diagnostic?: Awaited<ReturnType<typeof readToolFailureDiagnostic>>;
|
|
403
|
-
replayAudit?: DelegationReplayAudit;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
async function delegationFailureDetails(
|
|
407
|
-
active: ActiveDelegation,
|
|
408
|
-
response: SubagentDelegationResponse,
|
|
409
|
-
): Promise<DelegationFailureDetails> {
|
|
410
|
-
const terminalError = nonEmptyTerminalError(response);
|
|
411
|
-
const error =
|
|
412
|
-
terminalError ?? 'The subagent returned no terminal error details.';
|
|
413
|
-
const responseIdentityMatches =
|
|
414
|
-
response.childIndex === 0 &&
|
|
415
|
-
(response.agent === undefined || response.agent === active.agent);
|
|
416
|
-
const identity =
|
|
417
|
-
responseIdentityMatches && response.runId !== undefined
|
|
418
|
-
? { runId: response.runId, childIndex: 0 }
|
|
419
|
-
: undefined;
|
|
420
|
-
const [diagnostic, replayAudit] = await Promise.all([
|
|
421
|
-
readToolFailureDiagnostic(
|
|
422
|
-
response.sessionFile,
|
|
423
|
-
active.trustedSessionRoot,
|
|
424
|
-
identity,
|
|
425
|
-
failedToolName(terminalError),
|
|
426
|
-
terminalError,
|
|
427
|
-
),
|
|
428
|
-
readDelegationReplayAudit(
|
|
429
|
-
response.sessionFile,
|
|
430
|
-
active.trustedSessionRoot,
|
|
431
|
-
identity,
|
|
432
|
-
{
|
|
433
|
-
task: active.transcriptTask,
|
|
434
|
-
bashPermission: active.policy.permissions.bash,
|
|
435
|
-
approvedBashCommands: active.policy.approvedBashCommands ?? [],
|
|
436
|
-
},
|
|
437
|
-
),
|
|
438
|
-
]);
|
|
439
|
-
const validatedReplayAudit = validateReplayAudit(response, replayAudit);
|
|
440
|
-
const exitCode =
|
|
441
|
-
nonzeroTerminalExitCode(response) ??
|
|
442
|
-
response.exitCode ??
|
|
443
|
-
response.execution?.exitCode;
|
|
444
|
-
const reason = [
|
|
445
|
-
hasContradictoryCompletion(response)
|
|
446
|
-
? `Subagent "${active.agent}" reported terminal failure signals with completed status.`
|
|
447
|
-
: `Subagent "${active.agent}" ${response.status.replaceAll('_', ' ')}.`,
|
|
448
|
-
...(diagnostic ? formatToolFailureDiagnostic(diagnostic) : []),
|
|
449
|
-
...(exitCode !== undefined ? [`Subagent exit code: ${exitCode}`] : []),
|
|
450
|
-
`Terminal error: ${boundedFailureField(error)}`,
|
|
451
|
-
...(diagnostic && response.sessionFile
|
|
452
|
-
? [
|
|
453
|
-
`Diagnostic session: ${boundedFailureField(response.sessionFile.replaceAll(/\s+/g, ' '))}`,
|
|
454
|
-
]
|
|
455
|
-
: []),
|
|
456
|
-
].join('\n');
|
|
457
|
-
return {
|
|
458
|
-
reason,
|
|
459
|
-
status: response.status,
|
|
460
|
-
...(terminalError ? { error: terminalError } : {}),
|
|
461
|
-
...(exitCode !== undefined ? { exitCode } : {}),
|
|
462
|
-
...(diagnostic ? { diagnostic } : {}),
|
|
463
|
-
...(validatedReplayAudit ? { replayAudit: validatedReplayAudit } : {}),
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
interface MainStepIdentity {
|
|
468
|
-
runId: string;
|
|
469
|
-
stepId: string;
|
|
470
|
-
stepDigest: string;
|
|
471
|
-
sessionEpoch: number;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
interface ActivePromptReview {
|
|
475
|
-
requestId: string;
|
|
476
|
-
runId: string;
|
|
477
|
-
stepId: string;
|
|
478
|
-
sessionEpoch: number;
|
|
479
|
-
abortController: AbortController;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
interface WorkflowStartContext {
|
|
483
|
-
context: ExtensionContext;
|
|
484
|
-
skills: () => readonly { name: string }[] | undefined;
|
|
485
|
-
waitForIdle: () => Promise<void>;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
function emptyCatalog(): WorkflowCatalog {
|
|
489
|
-
return {
|
|
490
|
-
workflows: new Map(),
|
|
491
|
-
settings: DEFAULT_SETTINGS,
|
|
492
|
-
diagnostics: [],
|
|
493
|
-
userDirectory: '',
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
function formatDiagnostics(catalog: WorkflowCatalog): string {
|
|
498
|
-
const shown = catalog.diagnostics
|
|
499
|
-
.slice(0, 3)
|
|
500
|
-
.map((item) => `${item.path}: ${item.message}`);
|
|
501
|
-
const remaining = catalog.diagnostics.length - shown.length;
|
|
502
|
-
return [
|
|
503
|
-
...shown,
|
|
504
|
-
...(remaining > 0 ? [`${remaining} more diagnostic(s)`] : []),
|
|
505
|
-
].join('\n');
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
function skillNamesFromSystemPrompt(
|
|
509
|
-
systemPrompt: string,
|
|
510
|
-
): Array<{ name: string }> {
|
|
511
|
-
const sections = [
|
|
512
|
-
...systemPrompt.matchAll(
|
|
513
|
-
/<available_skills>([\s\S]*?)<\/available_skills>/g,
|
|
514
|
-
),
|
|
515
|
-
];
|
|
516
|
-
const section = sections.at(-1)?.[1] ?? '';
|
|
517
|
-
return [...section.matchAll(/<name>([^<]+)<\/name>/g)].map((match) => ({
|
|
518
|
-
name: match[1]!.trim(),
|
|
519
|
-
}));
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
async function waitForEventContextIdle(ctx: ExtensionContext): Promise<void> {
|
|
523
|
-
const deadline = Date.now() + 30_000;
|
|
524
|
-
while (!ctx.isIdle()) {
|
|
525
|
-
if (Date.now() >= deadline) {
|
|
526
|
-
throw new Error('Timed out waiting for the interrupted Pi turn to stop');
|
|
527
|
-
}
|
|
528
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
61
|
+
const STATUS_ACTIONS = createStatusActions();
|
|
62
|
+
const START_ACTIONS = createStartActions();
|
|
63
|
+
const PAUSE_ACTIONS = createPauseActions();
|
|
64
|
+
const RESUME_ACTION = createResumeAction();
|
|
65
|
+
const LIFECYCLE_ACTIONS = createLifecycleActions();
|
|
66
|
+
const STEP_EXECUTION_ACTIONS = createStepExecutionActions();
|
|
67
|
+
const DELEGATION_RESPONSE_ACTIONS = createDelegationResponseActions();
|
|
68
|
+
const DELEGATION_CONTROL_ACTIONS = createDelegationControlActions();
|
|
69
|
+
const GATE_SUBMISSION_ACTION = createGateSubmissionAction();
|
|
70
|
+
const PROMPT_GATE_ACTIONS = createPromptGateActions();
|
|
71
|
+
const PLANNOTATOR_RESULT_ACTIONS = createPlannotatorResultActions();
|
|
72
|
+
const CORE_ACTIONS = createCoreActions();
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Coordinates workflow commands while delegating all external effects through
|
|
76
|
+
* injectable dependencies.
|
|
77
|
+
*/
|
|
532
78
|
export class WorkflowHarness implements WorkflowCommandController {
|
|
533
79
|
private readonly pi: ExtensionAPI;
|
|
534
|
-
private readonly
|
|
535
|
-
private readonly
|
|
536
|
-
private
|
|
80
|
+
private readonly dependencies: WorkflowHarnessDependencies;
|
|
81
|
+
private readonly delegationFailures: DelegationFailureActions;
|
|
82
|
+
private readonly subagents: SubagentDelegationClientController;
|
|
83
|
+
private readonly mainSteps: MainStepRuntimeController;
|
|
84
|
+
private catalog: WorkflowCatalog = createEmptyCatalog();
|
|
537
85
|
private run: WorkflowRun | undefined;
|
|
538
86
|
private latestContext: ExtensionContext | undefined;
|
|
539
87
|
private availableSkills = new Set<string>();
|
|
540
|
-
private
|
|
88
|
+
private isSessionActive = false;
|
|
541
89
|
private sessionEpoch = 0;
|
|
542
90
|
private activeDelegation: ActiveDelegation | undefined;
|
|
543
91
|
private activePromptReview: ActivePromptReview | undefined;
|
|
544
92
|
private registeredWorkflowCommands = new Set<string>();
|
|
545
93
|
private catalogLoadSequence = 0;
|
|
546
|
-
private readonly mutationQueue
|
|
94
|
+
private readonly mutationQueue: SerialTaskQueueController;
|
|
547
95
|
private readonly statusShortcut: KeyId;
|
|
548
96
|
private readonly statusShortcutLabel: string;
|
|
549
97
|
private statusRefreshTimer: ReturnType<typeof setInterval> | undefined;
|
|
550
|
-
private
|
|
98
|
+
private isStatusOverlayOpen = false;
|
|
551
99
|
private legacyProgressWidgetContext: ExtensionContext | undefined;
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
workflowIds(): string[] {
|
|
571
|
-
return [...this.catalog.workflows.keys()].sort();
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
private registerMultilineCommandInput(): void {
|
|
575
|
-
this.pi.on('input', async (event, ctx) => {
|
|
576
|
-
if (
|
|
577
|
-
event.source === 'extension' ||
|
|
578
|
-
event.images?.length ||
|
|
579
|
-
!event.text.startsWith('/')
|
|
580
|
-
) {
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
const newline = event.text.indexOf('\n');
|
|
584
|
-
if (newline === -1) return;
|
|
585
|
-
const command = event.text.slice(1, newline).replace(/\r$/, '');
|
|
586
|
-
if (!this.registeredWorkflowCommands.has(command)) return;
|
|
587
|
-
const workflow = [...this.catalog.workflows.values()].find(
|
|
588
|
-
(candidate) => candidate.definition.command === command,
|
|
589
|
-
);
|
|
590
|
-
if (!workflow) return;
|
|
591
|
-
|
|
592
|
-
const input = event.text.slice(newline + 1);
|
|
593
|
-
const skills = skillNamesFromSystemPrompt(ctx.getSystemPrompt());
|
|
594
|
-
try {
|
|
595
|
-
await this.enqueueMutation(ctx, (sessionEpoch) =>
|
|
596
|
-
this.startNow(
|
|
597
|
-
workflow.definition.id,
|
|
598
|
-
input,
|
|
599
|
-
{
|
|
600
|
-
context: ctx,
|
|
601
|
-
skills: () => skills,
|
|
602
|
-
waitForIdle: () => waitForEventContextIdle(ctx),
|
|
603
|
-
},
|
|
604
|
-
sessionEpoch,
|
|
605
|
-
),
|
|
606
|
-
);
|
|
607
|
-
} catch (error) {
|
|
608
|
-
ctx.ui.notify(
|
|
609
|
-
`Cannot start workflow: ${error instanceof Error ? error.message : String(error)}`,
|
|
610
|
-
'error',
|
|
611
|
-
);
|
|
612
|
-
}
|
|
613
|
-
return { action: 'handled' as const };
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
async list(ctx: ExtensionCommandContext): Promise<void> {
|
|
618
|
-
const workflows = [...this.catalog.workflows.values()].sort((left, right) =>
|
|
619
|
-
left.definition.id.localeCompare(right.definition.id),
|
|
620
|
-
);
|
|
621
|
-
if (workflows.length === 0) {
|
|
622
|
-
ctx.ui.notify(
|
|
623
|
-
`No workflows loaded from ${this.catalog.userDirectory}`,
|
|
624
|
-
this.catalog.diagnostics.length > 0 ? 'warning' : 'info',
|
|
625
|
-
);
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
this.pi.sendMessage({
|
|
629
|
-
customType: 'workflow-list',
|
|
630
|
-
content: formatWorkflowList(
|
|
631
|
-
workflows.map((workflow) => workflow.definition),
|
|
632
|
-
),
|
|
633
|
-
display: true,
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
start(
|
|
638
|
-
workflowId: string,
|
|
639
|
-
input: string,
|
|
640
|
-
ctx: ExtensionCommandContext,
|
|
641
|
-
): Promise<void> {
|
|
642
|
-
return this.enqueueMutation(ctx, (sessionEpoch) =>
|
|
643
|
-
this.startNow(
|
|
644
|
-
workflowId,
|
|
645
|
-
input,
|
|
646
|
-
{
|
|
647
|
-
context: ctx,
|
|
648
|
-
skills: () => ctx.getSystemPromptOptions().skills,
|
|
649
|
-
waitForIdle: () => ctx.waitForIdle(),
|
|
650
|
-
},
|
|
651
|
-
sessionEpoch,
|
|
652
|
-
),
|
|
653
|
-
);
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
private async startNow(
|
|
100
|
+
private readonly workflowStatusSnapshot: () =>
|
|
101
|
+
WorkflowStatusSnapshot | undefined = STATUS_ACTIONS.workflowStatusSnapshot;
|
|
102
|
+
private readonly updateStatus: () => void = STATUS_ACTIONS.updateStatus;
|
|
103
|
+
private readonly refreshStatusWhileRunning: () => void =
|
|
104
|
+
STATUS_ACTIONS.refreshStatusWhileRunning;
|
|
105
|
+
private readonly stopStatusRefresh: () => void =
|
|
106
|
+
STATUS_ACTIONS.stopStatusRefresh;
|
|
107
|
+
private readonly registerWorkflowStatusShortcut: () => void =
|
|
108
|
+
STATUS_ACTIONS.registerWorkflowStatusShortcut;
|
|
109
|
+
private readonly openWorkflowStatus: (context: ExtensionContext) => void =
|
|
110
|
+
STATUS_ACTIONS.openWorkflowStatus;
|
|
111
|
+
private readonly showWorkflowStatus: (
|
|
112
|
+
context: ExtensionContext,
|
|
113
|
+
) => Promise<void> = STATUS_ACTIONS.showWorkflowStatus;
|
|
114
|
+
private readonly listWorkflows: (
|
|
115
|
+
context: ExtensionCommandContext,
|
|
116
|
+
) => Promise<void> = START_ACTIONS.listWorkflows;
|
|
117
|
+
private readonly startNow: (
|
|
657
118
|
workflowId: string,
|
|
658
119
|
input: string,
|
|
659
120
|
startContext: WorkflowStartContext,
|
|
660
121
|
sessionEpoch: number,
|
|
661
|
-
)
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
'warning',
|
|
667
|
-
);
|
|
668
|
-
return;
|
|
669
|
-
}
|
|
670
|
-
if (
|
|
671
|
-
this.run &&
|
|
672
|
-
this.run.status !== 'completed' &&
|
|
673
|
-
this.run.status !== 'aborted'
|
|
674
|
-
) {
|
|
675
|
-
ctx.ui.notify(
|
|
676
|
-
`Workflow "${this.run.workflowId}" is ${this.run.status}; resume or abort it first`,
|
|
677
|
-
'warning',
|
|
678
|
-
);
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
681
|
-
if (!ctx.isIdle()) {
|
|
682
|
-
ctx.abort();
|
|
683
|
-
await startContext.waitForIdle();
|
|
684
|
-
}
|
|
685
|
-
if (!this.sessionActive || this.sessionEpoch !== sessionEpoch) {
|
|
686
|
-
ctx.ui.notify(
|
|
687
|
-
'Workflow start was superseded by a session change',
|
|
688
|
-
'warning',
|
|
689
|
-
);
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
this.captureSkills(startContext.skills());
|
|
694
|
-
if (!(await this.reloadCatalog(ctx, false))) {
|
|
695
|
-
ctx.ui.notify(
|
|
696
|
-
'Workflow start was superseded by a newer configuration load',
|
|
697
|
-
'warning',
|
|
698
|
-
);
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
if (!this.sessionActive || this.sessionEpoch !== sessionEpoch) {
|
|
702
|
-
ctx.ui.notify(
|
|
703
|
-
'Workflow start was superseded by a session change',
|
|
704
|
-
'warning',
|
|
705
|
-
);
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
const workflow = this.catalog.workflows.get(workflowId);
|
|
709
|
-
if (!workflow) {
|
|
710
|
-
ctx.ui.notify(`Workflow "${workflowId}" is not loaded`, 'error');
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
|
-
const preflightErrors = this.preflight(workflow, workflow.definition.start);
|
|
714
|
-
if (preflightErrors.length > 0) {
|
|
715
|
-
ctx.ui.notify(
|
|
716
|
-
`Cannot start workflow:\n${preflightErrors.join('\n')}`,
|
|
717
|
-
'error',
|
|
718
|
-
);
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
const baselineTools = this.pi.getActiveTools();
|
|
723
|
-
this.run = createRun(
|
|
724
|
-
workflow,
|
|
725
|
-
input.trim(),
|
|
726
|
-
baselineTools,
|
|
727
|
-
randomUUID(),
|
|
728
|
-
Date.now(),
|
|
729
|
-
);
|
|
730
|
-
this.persist();
|
|
731
|
-
this.isolateMainSessionTools();
|
|
732
|
-
this.updateStatus();
|
|
733
|
-
this.openWorkflowStatus(ctx);
|
|
734
|
-
this.launchCurrentStep(workflow);
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
pause(reason: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
738
|
-
return this.enqueueMutation(ctx, () => this.pauseNow(reason, ctx));
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
private async pauseNow(
|
|
122
|
+
) => Promise<void> = START_ACTIONS.startNow;
|
|
123
|
+
private readonly reloadNow: (
|
|
124
|
+
context: ExtensionCommandContext,
|
|
125
|
+
) => Promise<void> = START_ACTIONS.reloadNow;
|
|
126
|
+
private readonly pauseNow: (
|
|
742
127
|
reason: string,
|
|
743
|
-
|
|
744
|
-
)
|
|
745
|
-
|
|
746
|
-
!this.run ||
|
|
747
|
-
this.run.status === 'completed' ||
|
|
748
|
-
this.run.status === 'aborted'
|
|
749
|
-
) {
|
|
750
|
-
ctx.ui.notify('No active workflow to pause', 'warning');
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
753
|
-
if (this.run.status === 'paused') {
|
|
754
|
-
ctx.ui.notify(
|
|
755
|
-
`Workflow is already paused${this.run.pauseReason ? `: ${this.run.pauseReason}` : ''}`,
|
|
756
|
-
'info',
|
|
757
|
-
);
|
|
758
|
-
return;
|
|
759
|
-
}
|
|
760
|
-
this.cancelPromptReview();
|
|
761
|
-
const mainSuspended = this.mainSteps.suspend();
|
|
762
|
-
const cancellationConfirmed = await this.cancelActiveDelegation(
|
|
763
|
-
'Workflow paused by user',
|
|
764
|
-
);
|
|
765
|
-
if (!ctx.isIdle()) {
|
|
766
|
-
ctx.abort();
|
|
767
|
-
if (mainSuspended) await ctx.waitForIdle();
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
this.run = pauseRun(this.run, reason, Date.now());
|
|
771
|
-
this.persist();
|
|
772
|
-
if (cancellationConfirmed) {
|
|
773
|
-
this.restoreBaselineTools();
|
|
774
|
-
} else {
|
|
775
|
-
this.isolateMainSessionTools();
|
|
776
|
-
}
|
|
777
|
-
this.updateStatus();
|
|
778
|
-
ctx.ui.notify(
|
|
779
|
-
cancellationConfirmed
|
|
780
|
-
? `Paused "${this.run.workflowId}" at step "${this.run.currentStepId}". Fix the issue, then run /workflow-resume.`
|
|
781
|
-
: `Pause recorded at "${this.run.currentStepId}", but child cancellation is not confirmed. Main tools remain isolated until it exits.`,
|
|
782
|
-
cancellationConfirmed ? 'info' : 'warning',
|
|
783
|
-
);
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
resume(ctx: ExtensionCommandContext): Promise<void> {
|
|
787
|
-
return this.enqueueMutation(ctx, () => this.resumeNow(ctx));
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
private async resumeNow(ctx: ExtensionCommandContext): Promise<void> {
|
|
791
|
-
if (!this.run || this.run.status !== 'paused') {
|
|
792
|
-
ctx.ui.notify('No paused workflow to resume', 'warning');
|
|
793
|
-
return;
|
|
794
|
-
}
|
|
795
|
-
if (this.activeDelegation) {
|
|
796
|
-
ctx.ui.notify(
|
|
797
|
-
`Cannot resume while subagent "${this.activeDelegation.agent}" is still cancelling`,
|
|
798
|
-
'warning',
|
|
799
|
-
);
|
|
800
|
-
return;
|
|
801
|
-
}
|
|
802
|
-
const checkpoint = captureResumeCheckpoint(this.run, this.sessionEpoch);
|
|
803
|
-
if (!ctx.isIdle()) {
|
|
804
|
-
ctx.abort();
|
|
805
|
-
await ctx.waitForIdle();
|
|
806
|
-
}
|
|
807
|
-
if (!matchesResumeCheckpoint(this.run, this.sessionEpoch, checkpoint)) {
|
|
808
|
-
ctx.ui.notify(
|
|
809
|
-
'Resume was superseded by another workflow or session change',
|
|
810
|
-
'warning',
|
|
811
|
-
);
|
|
812
|
-
return;
|
|
813
|
-
}
|
|
814
|
-
this.captureSkills(ctx.getSystemPromptOptions().skills);
|
|
815
|
-
await this.reloadCatalog(ctx, false);
|
|
816
|
-
if (!matchesResumeCheckpoint(this.run, this.sessionEpoch, checkpoint)) {
|
|
817
|
-
ctx.ui.notify(
|
|
818
|
-
'Resume was superseded by another workflow or session change',
|
|
819
|
-
'warning',
|
|
820
|
-
);
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
let workflow = this.catalog.workflows.get(this.run.workflowId);
|
|
825
|
-
if (!workflow) {
|
|
826
|
-
ctx.ui.notify(
|
|
827
|
-
`Workflow "${this.run.workflowId}" is no longer loaded; restore it or abort`,
|
|
828
|
-
'error',
|
|
829
|
-
);
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
const reconciled = reconcileRun(this.run, workflow, Date.now());
|
|
833
|
-
if (!reconciled.run) {
|
|
834
|
-
ctx.ui.notify(
|
|
835
|
-
reconciled.error ?? 'Cannot reconcile workflow configuration',
|
|
836
|
-
'error',
|
|
837
|
-
);
|
|
838
|
-
return;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
let resumed = reconciled.run;
|
|
842
|
-
if (
|
|
843
|
-
resumed.pendingGate?.provider === 'plannotator' &&
|
|
844
|
-
!resumed.pendingGate.reviewId
|
|
845
|
-
) {
|
|
846
|
-
resumed = failGate(
|
|
847
|
-
resumed,
|
|
848
|
-
'Gate submission was interrupted before a review id was recorded; submit it again',
|
|
849
|
-
Date.now(),
|
|
850
|
-
);
|
|
851
|
-
}
|
|
852
|
-
if (
|
|
853
|
-
resumed.pendingGate?.provider === 'plannotator' &&
|
|
854
|
-
resumed.pendingGate.reviewId &&
|
|
855
|
-
!resumed.pendingGate.resolution
|
|
856
|
-
) {
|
|
857
|
-
const requestedReviewId = resumed.pendingGate.reviewId;
|
|
858
|
-
const gateStep = workflow.definition.steps[resumed.pendingGate.stepId];
|
|
859
|
-
const statusResponse = await requestPlannotatorReviewStatus(
|
|
860
|
-
this.pi.events,
|
|
861
|
-
`${resumed.runId}:review-status:${randomUUID()}`,
|
|
862
|
-
requestedReviewId,
|
|
863
|
-
gateStep?.gate?.provider === 'plannotator'
|
|
864
|
-
? gateStep.gate.timeoutMs
|
|
865
|
-
: 5_000,
|
|
866
|
-
);
|
|
867
|
-
if (!matchesResumeCheckpoint(this.run, this.sessionEpoch, checkpoint)) {
|
|
868
|
-
ctx.ui.notify(
|
|
869
|
-
'Resume was superseded by another workflow or session change',
|
|
870
|
-
'warning',
|
|
871
|
-
);
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
workflow = this.catalog.workflows.get(this.run.workflowId);
|
|
876
|
-
if (!workflow) {
|
|
877
|
-
ctx.ui.notify(
|
|
878
|
-
`Workflow "${this.run.workflowId}" is no longer loaded; restore it or abort`,
|
|
879
|
-
'error',
|
|
880
|
-
);
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
883
|
-
const latest = reconcileRun(this.run, workflow, Date.now());
|
|
884
|
-
if (!latest.run) {
|
|
885
|
-
ctx.ui.notify(
|
|
886
|
-
latest.error ?? 'Cannot reconcile workflow configuration',
|
|
887
|
-
'error',
|
|
888
|
-
);
|
|
889
|
-
return;
|
|
890
|
-
}
|
|
891
|
-
resumed = latest.run;
|
|
892
|
-
|
|
893
|
-
if (
|
|
894
|
-
!resumed.pendingGate?.resolution &&
|
|
895
|
-
resumed.pendingGate?.reviewId === requestedReviewId &&
|
|
896
|
-
statusResponse.status !== 'handled'
|
|
897
|
-
) {
|
|
898
|
-
ctx.ui.notify(
|
|
899
|
-
statusResponse.error ?? 'Cannot query the pending Plannotator review',
|
|
900
|
-
'error',
|
|
901
|
-
);
|
|
902
|
-
return;
|
|
903
|
-
}
|
|
904
|
-
if (
|
|
905
|
-
!resumed.pendingGate?.resolution &&
|
|
906
|
-
resumed.pendingGate?.reviewId === requestedReviewId &&
|
|
907
|
-
statusResponse.status === 'handled' &&
|
|
908
|
-
statusResponse.result.status === 'completed'
|
|
909
|
-
) {
|
|
910
|
-
resumed = storeGateResolution(
|
|
911
|
-
resumed,
|
|
912
|
-
{
|
|
913
|
-
approved: statusResponse.result.approved,
|
|
914
|
-
feedback: statusResponse.result.feedback,
|
|
915
|
-
resolvedAt: Date.now(),
|
|
916
|
-
},
|
|
917
|
-
Date.now(),
|
|
918
|
-
);
|
|
919
|
-
} else if (
|
|
920
|
-
!resumed.pendingGate?.resolution &&
|
|
921
|
-
resumed.pendingGate?.reviewId === requestedReviewId &&
|
|
922
|
-
statusResponse.status === 'handled' &&
|
|
923
|
-
statusResponse.result.status === 'missing'
|
|
924
|
-
) {
|
|
925
|
-
resumed = failGate(
|
|
926
|
-
resumed,
|
|
927
|
-
'Plannotator no longer has the pending review; submit it again',
|
|
928
|
-
Date.now(),
|
|
929
|
-
);
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
const storedResolution = resumed.pendingGate?.resolution;
|
|
933
|
-
if (storedResolution) {
|
|
934
|
-
try {
|
|
935
|
-
resumed = resolveGate(workflow, resumed, storedResolution, Date.now());
|
|
936
|
-
} catch (error) {
|
|
937
|
-
ctx.ui.notify(
|
|
938
|
-
`Cannot apply stored gate result: ${error instanceof Error ? error.message : String(error)}`,
|
|
939
|
-
'error',
|
|
940
|
-
);
|
|
941
|
-
return;
|
|
942
|
-
}
|
|
943
|
-
} else {
|
|
944
|
-
resumed = resumeRun(resumed, Date.now());
|
|
945
|
-
}
|
|
946
|
-
this.run = resumed;
|
|
947
|
-
|
|
948
|
-
if (this.run.status === 'awaiting-gate') {
|
|
949
|
-
this.persist();
|
|
950
|
-
this.restoreBaselineTools();
|
|
951
|
-
this.updateStatus();
|
|
952
|
-
if (this.run.pendingGate?.provider === 'prompt') {
|
|
953
|
-
this.launchPromptReview(workflow, this.run, ctx);
|
|
954
|
-
ctx.ui.notify('Workflow resumed with built-in review open', 'info');
|
|
955
|
-
return;
|
|
956
|
-
}
|
|
957
|
-
ctx.ui.notify(
|
|
958
|
-
`Workflow resumed and is waiting for review ${this.run.pendingGate?.reviewId ?? ''}`.trim(),
|
|
959
|
-
'info',
|
|
960
|
-
);
|
|
961
|
-
return;
|
|
962
|
-
}
|
|
963
|
-
if (this.run.status !== 'running') {
|
|
964
|
-
this.persist();
|
|
965
|
-
this.restoreBaselineTools();
|
|
966
|
-
this.updateStatus();
|
|
967
|
-
ctx.ui.notify(`Workflow is now ${this.run.status}`, 'info');
|
|
968
|
-
return;
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
const preflightErrors = this.preflight(workflow, this.run.currentStepId);
|
|
972
|
-
if (preflightErrors.length > 0) {
|
|
973
|
-
this.run = failRun(
|
|
974
|
-
this.run,
|
|
975
|
-
`Step preflight failed: ${preflightErrors.join('; ')}`,
|
|
976
|
-
Date.now(),
|
|
977
|
-
);
|
|
978
|
-
this.persist();
|
|
979
|
-
this.restoreBaselineTools();
|
|
980
|
-
this.updateStatus();
|
|
981
|
-
ctx.ui.notify(
|
|
982
|
-
`Cannot resume workflow:\n${preflightErrors.join('\n')}`,
|
|
983
|
-
'error',
|
|
984
|
-
);
|
|
985
|
-
return;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
this.persist();
|
|
989
|
-
this.isolateMainSessionTools();
|
|
990
|
-
this.updateStatus();
|
|
991
|
-
this.launchCurrentStep(workflow);
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
abort(reason: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
995
|
-
return this.enqueueMutation(ctx, () => this.abortNow(reason, ctx));
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
private async abortNow(
|
|
128
|
+
context: ExtensionCommandContext,
|
|
129
|
+
) => Promise<void> = PAUSE_ACTIONS.pauseNow;
|
|
130
|
+
private readonly abortNow: (
|
|
999
131
|
reason: string,
|
|
1000
|
-
|
|
1001
|
-
)
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
const cancellationConfirmed = await this.cancelActiveDelegation(
|
|
1013
|
-
'Workflow aborted by user',
|
|
1014
|
-
);
|
|
1015
|
-
if (!ctx.isIdle()) {
|
|
1016
|
-
ctx.abort();
|
|
1017
|
-
if (mainSuspended) await ctx.waitForIdle();
|
|
1018
|
-
}
|
|
1019
|
-
this.run = abortRun(this.run, reason, Date.now());
|
|
1020
|
-
this.persist();
|
|
1021
|
-
if (cancellationConfirmed) {
|
|
1022
|
-
this.restoreBaselineTools();
|
|
1023
|
-
} else {
|
|
1024
|
-
this.isolateMainSessionTools();
|
|
1025
|
-
}
|
|
1026
|
-
this.updateStatus();
|
|
1027
|
-
ctx.ui.notify(
|
|
1028
|
-
cancellationConfirmed
|
|
1029
|
-
? `Aborted workflow "${this.run.workflowId}"`
|
|
1030
|
-
: `Workflow "${this.run.workflowId}" is aborted, but its child has not confirmed cancellation; main tools remain isolated`,
|
|
1031
|
-
cancellationConfirmed ? 'info' : 'warning',
|
|
1032
|
-
);
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
reload(ctx: ExtensionCommandContext): Promise<void> {
|
|
1036
|
-
return this.enqueueMutation(ctx, () => this.reloadNow(ctx));
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
private async reloadNow(ctx: ExtensionCommandContext): Promise<void> {
|
|
1040
|
-
if (
|
|
1041
|
-
this.run &&
|
|
1042
|
-
(this.run.status === 'running' || this.run.status === 'awaiting-gate')
|
|
1043
|
-
) {
|
|
1044
|
-
ctx.ui.notify(
|
|
1045
|
-
'Pause the workflow before reloading its configuration',
|
|
1046
|
-
'warning',
|
|
1047
|
-
);
|
|
1048
|
-
return;
|
|
1049
|
-
}
|
|
1050
|
-
this.captureSkills(ctx.getSystemPromptOptions().skills);
|
|
1051
|
-
await this.reloadCatalog(ctx, true);
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
private workflowStatusSnapshot(): WorkflowStatusSnapshot | undefined {
|
|
1055
|
-
if (!this.run) return undefined;
|
|
1056
|
-
const workflow = this.catalog.workflows.get(this.run.workflowId);
|
|
1057
|
-
let execution: WorkflowStatusExecution | undefined;
|
|
1058
|
-
if (this.activeDelegation) {
|
|
1059
|
-
execution = {
|
|
1060
|
-
kind: 'subagent',
|
|
1061
|
-
agent: this.activeDelegation.agent,
|
|
1062
|
-
requestId: this.activeDelegation.requestId,
|
|
1063
|
-
progress: this.activeDelegation.progress ?? 'starting',
|
|
1064
|
-
};
|
|
1065
|
-
} else if (this.mainSteps.activeStepId) {
|
|
1066
|
-
execution = { kind: 'main' };
|
|
1067
|
-
}
|
|
1068
|
-
return {
|
|
1069
|
-
run: this.run,
|
|
1070
|
-
now: Date.now(),
|
|
1071
|
-
...(workflow ? { workflow } : {}),
|
|
1072
|
-
...(execution ? { execution } : {}),
|
|
1073
|
-
};
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
private registerLifecycle(): void {
|
|
1077
|
-
this.pi.on('session_start', async (_event, ctx) => {
|
|
1078
|
-
this.sessionEpoch += 1;
|
|
1079
|
-
this.sessionActive = false;
|
|
1080
|
-
this.cancelPromptReview();
|
|
1081
|
-
this.mainSteps.deactivate();
|
|
1082
|
-
await this.cancelActiveDelegation('Pi session changed');
|
|
1083
|
-
if (this.run) this.restoreBaselineTools();
|
|
1084
|
-
this.run = undefined;
|
|
1085
|
-
this.latestContext = ctx;
|
|
1086
|
-
if (!(await this.reloadCatalog(ctx, false))) return;
|
|
1087
|
-
this.restoreFromSession(ctx);
|
|
1088
|
-
this.sessionActive = true;
|
|
1089
|
-
});
|
|
1090
|
-
|
|
1091
|
-
this.pi.on('session_tree', async (_event, ctx) => {
|
|
1092
|
-
this.sessionEpoch += 1;
|
|
1093
|
-
this.sessionActive = false;
|
|
1094
|
-
this.cancelPromptReview();
|
|
1095
|
-
this.mainSteps.deactivate();
|
|
1096
|
-
await this.cancelActiveDelegation('Pi session tree changed');
|
|
1097
|
-
this.latestContext = ctx;
|
|
1098
|
-
if (!(await this.reloadCatalog(ctx, false))) return;
|
|
1099
|
-
this.restoreFromSession(ctx);
|
|
1100
|
-
this.sessionActive = true;
|
|
1101
|
-
});
|
|
1102
|
-
|
|
1103
|
-
this.pi.on('session_shutdown', async () => {
|
|
1104
|
-
this.sessionEpoch += 1;
|
|
1105
|
-
this.sessionActive = false;
|
|
1106
|
-
this.cancelPromptReview();
|
|
1107
|
-
this.mainSteps.deactivate();
|
|
1108
|
-
await this.cancelActiveDelegation('Pi session shut down');
|
|
1109
|
-
if (this.run) this.restoreBaselineTools();
|
|
1110
|
-
this.run = undefined;
|
|
1111
|
-
this.latestContext = undefined;
|
|
1112
|
-
this.stopStatusRefresh();
|
|
1113
|
-
});
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
private registerPolicy(): void {
|
|
1117
|
-
this.pi.on('before_agent_start', (event, ctx) => {
|
|
1118
|
-
this.latestContext = ctx;
|
|
1119
|
-
this.captureSkills(event.systemPromptOptions.skills);
|
|
1120
|
-
if (!this.run || this.run.status !== 'running') return;
|
|
1121
|
-
const workflow = this.catalog.workflows.get(this.run.workflowId);
|
|
1122
|
-
if (!workflow) {
|
|
1123
|
-
this.run = failRun(
|
|
1124
|
-
this.run,
|
|
1125
|
-
'Workflow configuration disappeared; reload or restore it',
|
|
1126
|
-
Date.now(),
|
|
1127
|
-
);
|
|
1128
|
-
this.persist();
|
|
1129
|
-
this.restoreBaselineTools();
|
|
1130
|
-
this.updateStatus();
|
|
1131
|
-
return;
|
|
1132
|
-
}
|
|
1133
|
-
return {
|
|
1134
|
-
systemPrompt: `${event.systemPrompt}\n\n${buildMainWorkflowNotice(workflow, this.run, this.statusShortcutLabel)}`,
|
|
1135
|
-
};
|
|
1136
|
-
});
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
private launchCurrentStep(
|
|
132
|
+
context: ExtensionCommandContext,
|
|
133
|
+
) => Promise<void> = PAUSE_ACTIONS.abortNow;
|
|
134
|
+
private readonly resumeNow: (
|
|
135
|
+
context: ExtensionCommandContext,
|
|
136
|
+
) => Promise<void> = RESUME_ACTION.resumeNow;
|
|
137
|
+
private readonly registerMultilineCommandInput: () => void =
|
|
138
|
+
LIFECYCLE_ACTIONS.registerMultilineCommandInput;
|
|
139
|
+
private readonly registerLifecycle: () => void =
|
|
140
|
+
LIFECYCLE_ACTIONS.registerLifecycle;
|
|
141
|
+
private readonly registerPolicy: () => void =
|
|
142
|
+
LIFECYCLE_ACTIONS.registerPolicy;
|
|
143
|
+
private readonly launchCurrentStep: (
|
|
1140
144
|
workflow: LoadedWorkflow,
|
|
1141
|
-
|
|
1142
|
-
)
|
|
1143
|
-
|
|
1144
|
-
if (
|
|
1145
|
-
!run ||
|
|
1146
|
-
run.status !== 'running' ||
|
|
1147
|
-
this.activeDelegation ||
|
|
1148
|
-
this.mainSteps.activeStepId
|
|
1149
|
-
) {
|
|
1150
|
-
return;
|
|
1151
|
-
}
|
|
1152
|
-
const step = workflow.definition.steps[run.currentStepId];
|
|
1153
|
-
if (!step) {
|
|
1154
|
-
this.pauseForExecutionFailure(
|
|
1155
|
-
'Workflow',
|
|
1156
|
-
`Step "${run.currentStepId}" is missing from the workflow`,
|
|
1157
|
-
);
|
|
1158
|
-
return;
|
|
1159
|
-
}
|
|
1160
|
-
const subagent = step.subagent;
|
|
1161
|
-
if (!subagent) {
|
|
1162
|
-
this.launchMainStep(workflow, run, step);
|
|
1163
|
-
return;
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
const reviewedRepository = resolveReviewedRepositoryCwd(
|
|
1167
|
-
run.reviewedArtifact ?? '',
|
|
1168
|
-
);
|
|
1169
|
-
if (reviewedRepository.kind === 'invalid') {
|
|
1170
|
-
this.pauseForExecutionFailure('Subagent step', reviewedRepository.reason);
|
|
1171
|
-
return;
|
|
1172
|
-
}
|
|
1173
|
-
const delegationCwd =
|
|
1174
|
-
reviewedRepository.kind === 'resolved'
|
|
1175
|
-
? reviewedRepository.cwd
|
|
1176
|
-
: (this.latestContext?.cwd ?? process.cwd());
|
|
1177
|
-
const runtimeAgent = subagent.agent;
|
|
1178
|
-
const requestId = `${run.runId}:${run.currentStepId}:${randomUUID()}`;
|
|
1179
|
-
const resultDirectory = mkdtempSync(join(tmpdir(), 'pi-workflows-step-'));
|
|
1180
|
-
const capabilityPath = join(resultDirectory, 'capability');
|
|
1181
|
-
const capabilityToken = randomBytes(32).toString('hex');
|
|
1182
|
-
const resultPath = join(resultDirectory, 'result.json');
|
|
1183
|
-
writeFileSync(capabilityPath, capabilityToken, {
|
|
1184
|
-
encoding: 'utf8',
|
|
1185
|
-
flag: 'wx',
|
|
1186
|
-
mode: 0o600,
|
|
1187
|
-
});
|
|
1188
|
-
const outcomes = allowedOutcomes(workflow, run);
|
|
1189
|
-
const outcomeSet = new Set(outcomes);
|
|
1190
|
-
const approvedBashCommands = narrowApprovedBashCommands(
|
|
1191
|
-
run.reviewedArtifact ?? '',
|
|
1192
|
-
run.stepHandoff ?? '',
|
|
1193
|
-
step.permissions.bash.approvedSources ?? [],
|
|
1194
|
-
);
|
|
1195
|
-
const repositoryPolicy =
|
|
1196
|
-
reviewedRepository.kind === 'resolved'
|
|
1197
|
-
? {
|
|
1198
|
-
repositoryCwd: reviewedRepository.repositoryCwd,
|
|
1199
|
-
...(reviewedRepository.bootstrapping
|
|
1200
|
-
? { bootstrapCwd: reviewedRepository.cwd }
|
|
1201
|
-
: {}),
|
|
1202
|
-
}
|
|
1203
|
-
: {};
|
|
1204
|
-
const policyDigest = digest({
|
|
1205
|
-
version: 1,
|
|
1206
|
-
requestId,
|
|
1207
|
-
agent: runtimeAgent,
|
|
1208
|
-
runId: run.runId,
|
|
1209
|
-
stepId: run.currentStepId,
|
|
1210
|
-
stepDigest: run.currentStepDigest,
|
|
1211
|
-
capabilityPath,
|
|
1212
|
-
resultPath,
|
|
1213
|
-
approvedBashCommands,
|
|
1214
|
-
...repositoryPolicy,
|
|
1215
|
-
});
|
|
1216
|
-
const policy: ChildStepPolicy = {
|
|
1217
|
-
version: 1,
|
|
1218
|
-
requestId,
|
|
1219
|
-
agent: runtimeAgent,
|
|
1220
|
-
workflowId: workflow.definition.id,
|
|
1221
|
-
runId: run.runId,
|
|
1222
|
-
stepId: run.currentStepId,
|
|
1223
|
-
stepTitle: step.title,
|
|
1224
|
-
policyDigest,
|
|
1225
|
-
capabilityPath,
|
|
1226
|
-
capabilityToken,
|
|
1227
|
-
resultPath,
|
|
1228
|
-
permissions: structuredClone(step.permissions),
|
|
1229
|
-
...(approvedBashCommands.length > 0 ? { approvedBashCommands } : {}),
|
|
1230
|
-
...repositoryPolicy,
|
|
1231
|
-
outcomes,
|
|
1232
|
-
pauseOutcomes: Object.entries(step.transitions)
|
|
1233
|
-
.filter(
|
|
1234
|
-
([outcome, target]) => target === '$pause' && outcomeSet.has(outcome),
|
|
1235
|
-
)
|
|
1236
|
-
.map(([outcome]) => outcome),
|
|
1237
|
-
summaryMaxChars: workflow.definition.summaryMaxChars,
|
|
1238
|
-
...(step.gate ? { gateSubmitOutcome: step.gate.submitOutcome } : {}),
|
|
1239
|
-
};
|
|
1240
|
-
const trustedSessionRoot = deriveSubagentSessionRoot(
|
|
1241
|
-
this.latestContext?.sessionManager.getSessionFile(),
|
|
1242
|
-
);
|
|
1243
|
-
const transcriptTask = [
|
|
1244
|
-
buildDelegatedStepTask(workflow, run, ''),
|
|
1245
|
-
delegationTranscriptBinding(requestId, policyDigest),
|
|
1246
|
-
...(reinforcementRetry
|
|
1247
|
-
? [
|
|
1248
|
-
reinforcementRetryTask(
|
|
1249
|
-
reinforcementRetry.reason,
|
|
1250
|
-
reinforcementRetry.count,
|
|
1251
|
-
MAX_REINFORCEMENT_RETRIES,
|
|
1252
|
-
),
|
|
1253
|
-
]
|
|
1254
|
-
: []),
|
|
1255
|
-
].join('\n\n');
|
|
1256
|
-
const active: ActiveDelegation = {
|
|
1257
|
-
requestId,
|
|
1258
|
-
runId: run.runId,
|
|
1259
|
-
stepId: run.currentStepId,
|
|
1260
|
-
stepDigest: run.currentStepDigest,
|
|
1261
|
-
sessionEpoch: this.sessionEpoch,
|
|
1262
|
-
resultDirectory,
|
|
1263
|
-
policy,
|
|
1264
|
-
transcriptTask,
|
|
1265
|
-
agent: subagent.agent,
|
|
1266
|
-
...(trustedSessionRoot ? { trustedSessionRoot } : {}),
|
|
1267
|
-
reinforcementReplayAuthorized: subagent.retryToolFailures,
|
|
1268
|
-
reinforcementRetryCount: reinforcementRetry?.count ?? 0,
|
|
1269
|
-
};
|
|
1270
|
-
const request: SubagentDelegationRequest = {
|
|
1271
|
-
version: 1,
|
|
1272
|
-
requestId,
|
|
1273
|
-
agent: runtimeAgent,
|
|
1274
|
-
task: `${encodeChildPolicy(policy)}\n\n${transcriptTask}`,
|
|
1275
|
-
// Workflow steps are isolation boundaries. Never fork the parent or a
|
|
1276
|
-
// sibling step's transcript; pass only the explicit workflow handoff.
|
|
1277
|
-
context: 'fresh',
|
|
1278
|
-
cwd: delegationCwd,
|
|
1279
|
-
timeoutMs: subagent.timeoutMs,
|
|
1280
|
-
skill:
|
|
1281
|
-
step.permissions.skills.length > 0
|
|
1282
|
-
? [...step.permissions.skills]
|
|
1283
|
-
: false,
|
|
1284
|
-
output: false,
|
|
1285
|
-
outputSchema: WORKFLOW_COMPLETION_PARAMETERS as unknown as Record<
|
|
1286
|
-
string,
|
|
1287
|
-
unknown
|
|
1288
|
-
>,
|
|
1289
|
-
agentContract: { version: 1 },
|
|
1290
|
-
artifacts: subagent.artifacts,
|
|
1291
|
-
...(subagent.model ? { model: subagent.model } : {}),
|
|
1292
|
-
...(subagent.turnBudget
|
|
1293
|
-
? { turnBudget: structuredClone(subagent.turnBudget) }
|
|
1294
|
-
: {}),
|
|
1295
|
-
...(subagent.toolBudget
|
|
1296
|
-
? { toolBudget: structuredClone(subagent.toolBudget) }
|
|
1297
|
-
: {}),
|
|
1298
|
-
};
|
|
1299
|
-
|
|
1300
|
-
this.activeDelegation = active;
|
|
1301
|
-
this.updateStatus();
|
|
1302
|
-
this.latestContext?.ui.notify(
|
|
1303
|
-
`Delegated "${run.currentStepId}" to subagent "${subagent.agent}"`,
|
|
1304
|
-
'info',
|
|
1305
|
-
);
|
|
1306
|
-
void this.subagents
|
|
1307
|
-
.delegate(request, {
|
|
1308
|
-
onUpdate: (update) => this.handleDelegationUpdate(active, update),
|
|
1309
|
-
onLateTerminal: (response) =>
|
|
1310
|
-
this.queueDelegationResponse(active, response),
|
|
1311
|
-
})
|
|
1312
|
-
.then(
|
|
1313
|
-
(response) => this.queueDelegationResponse(active, response),
|
|
1314
|
-
(error: unknown) =>
|
|
1315
|
-
this.queueDelegationFailure(
|
|
1316
|
-
active,
|
|
1317
|
-
error instanceof Error ? error.message : String(error),
|
|
1318
|
-
),
|
|
1319
|
-
);
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
private launchMainStep(
|
|
145
|
+
recovery?: DelegationRecovery,
|
|
146
|
+
) => void = STEP_EXECUTION_ACTIONS.launchCurrentStep;
|
|
147
|
+
private readonly launchMainStep: (
|
|
1323
148
|
workflow: LoadedWorkflow,
|
|
1324
149
|
run: WorkflowRun,
|
|
1325
150
|
step: WorkflowStep,
|
|
1326
|
-
)
|
|
1327
|
-
|
|
1328
|
-
run.reviewedArtifact ?? '',
|
|
1329
|
-
run.stepHandoff ?? '',
|
|
1330
|
-
step.permissions.bash.approvedSources ?? [],
|
|
1331
|
-
);
|
|
1332
|
-
const identity: MainStepIdentity = {
|
|
1333
|
-
runId: run.runId,
|
|
1334
|
-
stepId: run.currentStepId,
|
|
1335
|
-
stepDigest: run.currentStepDigest,
|
|
1336
|
-
sessionEpoch: this.sessionEpoch,
|
|
1337
|
-
};
|
|
1338
|
-
const policyDigest = digest({
|
|
1339
|
-
version: 1,
|
|
1340
|
-
execution: 'main',
|
|
1341
|
-
workflowId: workflow.definition.id,
|
|
1342
|
-
runId: run.runId,
|
|
1343
|
-
stepId: run.currentStepId,
|
|
1344
|
-
stepDigest: run.currentStepDigest,
|
|
1345
|
-
permissions: step.permissions,
|
|
1346
|
-
approvedBashCommands,
|
|
1347
|
-
nonce: randomUUID(),
|
|
1348
|
-
});
|
|
1349
|
-
const execution: MainStepExecution = {
|
|
1350
|
-
workflowId: workflow.definition.id,
|
|
1351
|
-
runId: run.runId,
|
|
1352
|
-
stepId: run.currentStepId,
|
|
1353
|
-
stepDigest: run.currentStepDigest,
|
|
1354
|
-
policyDigest,
|
|
1355
|
-
step: structuredClone(step),
|
|
1356
|
-
approvedBashCommands,
|
|
1357
|
-
outcomes: allowedOutcomes(workflow, run),
|
|
1358
|
-
summaryMaxChars: workflow.definition.summaryMaxChars,
|
|
1359
|
-
...(step.gate ? { gateSubmitOutcome: step.gate.submitOutcome } : {}),
|
|
1360
|
-
onSettled: (result, context) =>
|
|
1361
|
-
this.queueMainStepResult(identity, result, context),
|
|
1362
|
-
};
|
|
1363
|
-
|
|
1364
|
-
try {
|
|
1365
|
-
this.mainSteps.activate(execution);
|
|
1366
|
-
this.updateStatus();
|
|
1367
|
-
this.latestContext?.ui.notify(
|
|
1368
|
-
`Started "${run.currentStepId}" in the main agent`,
|
|
1369
|
-
'info',
|
|
1370
|
-
);
|
|
1371
|
-
this.pi.sendUserMessage(buildMainStepTask(workflow, run), {
|
|
1372
|
-
deliverAs: 'followUp',
|
|
1373
|
-
});
|
|
1374
|
-
} catch (error) {
|
|
1375
|
-
this.mainSteps.deactivate();
|
|
1376
|
-
this.pauseForExecutionFailure(
|
|
1377
|
-
'Main-agent step',
|
|
1378
|
-
error instanceof Error ? error.message : String(error),
|
|
1379
|
-
);
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
private queueMainStepResult(
|
|
151
|
+
) => void = STEP_EXECUTION_ACTIONS.launchMainStep;
|
|
152
|
+
private readonly queueMainStepResult: (
|
|
1384
153
|
identity: MainStepIdentity,
|
|
1385
154
|
result: WorkflowStepResult | undefined,
|
|
1386
155
|
context: ExtensionContext,
|
|
1387
|
-
)
|
|
1388
|
-
|
|
1389
|
-
.run(() => this.finishMainStep(identity, result, context))
|
|
1390
|
-
.catch((error: unknown) => {
|
|
1391
|
-
this.pauseForExecutionFailure(
|
|
1392
|
-
'Main-agent step',
|
|
1393
|
-
error instanceof Error ? error.message : String(error),
|
|
1394
|
-
);
|
|
1395
|
-
});
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
private async finishMainStep(
|
|
156
|
+
) => Promise<void> = STEP_EXECUTION_ACTIONS.queueMainStepResult;
|
|
157
|
+
private readonly finishMainStep: (
|
|
1399
158
|
identity: MainStepIdentity,
|
|
1400
159
|
result: WorkflowStepResult | undefined,
|
|
1401
160
|
context: ExtensionContext,
|
|
1402
|
-
)
|
|
1403
|
-
|
|
1404
|
-
if (
|
|
1405
|
-
!this.sessionActive ||
|
|
1406
|
-
this.sessionEpoch !== identity.sessionEpoch ||
|
|
1407
|
-
!this.run ||
|
|
1408
|
-
this.run.status !== 'running' ||
|
|
1409
|
-
this.run.runId !== identity.runId ||
|
|
1410
|
-
this.run.currentStepId !== identity.stepId ||
|
|
1411
|
-
this.run.currentStepDigest !== identity.stepDigest
|
|
1412
|
-
) {
|
|
1413
|
-
return;
|
|
1414
|
-
}
|
|
1415
|
-
if (!result) {
|
|
1416
|
-
throw new Error(
|
|
1417
|
-
'agent settled without calling workflow_complete_step exactly once',
|
|
1418
|
-
);
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
const workflow = this.catalog.workflows.get(this.run.workflowId);
|
|
1422
|
-
const step = workflow?.definition.steps[this.run.currentStepId];
|
|
1423
|
-
if (!workflow || !step) {
|
|
1424
|
-
throw new Error('Active workflow configuration is unavailable');
|
|
1425
|
-
}
|
|
1426
|
-
if (step.gate?.submitOutcome === result.outcome) {
|
|
1427
|
-
await this.submitGate(
|
|
1428
|
-
workflow,
|
|
1429
|
-
this.run,
|
|
1430
|
-
result.outcome,
|
|
1431
|
-
result.artifact ?? '',
|
|
1432
|
-
);
|
|
1433
|
-
return;
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
this.run = advanceRun(
|
|
1437
|
-
workflow,
|
|
1438
|
-
this.run,
|
|
1439
|
-
result.outcome,
|
|
1440
|
-
result.summary,
|
|
1441
|
-
Date.now(),
|
|
1442
|
-
);
|
|
1443
|
-
this.settleAfterTransition(workflow);
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
private handleDelegationUpdate(
|
|
161
|
+
) => Promise<void> = STEP_EXECUTION_ACTIONS.finishMainStep;
|
|
162
|
+
private readonly handleDelegationUpdate: (
|
|
1447
163
|
active: ActiveDelegation,
|
|
1448
164
|
update: SubagentDelegationUpdate,
|
|
1449
|
-
)
|
|
1450
|
-
|
|
1451
|
-
const progress = [
|
|
1452
|
-
update.currentTool ? `tool ${update.currentTool}` : undefined,
|
|
1453
|
-
update.toolCount !== undefined ? `${update.toolCount} calls` : undefined,
|
|
1454
|
-
update.tokens !== undefined ? `${update.tokens} tokens` : undefined,
|
|
1455
|
-
].filter((part): part is string => part !== undefined);
|
|
1456
|
-
active.progress = progress.join(', ') || 'running';
|
|
1457
|
-
this.updateStatus();
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
private queueDelegationResponse(
|
|
165
|
+
) => void = DELEGATION_RESPONSE_ACTIONS.handleDelegationUpdate;
|
|
166
|
+
private readonly queueDelegationResponse: (
|
|
1461
167
|
active: ActiveDelegation,
|
|
1462
168
|
response: SubagentDelegationResponse,
|
|
1463
|
-
)
|
|
1464
|
-
|
|
1465
|
-
.run(() => this.finishDelegation(active, response))
|
|
1466
|
-
.catch((error: unknown) => {
|
|
1467
|
-
this.pauseForDelegationFailure(
|
|
1468
|
-
error instanceof Error ? error.message : String(error),
|
|
1469
|
-
);
|
|
1470
|
-
});
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
private queueDelegationFailure(
|
|
169
|
+
) => void = DELEGATION_RESPONSE_ACTIONS.queueDelegationResponse;
|
|
170
|
+
private readonly queueDelegationFailure: (
|
|
1474
171
|
active: ActiveDelegation,
|
|
1475
172
|
reason: string,
|
|
1476
|
-
)
|
|
1477
|
-
|
|
1478
|
-
.run(async () => {
|
|
1479
|
-
if (this.activeDelegation !== active) {
|
|
1480
|
-
await this.cleanupDelegation(active);
|
|
1481
|
-
return;
|
|
1482
|
-
}
|
|
1483
|
-
if (this.subagents.activeRequestId === active.requestId) {
|
|
1484
|
-
this.retainUnconfirmedDelegation(active, reason);
|
|
1485
|
-
return;
|
|
1486
|
-
}
|
|
1487
|
-
this.activeDelegation = undefined;
|
|
1488
|
-
await this.cleanupDelegation(active);
|
|
1489
|
-
this.pauseForDelegationFailure(reason);
|
|
1490
|
-
})
|
|
1491
|
-
.catch((error: unknown) => {
|
|
1492
|
-
this.pauseForDelegationFailure(
|
|
1493
|
-
error instanceof Error ? error.message : String(error),
|
|
1494
|
-
);
|
|
1495
|
-
});
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
private async finishDelegation(
|
|
173
|
+
) => void = DELEGATION_RESPONSE_ACTIONS.queueDelegationFailure;
|
|
174
|
+
private readonly finishDelegation: (
|
|
1499
175
|
active: ActiveDelegation,
|
|
1500
176
|
response: SubagentDelegationResponse,
|
|
1501
|
-
)
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
try {
|
|
1510
|
-
if (
|
|
1511
|
-
!this.sessionActive ||
|
|
1512
|
-
this.sessionEpoch !== active.sessionEpoch ||
|
|
1513
|
-
!this.run ||
|
|
1514
|
-
this.run.status !== 'running' ||
|
|
1515
|
-
this.run.runId !== active.runId ||
|
|
1516
|
-
this.run.currentStepId !== active.stepId ||
|
|
1517
|
-
this.run.currentStepDigest !== active.stepDigest
|
|
1518
|
-
) {
|
|
1519
|
-
return;
|
|
1520
|
-
}
|
|
1521
|
-
const workflow = this.catalog.workflows.get(this.run.workflowId);
|
|
1522
|
-
const step = workflow?.definition.steps[this.run.currentStepId];
|
|
1523
|
-
if (!workflow || !step) {
|
|
1524
|
-
throw new Error('Active workflow configuration is unavailable');
|
|
1525
|
-
}
|
|
1526
|
-
let recoveredTerminalFailure: DelegationFailureDetails | undefined;
|
|
1527
|
-
if (
|
|
1528
|
-
response.status !== 'completed' ||
|
|
1529
|
-
hasContradictoryCompletion(response)
|
|
1530
|
-
) {
|
|
1531
|
-
const failure = await delegationFailureDetails(active, response);
|
|
1532
|
-
terminalFailure = failure;
|
|
1533
|
-
if (
|
|
1534
|
-
response.status !== 'failed' ||
|
|
1535
|
-
failure.diagnostic?.completionAfterFailure !== true
|
|
1536
|
-
) {
|
|
1537
|
-
throw new Error(failure.reason);
|
|
1538
|
-
}
|
|
1539
|
-
const projectionError = recoveredProjectionError(
|
|
1540
|
-
active,
|
|
1541
|
-
response,
|
|
1542
|
-
failure.diagnostic,
|
|
1543
|
-
);
|
|
1544
|
-
if (projectionError) {
|
|
1545
|
-
throw new Error(rejectedRecoveryReason(failure, projectionError));
|
|
1546
|
-
}
|
|
1547
|
-
recoveredTerminalFailure = failure;
|
|
1548
|
-
}
|
|
1549
|
-
const requiredSkillWarning =
|
|
1550
|
-
step.requires.skills.length > 0
|
|
1551
|
-
? response.warnings?.find((warning) => /skill/i.test(warning))
|
|
1552
|
-
: undefined;
|
|
1553
|
-
if (requiredSkillWarning) {
|
|
1554
|
-
throw new Error(
|
|
1555
|
-
`Subagent skill preflight failed: ${requiredSkillWarning}`,
|
|
1556
|
-
);
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
let serializedResult: string;
|
|
1560
|
-
try {
|
|
1561
|
-
serializedResult = await readStableDelegatedResult(active);
|
|
1562
|
-
} catch (error) {
|
|
1563
|
-
if (recoveredTerminalFailure) {
|
|
1564
|
-
throw new Error(
|
|
1565
|
-
rejectedRecoveryReason(recoveredTerminalFailure, error),
|
|
1566
|
-
{ cause: error },
|
|
1567
|
-
);
|
|
1568
|
-
}
|
|
1569
|
-
if (
|
|
1570
|
-
(error as { code?: unknown } | null | undefined)?.code === 'ENOENT'
|
|
1571
|
-
) {
|
|
1572
|
-
throw new Error(
|
|
1573
|
-
`Subagent "${active.agent}" completed without producing the required correlated structured_output result`,
|
|
1574
|
-
{ cause: error },
|
|
1575
|
-
);
|
|
1576
|
-
}
|
|
1577
|
-
throw error;
|
|
1578
|
-
}
|
|
1579
|
-
let result: WorkflowStepResult;
|
|
1580
|
-
try {
|
|
1581
|
-
const rawResult = JSON.parse(serializedResult) as unknown;
|
|
1582
|
-
result = parseDelegatedStepResult(rawResult, active.policy);
|
|
1583
|
-
} catch (error) {
|
|
1584
|
-
if (recoveredTerminalFailure) {
|
|
1585
|
-
throw new Error(
|
|
1586
|
-
rejectedRecoveryReason(recoveredTerminalFailure, error),
|
|
1587
|
-
{ cause: error },
|
|
1588
|
-
);
|
|
1589
|
-
}
|
|
1590
|
-
throw error;
|
|
1591
|
-
}
|
|
1592
|
-
if (
|
|
1593
|
-
recoveredTerminalFailure?.diagnostic &&
|
|
1594
|
-
!completionMatchesResult(
|
|
1595
|
-
recoveredTerminalFailure.diagnostic,
|
|
1596
|
-
result,
|
|
1597
|
-
active.policy,
|
|
1598
|
-
)
|
|
1599
|
-
) {
|
|
1600
|
-
throw new Error(
|
|
1601
|
-
rejectedRecoveryReason(
|
|
1602
|
-
recoveredTerminalFailure,
|
|
1603
|
-
'structured_output transcript value does not match the correlated result',
|
|
1604
|
-
),
|
|
1605
|
-
);
|
|
1606
|
-
}
|
|
1607
|
-
if (recoveredTerminalFailure) {
|
|
1608
|
-
const falsePositive =
|
|
1609
|
-
recoveredTerminalFailure.diagnostic?.correlation ===
|
|
1610
|
-
'successful-output-before-completion';
|
|
1611
|
-
this.latestContext?.ui.notify(
|
|
1612
|
-
falsePositive
|
|
1613
|
-
? `Accepted "${active.stepId}" because the trusted child transcript proved the terminal tool error was a false positive and produced a matching structured result`
|
|
1614
|
-
: `Accepted "${active.stepId}" because the child resolved an earlier tool failure and produced a valid structured result`,
|
|
1615
|
-
'warning',
|
|
1616
|
-
);
|
|
1617
|
-
}
|
|
1618
|
-
if (step.gate?.submitOutcome === result.outcome) {
|
|
1619
|
-
await this.submitGate(
|
|
1620
|
-
workflow,
|
|
1621
|
-
this.run,
|
|
1622
|
-
result.outcome,
|
|
1623
|
-
result.artifact ?? '',
|
|
1624
|
-
);
|
|
1625
|
-
return;
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
this.run = advanceRun(
|
|
1629
|
-
workflow,
|
|
1630
|
-
this.run,
|
|
1631
|
-
result.outcome,
|
|
1632
|
-
result.summary,
|
|
1633
|
-
Date.now(),
|
|
1634
|
-
);
|
|
1635
|
-
this.settleAfterTransition(workflow);
|
|
1636
|
-
} catch (error) {
|
|
1637
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
1638
|
-
if (!this.retryDelegationAfterFailure(active, terminalFailure, reason)) {
|
|
1639
|
-
this.pauseForDelegationFailure(reason);
|
|
1640
|
-
}
|
|
1641
|
-
} finally {
|
|
1642
|
-
await this.cleanupDelegation(active);
|
|
1643
|
-
if (active.cancelling) this.releaseMainAfterCancellation(active);
|
|
1644
|
-
}
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
private async cancelActiveDelegation(reason: string): Promise<boolean> {
|
|
1648
|
-
const active = this.activeDelegation;
|
|
1649
|
-
if (!active) return true;
|
|
1650
|
-
active.cancelling = true;
|
|
1651
|
-
active.progress = 'cancelling';
|
|
1652
|
-
this.updateStatus();
|
|
1653
|
-
if (this.subagents.activeRequestId !== active.requestId) {
|
|
1654
|
-
active.progress = 'cancellation unconfirmed';
|
|
1655
|
-
this.updateStatus();
|
|
1656
|
-
this.latestContext?.ui.notify(
|
|
1657
|
-
`${reason}; the delegation channel already closed without a terminal response`,
|
|
1658
|
-
'warning',
|
|
1659
|
-
);
|
|
1660
|
-
return false;
|
|
1661
|
-
}
|
|
1662
|
-
const confirmed = await this.subagents.cancelActiveAndWait();
|
|
1663
|
-
if (confirmed && this.activeDelegation === active) {
|
|
1664
|
-
this.activeDelegation = undefined;
|
|
1665
|
-
await this.cleanupDelegation(active);
|
|
1666
|
-
} else if (!confirmed) {
|
|
1667
|
-
this.latestContext?.ui.notify(
|
|
1668
|
-
`${reason}; waiting for subagent "${active.agent}" to confirm termination`,
|
|
1669
|
-
'warning',
|
|
1670
|
-
);
|
|
1671
|
-
}
|
|
1672
|
-
return confirmed;
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
private async cleanupDelegation(active: ActiveDelegation): Promise<void> {
|
|
1676
|
-
await rm(active.resultDirectory, { recursive: true, force: true });
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
private retryDelegationAfterFailure(
|
|
177
|
+
) => Promise<void> = DELEGATION_RESPONSE_ACTIONS.finishDelegation;
|
|
178
|
+
private readonly cancelActiveDelegation: (
|
|
179
|
+
reason: string,
|
|
180
|
+
) => Promise<boolean> = DELEGATION_CONTROL_ACTIONS.cancelActiveDelegation;
|
|
181
|
+
private readonly cleanupDelegation: (
|
|
182
|
+
active: ActiveDelegation,
|
|
183
|
+
) => Promise<void> = DELEGATION_CONTROL_ACTIONS.cleanupDelegation;
|
|
184
|
+
private readonly retryDelegationAfterFailure: (
|
|
1680
185
|
active: ActiveDelegation,
|
|
1681
186
|
failure: DelegationFailureDetails | undefined,
|
|
1682
187
|
reason: string,
|
|
1683
|
-
)
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
failure.replayAudit,
|
|
1692
|
-
) ||
|
|
1693
|
-
!this.sessionActive ||
|
|
1694
|
-
this.sessionEpoch !== active.sessionEpoch ||
|
|
1695
|
-
!this.run ||
|
|
1696
|
-
this.run.status !== 'running' ||
|
|
1697
|
-
this.run.runId !== active.runId ||
|
|
1698
|
-
this.run.currentStepId !== active.stepId ||
|
|
1699
|
-
this.run.currentStepDigest !== active.stepDigest ||
|
|
1700
|
-
this.activeDelegation
|
|
1701
|
-
) {
|
|
1702
|
-
return false;
|
|
1703
|
-
}
|
|
1704
|
-
const workflow = this.catalog.workflows.get(this.run.workflowId);
|
|
1705
|
-
if (!workflow) return false;
|
|
1706
|
-
|
|
1707
|
-
this.latestContext?.ui.notify(
|
|
1708
|
-
`Reinforcement retry for "${active.stepId}" after a subagent failure (${active.reinforcementRetryCount + 1}/${MAX_REINFORCEMENT_RETRIES})`,
|
|
1709
|
-
'warning',
|
|
1710
|
-
);
|
|
1711
|
-
this.launchCurrentStep(workflow, {
|
|
1712
|
-
count: active.reinforcementRetryCount + 1,
|
|
1713
|
-
reason,
|
|
1714
|
-
});
|
|
1715
|
-
return true;
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
private pauseForDelegationFailure(reason: string): void {
|
|
1719
|
-
this.pauseForExecutionFailure('Subagent step', reason);
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
private pauseForExecutionFailure(label: string, reason: string): void {
|
|
1723
|
-
if (!this.run || this.run.status !== 'running') return;
|
|
1724
|
-
this.mainSteps.deactivate();
|
|
1725
|
-
this.run = failRun(this.run, `${label} failed: ${reason}`, Date.now());
|
|
1726
|
-
this.persist();
|
|
1727
|
-
if (this.activeDelegation) {
|
|
1728
|
-
this.isolateMainSessionTools();
|
|
1729
|
-
} else {
|
|
1730
|
-
this.restoreBaselineTools();
|
|
1731
|
-
}
|
|
1732
|
-
this.updateStatus();
|
|
1733
|
-
this.latestContext?.ui.notify(
|
|
1734
|
-
`Workflow paused at "${this.run.currentStepId}": ${reason}`,
|
|
1735
|
-
'error',
|
|
1736
|
-
);
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
private retainUnconfirmedDelegation(
|
|
188
|
+
) => boolean = DELEGATION_CONTROL_ACTIONS.retryDelegationAfterFailure;
|
|
189
|
+
private readonly pauseForDelegationFailure: (reason: string) => void =
|
|
190
|
+
DELEGATION_CONTROL_ACTIONS.pauseForDelegationFailure;
|
|
191
|
+
private readonly pauseForExecutionFailure: (
|
|
192
|
+
label: string,
|
|
193
|
+
reason: string,
|
|
194
|
+
) => void = DELEGATION_CONTROL_ACTIONS.pauseForExecutionFailure;
|
|
195
|
+
private readonly retainUnconfirmedDelegation: (
|
|
1740
196
|
active: ActiveDelegation,
|
|
1741
197
|
reason: string,
|
|
1742
|
-
)
|
|
1743
|
-
|
|
1744
|
-
active
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
this.run,
|
|
1748
|
-
`Subagent step failed: ${reason}`,
|
|
1749
|
-
Date.now(),
|
|
1750
|
-
);
|
|
1751
|
-
this.persist();
|
|
1752
|
-
}
|
|
1753
|
-
this.isolateMainSessionTools();
|
|
1754
|
-
this.updateStatus();
|
|
1755
|
-
this.latestContext?.ui.notify(
|
|
1756
|
-
`Workflow paused, but subagent "${active.agent}" has not confirmed termination. Main tools and resume remain blocked; restart Pi if no terminal response arrives.`,
|
|
1757
|
-
'error',
|
|
1758
|
-
);
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
private releaseMainAfterCancellation(active: ActiveDelegation): void {
|
|
1762
|
-
if (this.activeDelegation === active) this.activeDelegation = undefined;
|
|
1763
|
-
if (
|
|
1764
|
-
!this.activeDelegation &&
|
|
1765
|
-
this.run &&
|
|
1766
|
-
this.run.status !== 'running' &&
|
|
1767
|
-
this.run.status !== 'awaiting-gate'
|
|
1768
|
-
) {
|
|
1769
|
-
this.restoreBaselineTools();
|
|
1770
|
-
this.updateStatus();
|
|
1771
|
-
this.latestContext?.ui.notify(
|
|
1772
|
-
`Subagent "${active.agent}" has terminated; main tools are restored`,
|
|
1773
|
-
'info',
|
|
1774
|
-
);
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
|
-
private async submitGate(
|
|
198
|
+
) => void = DELEGATION_CONTROL_ACTIONS.retainUnconfirmedDelegation;
|
|
199
|
+
private readonly releaseMainAfterCancellation: (
|
|
200
|
+
active: ActiveDelegation,
|
|
201
|
+
) => void = DELEGATION_CONTROL_ACTIONS.releaseMainAfterCancellation;
|
|
202
|
+
private readonly submitGate: (
|
|
1779
203
|
workflow: LoadedWorkflow,
|
|
1780
204
|
originalRun: WorkflowRun,
|
|
1781
205
|
outcome: string,
|
|
1782
206
|
artifact: string,
|
|
1783
|
-
)
|
|
1784
|
-
|
|
1785
|
-
const requestId = `${originalRun.runId}:${originalRun.currentStepId}:${randomUUID()}`;
|
|
1786
|
-
const step = workflow.definition.steps[originalRun.currentStepId];
|
|
1787
|
-
if (!step?.gate) throw new Error('Current step has no gate');
|
|
1788
|
-
|
|
1789
|
-
const commandShapeError = reviewedCommandShapeError(artifact);
|
|
1790
|
-
if (commandShapeError) {
|
|
1791
|
-
const awaitingReview = beginGate(
|
|
1792
|
-
workflow,
|
|
1793
|
-
originalRun,
|
|
1794
|
-
outcome,
|
|
1795
|
-
artifact,
|
|
1796
|
-
requestId,
|
|
1797
|
-
Date.now(),
|
|
1798
|
-
);
|
|
1799
|
-
this.run = resolveGate(
|
|
1800
|
-
workflow,
|
|
1801
|
-
awaitingReview,
|
|
1802
|
-
{
|
|
1803
|
-
approved: false,
|
|
1804
|
-
feedback: commandShapeError,
|
|
1805
|
-
resolvedAt: Date.now(),
|
|
1806
|
-
},
|
|
1807
|
-
Date.now(),
|
|
1808
|
-
);
|
|
1809
|
-
this.latestContext?.ui.notify(
|
|
1810
|
-
`Plan contract needs repair before review: ${commandShapeError}`,
|
|
1811
|
-
'warning',
|
|
1812
|
-
);
|
|
1813
|
-
this.settleAfterTransition(workflow);
|
|
1814
|
-
return;
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
this.run = beginGate(
|
|
1818
|
-
workflow,
|
|
1819
|
-
originalRun,
|
|
1820
|
-
outcome,
|
|
1821
|
-
artifact,
|
|
1822
|
-
requestId,
|
|
1823
|
-
Date.now(),
|
|
1824
|
-
);
|
|
1825
|
-
this.persist();
|
|
1826
|
-
this.restoreBaselineTools();
|
|
1827
|
-
this.updateStatus();
|
|
1828
|
-
|
|
1829
|
-
if (step.gate.provider === 'prompt') {
|
|
1830
|
-
this.launchPromptReview(workflow, this.run, this.latestContext);
|
|
1831
|
-
return;
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
const response = await requestPlannotatorReview(
|
|
1835
|
-
this.pi.events,
|
|
1836
|
-
requestId,
|
|
1837
|
-
artifact,
|
|
1838
|
-
`pi-workflows:${workflow.definition.id}:${originalRun.currentStepId}`,
|
|
1839
|
-
step.gate.timeoutMs,
|
|
1840
|
-
);
|
|
1841
|
-
if (
|
|
1842
|
-
!this.sessionActive ||
|
|
1843
|
-
this.sessionEpoch !== requestSessionEpoch ||
|
|
1844
|
-
!this.run ||
|
|
1845
|
-
this.run.runId !== originalRun.runId ||
|
|
1846
|
-
this.run.currentStepId !== originalRun.currentStepId ||
|
|
1847
|
-
this.run.pendingGate?.requestId !== requestId ||
|
|
1848
|
-
this.run.pendingGate.reviewId !== undefined
|
|
1849
|
-
) {
|
|
1850
|
-
throw new Error('Gate request was superseded by a workflow state change');
|
|
1851
|
-
}
|
|
1852
|
-
if (
|
|
1853
|
-
this.run.status !== 'awaiting-gate' &&
|
|
1854
|
-
!(this.run.status === 'paused' && this.run.pendingGate)
|
|
1855
|
-
) {
|
|
1856
|
-
throw new Error('Gate request was superseded by a workflow state change');
|
|
1857
|
-
}
|
|
1858
|
-
if (response.status !== 'handled') {
|
|
1859
|
-
const reason = response.error ?? 'Plannotator is unavailable';
|
|
1860
|
-
const gateFailed = failGate(this.run, reason, Date.now());
|
|
1861
|
-
this.run = failRun(gateFailed, reason, Date.now());
|
|
1862
|
-
this.persist();
|
|
1863
|
-
if (this.run.status === 'running') {
|
|
1864
|
-
this.isolateMainSessionTools();
|
|
1865
|
-
} else {
|
|
1866
|
-
this.restoreBaselineTools();
|
|
1867
|
-
}
|
|
1868
|
-
this.updateStatus();
|
|
1869
|
-
throw new Error(reason);
|
|
1870
|
-
}
|
|
1871
|
-
this.run = attachGateReviewId(
|
|
1872
|
-
this.run,
|
|
1873
|
-
response.result.reviewId,
|
|
1874
|
-
Date.now(),
|
|
1875
|
-
);
|
|
1876
|
-
this.persist();
|
|
1877
|
-
this.updateStatus();
|
|
1878
|
-
this.latestContext?.ui.notify(
|
|
1879
|
-
`Submitted "${originalRun.currentStepId}" for Plannotator review ${response.result.reviewId}`,
|
|
1880
|
-
'info',
|
|
1881
|
-
);
|
|
1882
|
-
}
|
|
1883
|
-
|
|
1884
|
-
private launchPromptReview(
|
|
207
|
+
) => Promise<void> = GATE_SUBMISSION_ACTION.submitGate;
|
|
208
|
+
private readonly launchPromptReview: (
|
|
1885
209
|
workflow: LoadedWorkflow,
|
|
1886
210
|
run: WorkflowRun,
|
|
1887
211
|
context: ExtensionContext | undefined,
|
|
1888
|
-
)
|
|
1889
|
-
|
|
1890
|
-
if (!pendingGate || pendingGate.provider !== 'prompt') return;
|
|
1891
|
-
if (!context?.hasUI) {
|
|
1892
|
-
this.pausePromptGate(
|
|
1893
|
-
pendingGate.requestId,
|
|
1894
|
-
'Built-in review requires Pi TUI or RPC mode; resume there to continue',
|
|
1895
|
-
false,
|
|
1896
|
-
);
|
|
1897
|
-
return;
|
|
1898
|
-
}
|
|
1899
|
-
if (this.activePromptReview?.requestId === pendingGate.requestId) return;
|
|
1900
|
-
this.cancelPromptReview();
|
|
1901
|
-
|
|
1902
|
-
const active: ActivePromptReview = {
|
|
1903
|
-
requestId: pendingGate.requestId,
|
|
1904
|
-
runId: run.runId,
|
|
1905
|
-
stepId: pendingGate.stepId,
|
|
1906
|
-
sessionEpoch: this.sessionEpoch,
|
|
1907
|
-
abortController: new AbortController(),
|
|
1908
|
-
};
|
|
1909
|
-
this.activePromptReview = active;
|
|
1910
|
-
void requestPromptGateReview(
|
|
1911
|
-
context.ui,
|
|
1912
|
-
`Review ${workflow.definition.id}:${pendingGate.stepId}`,
|
|
1913
|
-
pendingGate.artifact,
|
|
1914
|
-
active.abortController.signal,
|
|
1915
|
-
).then(
|
|
1916
|
-
(result) => this.queuePromptReviewResult(active, result),
|
|
1917
|
-
(error: unknown) =>
|
|
1918
|
-
this.queuePromptReviewFailure(
|
|
1919
|
-
active,
|
|
1920
|
-
error instanceof Error ? error.message : String(error),
|
|
1921
|
-
),
|
|
1922
|
-
);
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
private queuePromptReviewResult(
|
|
212
|
+
) => void = PROMPT_GATE_ACTIONS.launchPromptReview;
|
|
213
|
+
private readonly queuePromptReviewResult: (
|
|
1926
214
|
active: ActivePromptReview,
|
|
1927
215
|
result: PromptGateReviewResult,
|
|
1928
|
-
)
|
|
1929
|
-
|
|
1930
|
-
.run(() => this.finishPromptReview(active, result))
|
|
1931
|
-
.catch((error: unknown) => {
|
|
1932
|
-
this.latestContext?.ui.notify(
|
|
1933
|
-
`Cannot apply built-in review: ${error instanceof Error ? error.message : String(error)}`,
|
|
1934
|
-
'error',
|
|
1935
|
-
);
|
|
1936
|
-
});
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
|
-
private queuePromptReviewFailure(
|
|
216
|
+
) => void = PROMPT_GATE_ACTIONS.queuePromptReviewResult;
|
|
217
|
+
private readonly queuePromptReviewFailure: (
|
|
1940
218
|
active: ActivePromptReview,
|
|
1941
219
|
reason: string,
|
|
1942
|
-
)
|
|
1943
|
-
|
|
1944
|
-
.run(async () => {
|
|
1945
|
-
if (this.activePromptReview !== active) return;
|
|
1946
|
-
this.activePromptReview = undefined;
|
|
1947
|
-
this.pausePromptGate(
|
|
1948
|
-
active.requestId,
|
|
1949
|
-
`Built-in review failed: ${reason}`,
|
|
1950
|
-
true,
|
|
1951
|
-
);
|
|
1952
|
-
})
|
|
1953
|
-
.catch((error: unknown) => {
|
|
1954
|
-
this.latestContext?.ui.notify(
|
|
1955
|
-
`Cannot pause failed built-in review: ${error instanceof Error ? error.message : String(error)}`,
|
|
1956
|
-
'error',
|
|
1957
|
-
);
|
|
1958
|
-
});
|
|
1959
|
-
}
|
|
1960
|
-
|
|
1961
|
-
private async finishPromptReview(
|
|
220
|
+
) => void = PROMPT_GATE_ACTIONS.queuePromptReviewFailure;
|
|
221
|
+
private readonly finishPromptReview: (
|
|
1962
222
|
active: ActivePromptReview,
|
|
1963
223
|
result: PromptGateReviewResult,
|
|
1964
|
-
)
|
|
1965
|
-
|
|
1966
|
-
this.activePromptReview = undefined;
|
|
1967
|
-
if (
|
|
1968
|
-
!this.sessionActive ||
|
|
1969
|
-
this.sessionEpoch !== active.sessionEpoch ||
|
|
1970
|
-
!this.run ||
|
|
1971
|
-
this.run.runId !== active.runId ||
|
|
1972
|
-
this.run.currentStepId !== active.stepId ||
|
|
1973
|
-
this.run.pendingGate?.provider !== 'prompt' ||
|
|
1974
|
-
this.run.pendingGate.requestId !== active.requestId
|
|
1975
|
-
) {
|
|
1976
|
-
return;
|
|
1977
|
-
}
|
|
1978
|
-
if (result.status === 'dismissed') {
|
|
1979
|
-
this.pausePromptGate(
|
|
1980
|
-
active.requestId,
|
|
1981
|
-
'Built-in review was dismissed; resume to reopen it',
|
|
1982
|
-
false,
|
|
1983
|
-
);
|
|
1984
|
-
return;
|
|
1985
|
-
}
|
|
1986
|
-
|
|
1987
|
-
const resolution: GateResolution = {
|
|
1988
|
-
approved: result.approved,
|
|
1989
|
-
feedback: result.feedback,
|
|
1990
|
-
resolvedAt: Date.now(),
|
|
1991
|
-
};
|
|
1992
|
-
if (this.run.status === 'paused') {
|
|
1993
|
-
this.run = storeGateResolution(this.run, resolution, Date.now());
|
|
1994
|
-
this.persist();
|
|
1995
|
-
this.latestContext?.ui.notify(
|
|
1996
|
-
'Built-in review finished while paused. Run /workflow-resume to apply it.',
|
|
1997
|
-
'info',
|
|
1998
|
-
);
|
|
1999
|
-
return;
|
|
2000
|
-
}
|
|
2001
|
-
if (this.run.status !== 'awaiting-gate') return;
|
|
2002
|
-
|
|
2003
|
-
const workflow = this.catalog.workflows.get(this.run.workflowId);
|
|
2004
|
-
if (!workflow) {
|
|
2005
|
-
this.pausePromptGate(
|
|
2006
|
-
active.requestId,
|
|
2007
|
-
'Built-in review finished, but workflow configuration is unavailable',
|
|
2008
|
-
true,
|
|
2009
|
-
);
|
|
2010
|
-
return;
|
|
2011
|
-
}
|
|
2012
|
-
try {
|
|
2013
|
-
this.run = resolveGate(workflow, this.run, resolution, Date.now());
|
|
2014
|
-
this.settleAfterTransition(workflow);
|
|
2015
|
-
} catch (error) {
|
|
2016
|
-
this.pausePromptGate(
|
|
2017
|
-
active.requestId,
|
|
2018
|
-
`Cannot apply built-in review: ${error instanceof Error ? error.message : String(error)}`,
|
|
2019
|
-
true,
|
|
2020
|
-
);
|
|
2021
|
-
}
|
|
2022
|
-
}
|
|
2023
|
-
|
|
2024
|
-
private pausePromptGate(
|
|
224
|
+
) => Promise<void> = PROMPT_GATE_ACTIONS.finishPromptReview;
|
|
225
|
+
private readonly pausePromptGate: (
|
|
2025
226
|
requestId: string,
|
|
2026
227
|
reason: string,
|
|
2027
|
-
|
|
2028
|
-
)
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
private cancelPromptReview(): void {
|
|
2051
|
-
const active = this.activePromptReview;
|
|
2052
|
-
if (!active) return;
|
|
2053
|
-
this.activePromptReview = undefined;
|
|
2054
|
-
active.abortController.abort();
|
|
2055
|
-
}
|
|
2056
|
-
|
|
2057
|
-
private registerPlannotatorResults(): void {
|
|
2058
|
-
this.pi.events.on(PLANNOTATOR_RESULT_CHANNEL, (data) => {
|
|
2059
|
-
void this.mutationQueue
|
|
2060
|
-
.run(() => this.handlePlannotatorResult(data))
|
|
2061
|
-
.catch((error: unknown) => {
|
|
2062
|
-
this.latestContext?.ui.notify(
|
|
2063
|
-
`Cannot apply Plannotator result: ${
|
|
2064
|
-
error instanceof Error ? error.message : String(error)
|
|
2065
|
-
}`,
|
|
2066
|
-
'error',
|
|
2067
|
-
);
|
|
2068
|
-
});
|
|
2069
|
-
});
|
|
2070
|
-
}
|
|
2071
|
-
|
|
2072
|
-
private async handlePlannotatorResult(data: unknown): Promise<void> {
|
|
2073
|
-
if (
|
|
2074
|
-
!this.sessionActive ||
|
|
2075
|
-
this.run?.pendingGate?.provider !== 'plannotator' ||
|
|
2076
|
-
!this.run.pendingGate.reviewId
|
|
2077
|
-
) {
|
|
2078
|
-
return;
|
|
2079
|
-
}
|
|
2080
|
-
const result = parsePlannotatorResult(data);
|
|
2081
|
-
if (!result || result.reviewId !== this.run.pendingGate.reviewId) return;
|
|
2082
|
-
|
|
2083
|
-
const resolution: GateResolution = {
|
|
2084
|
-
approved: result.approved,
|
|
2085
|
-
feedback: result.feedback,
|
|
2086
|
-
resolvedAt: Date.now(),
|
|
2087
|
-
};
|
|
2088
|
-
if (this.run.status === 'paused') {
|
|
2089
|
-
this.run = storeGateResolution(this.run, resolution, Date.now());
|
|
2090
|
-
this.persist();
|
|
2091
|
-
this.latestContext?.ui.notify(
|
|
2092
|
-
`Review ${result.reviewId} finished while paused. Run /workflow-resume to apply it.`,
|
|
2093
|
-
'info',
|
|
2094
|
-
);
|
|
2095
|
-
return;
|
|
2096
|
-
}
|
|
2097
|
-
if (this.run.status !== 'awaiting-gate') return;
|
|
2098
|
-
|
|
2099
|
-
const workflow = this.catalog.workflows.get(this.run.workflowId);
|
|
2100
|
-
if (!workflow) {
|
|
2101
|
-
this.run = failRun(
|
|
2102
|
-
this.run,
|
|
2103
|
-
'Gate result arrived, but workflow configuration is unavailable',
|
|
2104
|
-
Date.now(),
|
|
2105
|
-
);
|
|
2106
|
-
this.persist();
|
|
2107
|
-
this.restoreBaselineTools();
|
|
2108
|
-
this.updateStatus();
|
|
2109
|
-
return;
|
|
2110
|
-
}
|
|
2111
|
-
try {
|
|
2112
|
-
this.run = resolveGate(workflow, this.run, resolution, Date.now());
|
|
2113
|
-
this.settleAfterTransition(workflow);
|
|
2114
|
-
} catch (error) {
|
|
2115
|
-
this.run = failRun(
|
|
2116
|
-
this.run,
|
|
2117
|
-
`Cannot apply gate result: ${error instanceof Error ? error.message : String(error)}`,
|
|
2118
|
-
Date.now(),
|
|
2119
|
-
);
|
|
2120
|
-
this.persist();
|
|
2121
|
-
this.restoreBaselineTools();
|
|
2122
|
-
this.updateStatus();
|
|
2123
|
-
}
|
|
2124
|
-
}
|
|
2125
|
-
|
|
2126
|
-
private settleAfterTransition(workflow: LoadedWorkflow): void {
|
|
2127
|
-
if (!this.run) return;
|
|
2128
|
-
if (this.run.status === 'running') {
|
|
2129
|
-
const preflightErrors = this.preflight(workflow, this.run.currentStepId);
|
|
2130
|
-
if (preflightErrors.length > 0) {
|
|
2131
|
-
this.run = failRun(
|
|
2132
|
-
this.run,
|
|
2133
|
-
`Step preflight failed: ${preflightErrors.join('; ')}`,
|
|
2134
|
-
Date.now(),
|
|
2135
|
-
);
|
|
2136
|
-
}
|
|
2137
|
-
}
|
|
2138
|
-
|
|
2139
|
-
this.persist();
|
|
2140
|
-
if (this.run.status !== 'running') {
|
|
2141
|
-
this.restoreBaselineTools();
|
|
2142
|
-
this.updateStatus();
|
|
2143
|
-
if (this.run.status === 'completed') {
|
|
2144
|
-
this.latestContext?.ui.notify(
|
|
2145
|
-
`Workflow "${this.run.workflowId}" completed`,
|
|
2146
|
-
'info',
|
|
2147
|
-
);
|
|
2148
|
-
} else if (this.run.status === 'paused') {
|
|
2149
|
-
this.latestContext?.ui.notify(
|
|
2150
|
-
`Workflow paused: ${this.run.pauseReason ?? 'manual action required'}`,
|
|
2151
|
-
'warning',
|
|
2152
|
-
);
|
|
2153
|
-
}
|
|
2154
|
-
return;
|
|
2155
|
-
}
|
|
2156
|
-
|
|
2157
|
-
this.isolateMainSessionTools();
|
|
2158
|
-
this.updateStatus();
|
|
2159
|
-
this.launchCurrentStep(workflow);
|
|
2160
|
-
}
|
|
2161
|
-
|
|
2162
|
-
private preflight(workflow: LoadedWorkflow, stepId: string): string[] {
|
|
2163
|
-
const step = workflow.definition.steps[stepId];
|
|
2164
|
-
if (!step) return [`step "${stepId}" does not exist`];
|
|
2165
|
-
return preflightStep(step, {
|
|
2166
|
-
tools: this.pi.getAllTools(),
|
|
2167
|
-
commands: this.pi.getCommands(),
|
|
2168
|
-
skills: this.availableSkills,
|
|
2169
|
-
});
|
|
2170
|
-
}
|
|
2171
|
-
|
|
2172
|
-
private isolateMainSessionTools(): void {
|
|
2173
|
-
this.pi.setActiveTools([]);
|
|
2174
|
-
}
|
|
2175
|
-
|
|
2176
|
-
private restoreBaselineTools(): void {
|
|
2177
|
-
this.mainSteps.release();
|
|
2178
|
-
if (this.run) {
|
|
2179
|
-
this.pi.setActiveTools(this.run.baselineTools);
|
|
2180
|
-
return;
|
|
2181
|
-
}
|
|
2182
|
-
this.pi.setActiveTools(this.pi.getActiveTools());
|
|
2183
|
-
}
|
|
2184
|
-
|
|
2185
|
-
private captureSkills(skills: readonly { name: string }[] | undefined): void {
|
|
2186
|
-
if (!skills) return;
|
|
2187
|
-
this.availableSkills = new Set(skills.map((skill) => skill.name));
|
|
2188
|
-
}
|
|
2189
|
-
|
|
2190
|
-
private enqueueMutation(
|
|
2191
|
-
ctx: ExtensionContext,
|
|
228
|
+
isFailed: boolean,
|
|
229
|
+
) => void = PROMPT_GATE_ACTIONS.pausePromptGate;
|
|
230
|
+
private readonly cancelPromptReview: () => void =
|
|
231
|
+
PROMPT_GATE_ACTIONS.cancelPromptReview;
|
|
232
|
+
private readonly registerPlannotatorResults: () => void =
|
|
233
|
+
PLANNOTATOR_RESULT_ACTIONS.registerPlannotatorResults;
|
|
234
|
+
private readonly handlePlannotatorResult: (data: unknown) => Promise<void> =
|
|
235
|
+
PLANNOTATOR_RESULT_ACTIONS.handlePlannotatorResult;
|
|
236
|
+
private readonly settleAfterTransition: (workflow: LoadedWorkflow) => void =
|
|
237
|
+
CORE_ACTIONS.settleAfterTransition;
|
|
238
|
+
private readonly preflight: (
|
|
239
|
+
workflow: LoadedWorkflow,
|
|
240
|
+
stepId: string,
|
|
241
|
+
) => Array<string> = CORE_ACTIONS.preflight;
|
|
242
|
+
private readonly isolateMainSessionTools: () => void =
|
|
243
|
+
CORE_ACTIONS.isolateMainSessionTools;
|
|
244
|
+
private readonly restoreBaselineTools: () => void =
|
|
245
|
+
CORE_ACTIONS.restoreBaselineTools;
|
|
246
|
+
private readonly captureSkills: (
|
|
247
|
+
skills: ReadonlyArray<{ name: string }> | undefined,
|
|
248
|
+
) => void = CORE_ACTIONS.captureSkills;
|
|
249
|
+
private readonly enqueueMutation: (
|
|
250
|
+
context: ExtensionContext,
|
|
2192
251
|
operation: (sessionEpoch: number) => Promise<void>,
|
|
2193
|
-
)
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
252
|
+
) => Promise<void> = CORE_ACTIONS.enqueueMutation;
|
|
253
|
+
private readonly persist: () => void = CORE_ACTIONS.persist;
|
|
254
|
+
private readonly restoreFromSession: (context: ExtensionContext) => void =
|
|
255
|
+
CORE_ACTIONS.restoreFromSession;
|
|
256
|
+
private readonly reloadCatalog: (
|
|
257
|
+
context: ExtensionContext,
|
|
258
|
+
shouldAnnounce: boolean,
|
|
259
|
+
) => Promise<boolean> = CORE_ACTIONS.reloadCatalog;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Creates a workflow harness and registers its Pi integration surface.
|
|
263
|
+
*
|
|
264
|
+
* @param pi - Pi extension API used by the runtime adapters.
|
|
265
|
+
* @param statusShortcut - Shortcut used to open workflow status.
|
|
266
|
+
* @param dependencyOverrides - Optional effect implementations for tests or
|
|
267
|
+
* alternate runtimes.
|
|
268
|
+
*/
|
|
269
|
+
constructor(
|
|
270
|
+
pi: ExtensionAPI,
|
|
271
|
+
statusShortcut: KeyId = DEFAULT_STATUS_SHORTCUT,
|
|
272
|
+
dependencyOverrides: Partial<WorkflowHarnessDependencies> = {},
|
|
273
|
+
) {
|
|
274
|
+
this.pi = pi;
|
|
275
|
+
this.dependencies = createWorkflowHarnessDependencies(dependencyOverrides);
|
|
276
|
+
this.delegationFailures = createDelegationFailureActions(this.dependencies);
|
|
277
|
+
this.statusShortcut = statusShortcut;
|
|
278
|
+
this.statusShortcutLabel = formatShortcutLabel(statusShortcut);
|
|
279
|
+
this.subagents = this.dependencies.createSubagentClient(pi);
|
|
280
|
+
this.mainSteps = this.dependencies.createMainStepRuntime(pi);
|
|
281
|
+
this.mutationQueue = this.dependencies.createMutationQueue();
|
|
282
|
+
registerHarnessCommands(pi, this);
|
|
283
|
+
this.registerWorkflowStatusShortcut();
|
|
284
|
+
this.registerMultilineCommandInput();
|
|
285
|
+
this.registerLifecycle();
|
|
286
|
+
this.registerPolicy();
|
|
287
|
+
this.registerPlannotatorResults();
|
|
2215
288
|
}
|
|
2216
289
|
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
const entries = ctx.sessionManager.getBranch();
|
|
2221
|
-
const checkpoint = readLatestCheckpoint(entries, STATE_ENTRY_TYPE);
|
|
2222
|
-
this.run = checkpoint.status === 'valid' ? checkpoint.run : undefined;
|
|
2223
|
-
if (checkpoint.status === 'invalid') {
|
|
2224
|
-
ctx.ui.notify(
|
|
2225
|
-
'The newest workflow checkpoint is invalid or from an unsupported version; recovery stopped',
|
|
2226
|
-
'error',
|
|
2227
|
-
);
|
|
2228
|
-
}
|
|
2229
|
-
if (
|
|
2230
|
-
this.run &&
|
|
2231
|
-
(this.run.status === 'running' || this.run.status === 'awaiting-gate')
|
|
2232
|
-
) {
|
|
2233
|
-
this.run = pauseRun(
|
|
2234
|
-
this.run,
|
|
2235
|
-
'Session was restored; inspect the checkpoint before resuming',
|
|
2236
|
-
Date.now(),
|
|
2237
|
-
);
|
|
2238
|
-
this.persist();
|
|
2239
|
-
}
|
|
2240
|
-
if (!this.run && previousBaseline) {
|
|
2241
|
-
this.pi.setActiveTools(previousBaseline);
|
|
2242
|
-
} else {
|
|
2243
|
-
this.restoreBaselineTools();
|
|
2244
|
-
}
|
|
2245
|
-
if (this.activeDelegation) this.isolateMainSessionTools();
|
|
2246
|
-
this.updateStatus();
|
|
290
|
+
/** Returns the loaded workflow identifiers in stable display order. */
|
|
291
|
+
workflowIds(): Array<string> {
|
|
292
|
+
return [...this.catalog.workflows.keys()].sort();
|
|
2247
293
|
}
|
|
2248
294
|
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
): Promise<boolean> {
|
|
2253
|
-
const loadSequence = ++this.catalogLoadSequence;
|
|
2254
|
-
const sessionEpoch = this.sessionEpoch;
|
|
2255
|
-
const catalog = await loadCatalog({
|
|
2256
|
-
cwd: ctx.cwd,
|
|
2257
|
-
projectTrusted: ctx.isProjectTrusted(),
|
|
2258
|
-
});
|
|
2259
|
-
if (
|
|
2260
|
-
loadSequence !== this.catalogLoadSequence ||
|
|
2261
|
-
sessionEpoch !== this.sessionEpoch
|
|
2262
|
-
) {
|
|
2263
|
-
return false;
|
|
2264
|
-
}
|
|
2265
|
-
this.latestContext = ctx;
|
|
2266
|
-
if (catalog.settings.statusShortcut !== this.statusShortcut) {
|
|
2267
|
-
catalog.diagnostics.push({
|
|
2268
|
-
level: 'warning',
|
|
2269
|
-
path: join(catalog.userDirectory, 'settings.yaml'),
|
|
2270
|
-
message:
|
|
2271
|
-
`settings.statusShortcut is "${catalog.settings.statusShortcut}", ` +
|
|
2272
|
-
`but the active shortcut is "${this.statusShortcut}"; run Pi /reload to apply shortcut changes`,
|
|
2273
|
-
});
|
|
2274
|
-
}
|
|
2275
|
-
const availableCommands = this.pi.getCommands();
|
|
2276
|
-
for (const [workflowId, workflow] of catalog.workflows) {
|
|
2277
|
-
const command = workflow.definition.command;
|
|
2278
|
-
if (
|
|
2279
|
-
hasRuntimeCommandConflict(
|
|
2280
|
-
command,
|
|
2281
|
-
availableCommands,
|
|
2282
|
-
this.registeredWorkflowCommands,
|
|
2283
|
-
)
|
|
2284
|
-
) {
|
|
2285
|
-
catalog.workflows.delete(workflowId);
|
|
2286
|
-
catalog.diagnostics.push({
|
|
2287
|
-
level: 'error',
|
|
2288
|
-
path: workflow.sourcePath,
|
|
2289
|
-
message: `command "/${command}" conflicts with another loaded Pi resource`,
|
|
2290
|
-
});
|
|
2291
|
-
}
|
|
2292
|
-
}
|
|
2293
|
-
this.catalog = catalog;
|
|
2294
|
-
for (const workflow of catalog.workflows.values()) {
|
|
2295
|
-
this.pi.registerCommand(workflow.definition.command, {
|
|
2296
|
-
description: workflow.definition.description,
|
|
2297
|
-
handler: async (args, commandContext) =>
|
|
2298
|
-
this.start(workflow.definition.id, args, commandContext),
|
|
2299
|
-
});
|
|
2300
|
-
this.registeredWorkflowCommands.add(workflow.definition.command);
|
|
2301
|
-
}
|
|
2302
|
-
|
|
2303
|
-
if (announce) {
|
|
2304
|
-
const diagnosticText = formatDiagnostics(this.catalog);
|
|
2305
|
-
ctx.ui.notify(
|
|
2306
|
-
diagnosticText
|
|
2307
|
-
? `Loaded ${this.catalog.workflows.size} workflow(s)\n${diagnosticText}`
|
|
2308
|
-
: `Loaded ${this.catalog.workflows.size} workflow(s)`,
|
|
2309
|
-
diagnosticText ? 'warning' : 'info',
|
|
2310
|
-
);
|
|
2311
|
-
} else if (
|
|
2312
|
-
this.catalog.diagnostics.some((item) => item.level === 'error')
|
|
2313
|
-
) {
|
|
2314
|
-
ctx.ui.notify(
|
|
2315
|
-
`Workflow configuration errors:\n${formatDiagnostics(this.catalog)}`,
|
|
2316
|
-
'warning',
|
|
2317
|
-
);
|
|
2318
|
-
}
|
|
2319
|
-
return true;
|
|
295
|
+
/** Displays the currently loaded workflows. */
|
|
296
|
+
async list(context: ExtensionCommandContext): Promise<void> {
|
|
297
|
+
await this.listWorkflows(context);
|
|
2320
298
|
}
|
|
2321
299
|
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
STATUS_KEY,
|
|
2340
|
-
`${workflowStatusIcon(this.run, snapshot?.now)} ${this.run.workflowId}: working · ${this.statusShortcutLabel}`,
|
|
300
|
+
/** Starts a loaded workflow with the supplied user input. */
|
|
301
|
+
start(
|
|
302
|
+
workflowId: string,
|
|
303
|
+
input: string,
|
|
304
|
+
context: ExtensionCommandContext,
|
|
305
|
+
): Promise<void> {
|
|
306
|
+
return this.enqueueMutation(context, (sessionEpoch) =>
|
|
307
|
+
this.startNow(
|
|
308
|
+
workflowId,
|
|
309
|
+
input,
|
|
310
|
+
{
|
|
311
|
+
context,
|
|
312
|
+
skills: () => context.getSystemPromptOptions().skills,
|
|
313
|
+
waitForIdle: () => context.waitForIdle(),
|
|
314
|
+
},
|
|
315
|
+
sessionEpoch,
|
|
316
|
+
),
|
|
2341
317
|
);
|
|
2342
318
|
}
|
|
2343
319
|
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
this.statusRefreshTimer = setInterval(
|
|
2348
|
-
() => this.updateStatus(),
|
|
2349
|
-
STATUS_REFRESH_INTERVAL_MS,
|
|
2350
|
-
);
|
|
2351
|
-
this.statusRefreshTimer.unref?.();
|
|
2352
|
-
return;
|
|
2353
|
-
}
|
|
2354
|
-
this.stopStatusRefresh();
|
|
2355
|
-
}
|
|
2356
|
-
|
|
2357
|
-
private stopStatusRefresh(): void {
|
|
2358
|
-
if (this.statusRefreshTimer) clearInterval(this.statusRefreshTimer);
|
|
2359
|
-
this.statusRefreshTimer = undefined;
|
|
320
|
+
/** Pauses the active workflow while retaining its checkpoint. */
|
|
321
|
+
pause(reason: string, context: ExtensionCommandContext): Promise<void> {
|
|
322
|
+
return this.enqueueMutation(context, () => this.pauseNow(reason, context));
|
|
2360
323
|
}
|
|
2361
324
|
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
handler: async (ctx) => {
|
|
2366
|
-
this.latestContext = ctx;
|
|
2367
|
-
if (this.statusOverlayOpen) return;
|
|
2368
|
-
if (!this.run) {
|
|
2369
|
-
ctx.ui.notify('No workflow checkpoint in this session', 'info');
|
|
2370
|
-
return;
|
|
2371
|
-
}
|
|
2372
|
-
await this.showWorkflowStatus(ctx);
|
|
2373
|
-
},
|
|
2374
|
-
});
|
|
325
|
+
/** Resumes the active paused workflow after reconciling configuration. */
|
|
326
|
+
resume(context: ExtensionCommandContext): Promise<void> {
|
|
327
|
+
return this.enqueueMutation(context, () => this.resumeNow(context));
|
|
2375
328
|
}
|
|
2376
329
|
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
`Cannot open workflow status: ${error instanceof Error ? error.message : String(error)}`,
|
|
2381
|
-
'error',
|
|
2382
|
-
);
|
|
2383
|
-
});
|
|
330
|
+
/** Aborts the active workflow and cancels any delegated execution. */
|
|
331
|
+
abort(reason: string, context: ExtensionCommandContext): Promise<void> {
|
|
332
|
+
return this.enqueueMutation(context, () => this.abortNow(reason, context));
|
|
2384
333
|
}
|
|
2385
334
|
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
!this.run ||
|
|
2390
|
-
!ctx.hasUI ||
|
|
2391
|
-
ctx.mode !== 'tui'
|
|
2392
|
-
) {
|
|
2393
|
-
return;
|
|
2394
|
-
}
|
|
2395
|
-
this.statusOverlayOpen = true;
|
|
2396
|
-
try {
|
|
2397
|
-
await showWorkflowStatusOverlay(
|
|
2398
|
-
ctx,
|
|
2399
|
-
() => this.workflowStatusSnapshot(),
|
|
2400
|
-
this.statusShortcut,
|
|
2401
|
-
);
|
|
2402
|
-
} finally {
|
|
2403
|
-
this.statusOverlayOpen = false;
|
|
2404
|
-
}
|
|
335
|
+
/** Reloads workflow configuration while no workflow is executing. */
|
|
336
|
+
reload(context: ExtensionCommandContext): Promise<void> {
|
|
337
|
+
return this.enqueueMutation(context, () => this.reloadNow(context));
|
|
2405
338
|
}
|
|
2406
339
|
}
|