@wichayutdew/pi-workflows 0.3.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -39
- package/dist/index.js +5754 -4794
- package/package.json +1 -1
- package/src/command-names.ts +7 -1
- package/src/commands.ts +126 -52
- package/src/config/catalog.ts +162 -0
- package/src/config/ceiling.ts +116 -116
- package/src/config/command-conflicts.ts +4 -4
- package/src/config/diagnostics.ts +28 -0
- package/src/config/load-settings.ts +37 -0
- package/src/config/load-types.ts +53 -0
- package/src/config/load-workflows.ts +135 -0
- package/src/config/load.ts +43 -311
- package/src/config/types.ts +137 -135
- package/src/config/validate.ts +12 -1261
- package/src/config/validation/permissions.ts +291 -0
- package/src/config/validation/prompt.ts +19 -0
- package/src/config/validation/settings.ts +122 -0
- package/src/config/validation/shared.ts +129 -0
- package/src/config/validation/shortcut.ts +118 -0
- package/src/config/validation/step.ts +228 -0
- package/src/config/validation/subagent.ts +288 -0
- package/src/config/validation/workflow.ts +156 -0
- package/src/config/yaml.ts +29 -0
- package/src/digest.ts +38 -6
- package/src/engine/checkpoint.ts +13 -9
- package/src/engine/create-run.ts +42 -0
- package/src/engine/gate-transitions.ts +184 -0
- package/src/engine/reconciliation-history.ts +88 -0
- package/src/engine/resume.ts +19 -7
- package/src/engine/run-advance.ts +122 -0
- package/src/engine/run-lifecycle.ts +124 -0
- package/src/engine/run-reconciliation.ts +116 -0
- package/src/engine/run-validation.ts +128 -0
- package/src/engine/state-types.ts +64 -0
- package/src/engine/state.ts +10 -193
- package/src/engine/transition-helpers.ts +28 -0
- package/src/engine/transition-types.ts +8 -0
- package/src/engine/transitions.ts +17 -471
- package/src/harness/action-context.ts +181 -0
- package/src/harness/catalog.ts +45 -0
- package/src/harness/context-idle.ts +21 -0
- package/src/harness/core-actions.ts +299 -0
- package/src/harness/delegation-control-actions.ts +250 -0
- package/src/harness/delegation-failure.ts +215 -0
- package/src/harness/delegation-plan.ts +219 -0
- package/src/harness/delegation-recovery-validation.ts +159 -0
- package/src/harness/delegation-response-actions.ts +295 -0
- package/src/harness/delegation-retry-policy.ts +118 -0
- package/src/harness/dependencies.ts +165 -0
- package/src/harness/gate-submission-action.ts +154 -0
- package/src/harness/lifecycle-actions.ts +154 -0
- package/src/harness/pause-actions.ts +120 -0
- package/src/harness/plannotator-result-actions.ts +123 -0
- package/src/harness/prompt-gate-actions.ts +270 -0
- package/src/harness/resume-action.ts +261 -0
- package/src/harness/start-actions.ts +183 -0
- package/src/harness/status-actions.ts +191 -0
- package/src/harness/step-execution-actions.ts +283 -0
- package/src/harness/types.ts +76 -0
- package/src/harness.ts +236 -2303
- package/src/index.ts +73 -11
- package/src/integrations/plannotator-requests.ts +127 -0
- package/src/integrations/plannotator-responses.ts +141 -0
- package/src/integrations/plannotator-types.ts +44 -0
- package/src/integrations/plannotator.ts +16 -235
- package/src/integrations/prompt-gate.ts +17 -10
- package/src/integrations/subagents/child-policy-envelope.ts +119 -0
- package/src/integrations/subagents/child-policy-paths.ts +66 -0
- package/src/integrations/subagents/child-policy-sections.ts +174 -0
- package/src/integrations/subagents/child-policy-types.ts +32 -0
- package/src/integrations/subagents/child-policy-validation.ts +163 -0
- package/src/integrations/subagents/child-runtime-completion.ts +56 -0
- package/src/integrations/subagents/child-runtime-dependencies.ts +47 -0
- package/src/integrations/subagents/child-runtime-files.ts +156 -0
- package/src/integrations/subagents/child-runtime-policy.ts +83 -0
- package/src/integrations/subagents/child-runtime-types.ts +30 -0
- package/src/integrations/subagents/child-runtime.ts +140 -301
- package/src/integrations/subagents/client-delegation.ts +181 -0
- package/src/integrations/subagents/client-messages.ts +66 -0
- package/src/integrations/subagents/client-types.ts +36 -0
- package/src/integrations/subagents/client.ts +127 -219
- package/src/integrations/subagents/delegated-result.ts +31 -0
- package/src/integrations/subagents/diagnostic-format.ts +40 -0
- package/src/integrations/subagents/diagnostic-text.ts +114 -0
- package/src/integrations/subagents/diagnostic-types.ts +77 -0
- package/src/integrations/subagents/diagnostics.ts +24 -977
- package/src/integrations/subagents/failure-correlation.ts +235 -0
- package/src/integrations/subagents/failure-transcript.ts +192 -0
- package/src/integrations/subagents/hidden-bash-failure.ts +98 -0
- package/src/integrations/subagents/protocol-events.ts +27 -0
- package/src/integrations/subagents/protocol.ts +30 -375
- package/src/integrations/subagents/replay-audit.ts +147 -0
- package/src/integrations/subagents/replay-safety.ts +82 -0
- package/src/integrations/subagents/session-diagnostics.ts +258 -0
- package/src/policy/approved-command-extraction.ts +71 -0
- package/src/policy/approved-commands.ts +15 -432
- package/src/policy/bash-authorization.ts +86 -0
- package/src/policy/bash-read-only.ts +161 -0
- package/src/policy/bash-types.ts +20 -0
- package/src/policy/bash.ts +4 -346
- package/src/policy/completion-batch.ts +21 -16
- package/src/policy/immutable-input.ts +5 -2
- package/src/policy/mcp-authorization.ts +71 -0
- package/src/policy/restricted-command.ts +141 -0
- package/src/policy/restricted-git.ts +30 -0
- package/src/policy/reviewed-artifact.ts +97 -0
- package/src/policy/reviewed-command-safety.ts +150 -0
- package/src/policy/reviewed-command-shape.ts +49 -0
- package/src/policy/reviewed-repository-contract.ts +138 -0
- package/src/policy/reviewed-repository-cwd.ts +102 -0
- package/src/policy/tool-call-authorization.ts +58 -0
- package/src/policy/tool-selection.ts +72 -0
- package/src/policy/tool-types.ts +14 -0
- package/src/policy/tools.ts +11 -150
- package/src/preflight.ts +96 -61
- package/src/prompt/main-workflow-notice.ts +41 -0
- package/src/prompt/retry-task.ts +83 -0
- package/src/prompt/step-contract.ts +101 -0
- package/src/prompt/step-sections.ts +75 -0
- package/src/prompt/step-task.ts +156 -0
- package/src/prompt/template.ts +81 -0
- package/src/prompt.ts +10 -254
- package/src/runtime/completion-tool.ts +3 -0
- package/src/runtime/main-step-completion.ts +78 -0
- package/src/runtime/main-step-lifecycle.ts +37 -0
- package/src/runtime/main-step-policy.ts +84 -0
- package/src/runtime/main-step-runtime-types.ts +64 -0
- package/src/runtime/main-step-runtime.ts +129 -200
- package/src/runtime/main-step-state.ts +85 -0
- package/src/runtime/serial-task-queue.ts +48 -13
- package/src/runtime/step-result.ts +48 -27
- package/src/workflow-list.ts +15 -6
- package/src/workflow-status/format-status.ts +52 -0
- package/src/workflow-status/formatting.ts +178 -0
- package/src/workflow-status/layout.ts +109 -0
- package/src/workflow-status/render-board.ts +101 -0
- package/src/workflow-status/render-path.ts +123 -0
- package/src/workflow-status/render-summary.ts +144 -0
- package/src/workflow-status/types.ts +40 -0
- package/src/workflow-status/view.ts +273 -0
- package/src/workflow-status.ts +20 -820
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import type { WorkflowHarnessDependencies } from './dependencies.ts';
|
|
3
|
+
|
|
4
|
+
const CONTEXT_IDLE_TIMEOUT_MS = 30_000;
|
|
5
|
+
const CONTEXT_IDLE_POLL_INTERVAL_MS = 10;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Waits for an interrupted Pi event context to settle before workflow start.
|
|
9
|
+
*/
|
|
10
|
+
export async function waitForEventContextIdle(
|
|
11
|
+
context: ExtensionContext,
|
|
12
|
+
dependencies: Pick<WorkflowHarnessDependencies, 'now' | 'waitForDelay'>,
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
const deadline = dependencies.now() + CONTEXT_IDLE_TIMEOUT_MS;
|
|
15
|
+
while (!context.isIdle()) {
|
|
16
|
+
if (dependencies.now() >= deadline) {
|
|
17
|
+
throw new Error('Timed out waiting for the interrupted Pi turn to stop');
|
|
18
|
+
}
|
|
19
|
+
await dependencies.waitForDelay(CONTEXT_IDLE_POLL_INTERVAL_MS);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
3
|
+
import type { LoadedWorkflow } from '../config/types.ts';
|
|
4
|
+
import { hasRuntimeCommandConflict } from '../config/command-conflicts.ts';
|
|
5
|
+
import { readLatestCheckpoint } from '../engine/checkpoint.ts';
|
|
6
|
+
import { failRun, pauseRun } from '../engine/transitions.ts';
|
|
7
|
+
import { preflightStep } from '../preflight.ts';
|
|
8
|
+
import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
|
|
9
|
+
import { formatCatalogDiagnostics } from './catalog.ts';
|
|
10
|
+
|
|
11
|
+
const STATE_ENTRY_TYPE = 'pi-workflows-state-v1';
|
|
12
|
+
|
|
13
|
+
type HarnessActionContext = Pick<
|
|
14
|
+
FullHarnessActionContext,
|
|
15
|
+
| 'activeDelegation'
|
|
16
|
+
| 'availableSkills'
|
|
17
|
+
| 'catalog'
|
|
18
|
+
| 'catalogLoadSequence'
|
|
19
|
+
| 'dependencies'
|
|
20
|
+
| 'isSessionActive'
|
|
21
|
+
| 'isolateMainSessionTools'
|
|
22
|
+
| 'latestContext'
|
|
23
|
+
| 'launchCurrentStep'
|
|
24
|
+
| 'mainSteps'
|
|
25
|
+
| 'mutationQueue'
|
|
26
|
+
| 'persist'
|
|
27
|
+
| 'pi'
|
|
28
|
+
| 'preflight'
|
|
29
|
+
| 'registeredWorkflowCommands'
|
|
30
|
+
| 'restoreBaselineTools'
|
|
31
|
+
| 'run'
|
|
32
|
+
| 'sessionEpoch'
|
|
33
|
+
| 'start'
|
|
34
|
+
| 'statusShortcut'
|
|
35
|
+
| 'updateStatus'
|
|
36
|
+
>;
|
|
37
|
+
|
|
38
|
+
export type CoreActions = {
|
|
39
|
+
settleAfterTransition: (
|
|
40
|
+
this: HarnessActionContext,
|
|
41
|
+
workflow: LoadedWorkflow,
|
|
42
|
+
) => void;
|
|
43
|
+
preflight: (
|
|
44
|
+
this: HarnessActionContext,
|
|
45
|
+
workflow: LoadedWorkflow,
|
|
46
|
+
stepId: string,
|
|
47
|
+
) => Array<string>;
|
|
48
|
+
isolateMainSessionTools: (this: HarnessActionContext) => void;
|
|
49
|
+
restoreBaselineTools: (this: HarnessActionContext) => void;
|
|
50
|
+
captureSkills: (
|
|
51
|
+
this: HarnessActionContext,
|
|
52
|
+
skills: ReadonlyArray<{ name: string }> | undefined,
|
|
53
|
+
) => void;
|
|
54
|
+
enqueueMutation: (
|
|
55
|
+
this: HarnessActionContext,
|
|
56
|
+
context: ExtensionContext,
|
|
57
|
+
operation: (sessionEpoch: number) => Promise<void>,
|
|
58
|
+
) => Promise<void>;
|
|
59
|
+
persist: (this: HarnessActionContext) => void;
|
|
60
|
+
restoreFromSession: (
|
|
61
|
+
this: HarnessActionContext,
|
|
62
|
+
context: ExtensionContext,
|
|
63
|
+
) => void;
|
|
64
|
+
reloadCatalog: (
|
|
65
|
+
this: HarnessActionContext,
|
|
66
|
+
context: ExtensionContext,
|
|
67
|
+
shouldAnnounce: boolean,
|
|
68
|
+
) => Promise<boolean>;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function settleAfterTransition(
|
|
72
|
+
this: HarnessActionContext,
|
|
73
|
+
workflow: LoadedWorkflow,
|
|
74
|
+
): void {
|
|
75
|
+
if (!this.run) return;
|
|
76
|
+
if (this.run.status === 'running') {
|
|
77
|
+
const preflightErrors = this.preflight(workflow, this.run.currentStepId);
|
|
78
|
+
if (preflightErrors.length > 0) {
|
|
79
|
+
this.run = failRun(
|
|
80
|
+
this.run,
|
|
81
|
+
`Step preflight failed: ${preflightErrors.join('; ')}`,
|
|
82
|
+
this.dependencies.now(),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.persist();
|
|
88
|
+
if (this.run.status !== 'running') {
|
|
89
|
+
this.restoreBaselineTools();
|
|
90
|
+
this.updateStatus();
|
|
91
|
+
if (this.run.status === 'completed') {
|
|
92
|
+
this.latestContext?.ui.notify(
|
|
93
|
+
`Workflow "${this.run.workflowId}" completed`,
|
|
94
|
+
'info',
|
|
95
|
+
);
|
|
96
|
+
} else if (this.run.status === 'paused') {
|
|
97
|
+
this.latestContext?.ui.notify(
|
|
98
|
+
`Workflow paused: ${this.run.pauseReason ?? 'manual action required'}`,
|
|
99
|
+
'warning',
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.isolateMainSessionTools();
|
|
106
|
+
this.updateStatus();
|
|
107
|
+
this.launchCurrentStep(workflow);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function preflight(
|
|
111
|
+
this: HarnessActionContext,
|
|
112
|
+
workflow: LoadedWorkflow,
|
|
113
|
+
stepId: string,
|
|
114
|
+
): Array<string> {
|
|
115
|
+
const step = workflow.definition.steps[stepId];
|
|
116
|
+
if (!step) return [`step "${stepId}" does not exist`];
|
|
117
|
+
return preflightStep(step, {
|
|
118
|
+
tools: this.pi.getAllTools(),
|
|
119
|
+
commands: this.pi.getCommands(),
|
|
120
|
+
skills: this.availableSkills,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isolateMainSessionTools(this: HarnessActionContext): void {
|
|
125
|
+
this.pi.setActiveTools([]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function restoreBaselineTools(this: HarnessActionContext): void {
|
|
129
|
+
this.mainSteps.release();
|
|
130
|
+
if (this.run) {
|
|
131
|
+
this.pi.setActiveTools([...this.run.baselineTools]);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
this.pi.setActiveTools(this.pi.getActiveTools());
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function captureSkills(
|
|
138
|
+
this: HarnessActionContext,
|
|
139
|
+
skills: ReadonlyArray<{ name: string }> | undefined,
|
|
140
|
+
): void {
|
|
141
|
+
if (!skills) return;
|
|
142
|
+
this.availableSkills = new Set(skills.map((skill) => skill.name));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function enqueueMutation(
|
|
146
|
+
this: HarnessActionContext,
|
|
147
|
+
context: ExtensionContext,
|
|
148
|
+
operation: (sessionEpoch: number) => Promise<void>,
|
|
149
|
+
): Promise<void> {
|
|
150
|
+
if (!this.isSessionActive) {
|
|
151
|
+
context.ui.notify('The Pi session is still initializing', 'warning');
|
|
152
|
+
return Promise.resolve();
|
|
153
|
+
}
|
|
154
|
+
const sessionEpoch = this.sessionEpoch;
|
|
155
|
+
return this.mutationQueue.run(async () => {
|
|
156
|
+
if (!this.isSessionActive || this.sessionEpoch !== sessionEpoch) {
|
|
157
|
+
context.ui.notify(
|
|
158
|
+
'Workflow command was superseded by a session change',
|
|
159
|
+
'warning',
|
|
160
|
+
);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
await operation(sessionEpoch);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function persist(this: HarnessActionContext): void {
|
|
168
|
+
if (this.run) {
|
|
169
|
+
this.pi.appendEntry(STATE_ENTRY_TYPE, structuredClone(this.run));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function restoreFromSession(
|
|
174
|
+
this: HarnessActionContext,
|
|
175
|
+
context: ExtensionContext,
|
|
176
|
+
): void {
|
|
177
|
+
this.latestContext = context;
|
|
178
|
+
const previousBaseline = this.run?.baselineTools;
|
|
179
|
+
const entries = context.sessionManager.getBranch();
|
|
180
|
+
const checkpoint = readLatestCheckpoint(entries, STATE_ENTRY_TYPE);
|
|
181
|
+
this.run = checkpoint.status === 'valid' ? checkpoint.run : undefined;
|
|
182
|
+
if (checkpoint.status === 'invalid') {
|
|
183
|
+
context.ui.notify(
|
|
184
|
+
'The newest workflow checkpoint is invalid or from an unsupported version; recovery stopped',
|
|
185
|
+
'error',
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
if (
|
|
189
|
+
this.run &&
|
|
190
|
+
(this.run.status === 'running' || this.run.status === 'awaiting-gate')
|
|
191
|
+
) {
|
|
192
|
+
this.run = pauseRun(
|
|
193
|
+
this.run,
|
|
194
|
+
'Session was restored; inspect the checkpoint before resuming',
|
|
195
|
+
this.dependencies.now(),
|
|
196
|
+
);
|
|
197
|
+
this.persist();
|
|
198
|
+
}
|
|
199
|
+
if (!this.run && previousBaseline) {
|
|
200
|
+
this.pi.setActiveTools([...previousBaseline]);
|
|
201
|
+
} else {
|
|
202
|
+
this.restoreBaselineTools();
|
|
203
|
+
}
|
|
204
|
+
if (this.activeDelegation) this.isolateMainSessionTools();
|
|
205
|
+
this.updateStatus();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function reloadCatalog(
|
|
209
|
+
this: HarnessActionContext,
|
|
210
|
+
context: ExtensionContext,
|
|
211
|
+
shouldAnnounce: boolean,
|
|
212
|
+
): Promise<boolean> {
|
|
213
|
+
const loadSequence = ++this.catalogLoadSequence;
|
|
214
|
+
const sessionEpoch = this.sessionEpoch;
|
|
215
|
+
const catalog = await this.dependencies.loadCatalog({
|
|
216
|
+
cwd: context.cwd,
|
|
217
|
+
projectTrusted: context.isProjectTrusted(),
|
|
218
|
+
});
|
|
219
|
+
if (
|
|
220
|
+
loadSequence !== this.catalogLoadSequence ||
|
|
221
|
+
sessionEpoch !== this.sessionEpoch
|
|
222
|
+
) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
this.latestContext = context;
|
|
226
|
+
const workflows = new Map(catalog.workflows);
|
|
227
|
+
const diagnostics = [...catalog.diagnostics];
|
|
228
|
+
if (catalog.settings.statusShortcut !== this.statusShortcut) {
|
|
229
|
+
diagnostics.push({
|
|
230
|
+
level: 'warning',
|
|
231
|
+
path: join(catalog.userDirectory, 'settings.yaml'),
|
|
232
|
+
message:
|
|
233
|
+
`settings.statusShortcut is "${catalog.settings.statusShortcut}", ` +
|
|
234
|
+
`but the active shortcut is "${this.statusShortcut}"; run Pi /reload to apply shortcut changes`,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
const availableCommands = this.pi.getCommands();
|
|
238
|
+
for (const [workflowId, workflow] of catalog.workflows) {
|
|
239
|
+
const command = workflow.definition.command;
|
|
240
|
+
if (
|
|
241
|
+
hasRuntimeCommandConflict(
|
|
242
|
+
command,
|
|
243
|
+
availableCommands,
|
|
244
|
+
this.registeredWorkflowCommands,
|
|
245
|
+
)
|
|
246
|
+
) {
|
|
247
|
+
workflows.delete(workflowId);
|
|
248
|
+
diagnostics.push({
|
|
249
|
+
level: 'error',
|
|
250
|
+
path: workflow.sourcePath,
|
|
251
|
+
message: `command "/${command}" conflicts with another loaded Pi resource`,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
this.catalog = { ...catalog, workflows, diagnostics };
|
|
256
|
+
for (const workflow of workflows.values()) {
|
|
257
|
+
this.pi.registerCommand(workflow.definition.command, {
|
|
258
|
+
description: workflow.definition.description,
|
|
259
|
+
handler: async (args, commandContext) =>
|
|
260
|
+
this.start(workflow.definition.id, args, commandContext),
|
|
261
|
+
});
|
|
262
|
+
this.registeredWorkflowCommands.add(workflow.definition.command);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const diagnosticText = formatCatalogDiagnostics(this.catalog);
|
|
266
|
+
if (shouldAnnounce) {
|
|
267
|
+
context.ui.notify(
|
|
268
|
+
diagnosticText
|
|
269
|
+
? `Loaded ${this.catalog.workflows.size} workflow(s)\n${diagnosticText}`
|
|
270
|
+
: `Loaded ${this.catalog.workflows.size} workflow(s)`,
|
|
271
|
+
diagnosticText ? 'warning' : 'info',
|
|
272
|
+
);
|
|
273
|
+
} else if (
|
|
274
|
+
this.catalog.diagnostics.some((diagnostic) => diagnostic.level === 'error')
|
|
275
|
+
) {
|
|
276
|
+
context.ui.notify(
|
|
277
|
+
`Workflow configuration errors:\n${diagnosticText}`,
|
|
278
|
+
'warning',
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Returns shared transition, persistence, preflight, and catalog actions.
|
|
286
|
+
*/
|
|
287
|
+
export function createCoreActions(): CoreActions {
|
|
288
|
+
return {
|
|
289
|
+
settleAfterTransition,
|
|
290
|
+
preflight,
|
|
291
|
+
isolateMainSessionTools,
|
|
292
|
+
restoreBaselineTools,
|
|
293
|
+
captureSkills,
|
|
294
|
+
enqueueMutation,
|
|
295
|
+
persist,
|
|
296
|
+
restoreFromSession,
|
|
297
|
+
reloadCatalog,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { failRun } from '../engine/transitions.ts';
|
|
2
|
+
import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
|
|
3
|
+
import {
|
|
4
|
+
boundedFailureField,
|
|
5
|
+
MAX_DELEGATION_RECOVERY_ATTEMPTS,
|
|
6
|
+
} from './delegation-retry-policy.ts';
|
|
7
|
+
import type { ActiveDelegation, DelegationFailureDetails } from './types.ts';
|
|
8
|
+
|
|
9
|
+
type HarnessActionContext = Pick<
|
|
10
|
+
FullHarnessActionContext,
|
|
11
|
+
| 'activeDelegation'
|
|
12
|
+
| 'catalog'
|
|
13
|
+
| 'cleanupDelegation'
|
|
14
|
+
| 'delegationFailures'
|
|
15
|
+
| 'dependencies'
|
|
16
|
+
| 'isSessionActive'
|
|
17
|
+
| 'isolateMainSessionTools'
|
|
18
|
+
| 'latestContext'
|
|
19
|
+
| 'launchCurrentStep'
|
|
20
|
+
| 'mainSteps'
|
|
21
|
+
| 'pauseForExecutionFailure'
|
|
22
|
+
| 'persist'
|
|
23
|
+
| 'restoreBaselineTools'
|
|
24
|
+
| 'run'
|
|
25
|
+
| 'sessionEpoch'
|
|
26
|
+
| 'subagents'
|
|
27
|
+
| 'updateStatus'
|
|
28
|
+
>;
|
|
29
|
+
|
|
30
|
+
export type DelegationControlActions = {
|
|
31
|
+
cancelActiveDelegation: (
|
|
32
|
+
this: HarnessActionContext,
|
|
33
|
+
reason: string,
|
|
34
|
+
) => Promise<boolean>;
|
|
35
|
+
cleanupDelegation: (
|
|
36
|
+
this: HarnessActionContext,
|
|
37
|
+
active: ActiveDelegation,
|
|
38
|
+
) => Promise<void>;
|
|
39
|
+
retryDelegationAfterFailure: (
|
|
40
|
+
this: HarnessActionContext,
|
|
41
|
+
active: ActiveDelegation,
|
|
42
|
+
failure: DelegationFailureDetails | undefined,
|
|
43
|
+
reason: string,
|
|
44
|
+
) => boolean;
|
|
45
|
+
pauseForDelegationFailure: (
|
|
46
|
+
this: HarnessActionContext,
|
|
47
|
+
reason: string,
|
|
48
|
+
) => void;
|
|
49
|
+
pauseForExecutionFailure: (
|
|
50
|
+
this: HarnessActionContext,
|
|
51
|
+
label: string,
|
|
52
|
+
reason: string,
|
|
53
|
+
) => void;
|
|
54
|
+
retainUnconfirmedDelegation: (
|
|
55
|
+
this: HarnessActionContext,
|
|
56
|
+
active: ActiveDelegation,
|
|
57
|
+
reason: string,
|
|
58
|
+
) => void;
|
|
59
|
+
releaseMainAfterCancellation: (
|
|
60
|
+
this: HarnessActionContext,
|
|
61
|
+
active: ActiveDelegation,
|
|
62
|
+
) => void;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
async function cancelActiveDelegation(
|
|
66
|
+
this: HarnessActionContext,
|
|
67
|
+
reason: string,
|
|
68
|
+
): Promise<boolean> {
|
|
69
|
+
const active = this.activeDelegation;
|
|
70
|
+
if (!active) return true;
|
|
71
|
+
active.cancelling = true;
|
|
72
|
+
active.progress = 'cancelling';
|
|
73
|
+
this.updateStatus();
|
|
74
|
+
if (this.subagents.activeRequestId !== active.requestId) {
|
|
75
|
+
active.progress = 'cancellation unconfirmed';
|
|
76
|
+
this.updateStatus();
|
|
77
|
+
this.latestContext?.ui.notify(
|
|
78
|
+
`${reason}; the delegation channel already closed without a terminal response`,
|
|
79
|
+
'warning',
|
|
80
|
+
);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const isConfirmed = await this.subagents.cancelActiveAndWait();
|
|
84
|
+
if (isConfirmed && this.activeDelegation === active) {
|
|
85
|
+
this.activeDelegation = undefined;
|
|
86
|
+
await this.cleanupDelegation(active);
|
|
87
|
+
} else if (!isConfirmed) {
|
|
88
|
+
this.latestContext?.ui.notify(
|
|
89
|
+
`${reason}; waiting for subagent "${active.agent}" to confirm termination`,
|
|
90
|
+
'warning',
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return isConfirmed;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function cleanupDelegation(
|
|
97
|
+
this: HarnessActionContext,
|
|
98
|
+
active: ActiveDelegation,
|
|
99
|
+
): Promise<void> {
|
|
100
|
+
try {
|
|
101
|
+
await this.dependencies.removeDelegationWorkspace(active.resultDirectory);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
104
|
+
try {
|
|
105
|
+
this.latestContext?.ui.notify(
|
|
106
|
+
`Could not remove temporary delegation workspace "${active.resultDirectory}": ${boundedFailureField(detail)}`,
|
|
107
|
+
'warning',
|
|
108
|
+
);
|
|
109
|
+
} catch {
|
|
110
|
+
// Cleanup and its warning are housekeeping; neither may pause the run.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function retryDelegationAfterFailure(
|
|
116
|
+
this: HarnessActionContext,
|
|
117
|
+
active: ActiveDelegation,
|
|
118
|
+
failure: DelegationFailureDetails | undefined,
|
|
119
|
+
reason: string,
|
|
120
|
+
): boolean {
|
|
121
|
+
if (!failure) return false;
|
|
122
|
+
const fingerprint =
|
|
123
|
+
this.delegationFailures.delegationFailureFingerprint(failure);
|
|
124
|
+
const isRepeatedFailure = active.recoveryFailures.some(
|
|
125
|
+
(previousFailure) => previousFailure.fingerprint === fingerprint,
|
|
126
|
+
);
|
|
127
|
+
if (
|
|
128
|
+
active.recoveryAttemptCount >= MAX_DELEGATION_RECOVERY_ATTEMPTS ||
|
|
129
|
+
isRepeatedFailure ||
|
|
130
|
+
!this.delegationFailures.isRetryableTerminalFailure(failure) ||
|
|
131
|
+
!this.delegationFailures.isSafeToRetryDelegation(
|
|
132
|
+
active.policy,
|
|
133
|
+
active.broadRecoveryAuthorized,
|
|
134
|
+
failure.replayAudit,
|
|
135
|
+
) ||
|
|
136
|
+
!this.isSessionActive ||
|
|
137
|
+
this.sessionEpoch !== active.sessionEpoch ||
|
|
138
|
+
!this.run ||
|
|
139
|
+
this.run.status !== 'running' ||
|
|
140
|
+
this.run.runId !== active.runId ||
|
|
141
|
+
this.run.currentStepId !== active.stepId ||
|
|
142
|
+
this.run.currentStepDigest !== active.stepDigest ||
|
|
143
|
+
this.activeDelegation
|
|
144
|
+
) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
const workflow = this.catalog.workflows.get(this.run.workflowId);
|
|
148
|
+
if (!workflow) return false;
|
|
149
|
+
|
|
150
|
+
const attempt = active.recoveryAttemptCount + 1;
|
|
151
|
+
this.latestContext?.ui.notify(
|
|
152
|
+
`Automatic recovery for "${active.stepId}" after a subagent failure (${attempt}/${MAX_DELEGATION_RECOVERY_ATTEMPTS})`,
|
|
153
|
+
'warning',
|
|
154
|
+
);
|
|
155
|
+
this.launchCurrentStep(workflow, {
|
|
156
|
+
attempt,
|
|
157
|
+
failures: [...active.recoveryFailures, { fingerprint, reason }],
|
|
158
|
+
});
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function pauseForDelegationFailure(
|
|
163
|
+
this: HarnessActionContext,
|
|
164
|
+
reason: string,
|
|
165
|
+
): void {
|
|
166
|
+
this.pauseForExecutionFailure('Subagent step', reason);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function pauseForExecutionFailure(
|
|
170
|
+
this: HarnessActionContext,
|
|
171
|
+
label: string,
|
|
172
|
+
reason: string,
|
|
173
|
+
): void {
|
|
174
|
+
if (!this.run || this.run.status !== 'running') return;
|
|
175
|
+
this.mainSteps.deactivate();
|
|
176
|
+
this.run = failRun(
|
|
177
|
+
this.run,
|
|
178
|
+
`${label} failed: ${reason}`,
|
|
179
|
+
this.dependencies.now(),
|
|
180
|
+
);
|
|
181
|
+
this.persist();
|
|
182
|
+
if (this.activeDelegation) {
|
|
183
|
+
this.isolateMainSessionTools();
|
|
184
|
+
} else {
|
|
185
|
+
this.restoreBaselineTools();
|
|
186
|
+
}
|
|
187
|
+
this.updateStatus();
|
|
188
|
+
this.latestContext?.ui.notify(
|
|
189
|
+
`Workflow paused at "${this.run.currentStepId}": ${reason}`,
|
|
190
|
+
'error',
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function retainUnconfirmedDelegation(
|
|
195
|
+
this: HarnessActionContext,
|
|
196
|
+
active: ActiveDelegation,
|
|
197
|
+
reason: string,
|
|
198
|
+
): void {
|
|
199
|
+
active.cancelling = true;
|
|
200
|
+
active.progress = 'cancellation unconfirmed';
|
|
201
|
+
if (this.run?.status === 'running') {
|
|
202
|
+
this.run = failRun(
|
|
203
|
+
this.run,
|
|
204
|
+
`Subagent step failed: ${reason}`,
|
|
205
|
+
this.dependencies.now(),
|
|
206
|
+
);
|
|
207
|
+
this.persist();
|
|
208
|
+
}
|
|
209
|
+
this.isolateMainSessionTools();
|
|
210
|
+
this.updateStatus();
|
|
211
|
+
this.latestContext?.ui.notify(
|
|
212
|
+
`Workflow paused, but subagent "${active.agent}" has not confirmed termination. Main tools and resume remain blocked; restart Pi if no terminal response arrives.`,
|
|
213
|
+
'error',
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function releaseMainAfterCancellation(
|
|
218
|
+
this: HarnessActionContext,
|
|
219
|
+
active: ActiveDelegation,
|
|
220
|
+
): void {
|
|
221
|
+
if (this.activeDelegation === active) this.activeDelegation = undefined;
|
|
222
|
+
if (
|
|
223
|
+
!this.activeDelegation &&
|
|
224
|
+
this.run &&
|
|
225
|
+
this.run.status !== 'running' &&
|
|
226
|
+
this.run.status !== 'awaiting-gate'
|
|
227
|
+
) {
|
|
228
|
+
this.restoreBaselineTools();
|
|
229
|
+
this.updateStatus();
|
|
230
|
+
this.latestContext?.ui.notify(
|
|
231
|
+
`Subagent "${active.agent}" has terminated; main tools are restored`,
|
|
232
|
+
'info',
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Returns delegation cancellation and recovery actions for composition.
|
|
239
|
+
*/
|
|
240
|
+
export function createDelegationControlActions(): DelegationControlActions {
|
|
241
|
+
return {
|
|
242
|
+
cancelActiveDelegation,
|
|
243
|
+
cleanupDelegation,
|
|
244
|
+
retryDelegationAfterFailure,
|
|
245
|
+
pauseForDelegationFailure,
|
|
246
|
+
pauseForExecutionFailure,
|
|
247
|
+
retainUnconfirmedDelegation,
|
|
248
|
+
releaseMainAfterCancellation,
|
|
249
|
+
};
|
|
250
|
+
}
|