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