@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
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import type { WorkflowStepResult } from '../runtime/step-result.ts';
|
|
2
|
+
import {
|
|
3
|
+
failedToolName,
|
|
4
|
+
formatToolFailureDiagnostic,
|
|
5
|
+
} from '../integrations/subagents/diagnostics.ts';
|
|
6
|
+
import type {
|
|
7
|
+
ChildStepPolicy,
|
|
8
|
+
SubagentDelegationResponse,
|
|
9
|
+
} from '../integrations/subagents/protocol.ts';
|
|
10
|
+
import type { WorkflowHarnessDependencies } from './dependencies.ts';
|
|
11
|
+
import {
|
|
12
|
+
completionMatchesResult,
|
|
13
|
+
recoveredProjectionError,
|
|
14
|
+
} from './delegation-recovery-validation.ts';
|
|
15
|
+
import {
|
|
16
|
+
boundedFailureField,
|
|
17
|
+
delegationFailureFingerprint,
|
|
18
|
+
isRetryableTerminalFailure,
|
|
19
|
+
isSafeToRetryDelegation,
|
|
20
|
+
rejectedRecoveryReason,
|
|
21
|
+
} from './delegation-retry-policy.ts';
|
|
22
|
+
import type {
|
|
23
|
+
ActiveDelegation,
|
|
24
|
+
DelegationFailureDetails,
|
|
25
|
+
DelegationRecoveryBlocker,
|
|
26
|
+
} from './types.ts';
|
|
27
|
+
|
|
28
|
+
export type DelegationFailureActions = {
|
|
29
|
+
delegationFailureFingerprint: (failure: DelegationFailureDetails) => string;
|
|
30
|
+
hasContradictoryCompletion: (response: SubagentDelegationResponse) => boolean;
|
|
31
|
+
isRetryableTerminalFailure: (failure: DelegationFailureDetails) => boolean;
|
|
32
|
+
isSafeToRetryDelegation: (
|
|
33
|
+
policy: ChildStepPolicy,
|
|
34
|
+
isReplayExplicitlyAuthorized: boolean,
|
|
35
|
+
replayAudit: DelegationFailureDetails['replayAudit'],
|
|
36
|
+
) => boolean;
|
|
37
|
+
rejectedRecoveryReason: (
|
|
38
|
+
failure: DelegationFailureDetails,
|
|
39
|
+
error: unknown,
|
|
40
|
+
) => string;
|
|
41
|
+
completionMatchesResult: (
|
|
42
|
+
diagnostic: NonNullable<DelegationFailureDetails['diagnostic']>,
|
|
43
|
+
result: WorkflowStepResult,
|
|
44
|
+
policy: ChildStepPolicy,
|
|
45
|
+
) => boolean;
|
|
46
|
+
recoveredProjectionError: (
|
|
47
|
+
active: ActiveDelegation,
|
|
48
|
+
response: SubagentDelegationResponse,
|
|
49
|
+
diagnostic: NonNullable<DelegationFailureDetails['diagnostic']>,
|
|
50
|
+
) => string | undefined;
|
|
51
|
+
describeDelegationFailure: (
|
|
52
|
+
active: ActiveDelegation,
|
|
53
|
+
response: SubagentDelegationResponse,
|
|
54
|
+
) => Promise<DelegationFailureDetails>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function nonEmptyTerminalError(
|
|
58
|
+
response: SubagentDelegationResponse,
|
|
59
|
+
): string | undefined {
|
|
60
|
+
return [response.error, response.execution?.error].find(
|
|
61
|
+
(error): error is string =>
|
|
62
|
+
typeof error === 'string' && error.trim().length > 0,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function nonzeroTerminalExitCode(
|
|
67
|
+
response: SubagentDelegationResponse,
|
|
68
|
+
): number | undefined {
|
|
69
|
+
return [response.exitCode, response.execution?.exitCode].find(
|
|
70
|
+
(exitCode): exitCode is number =>
|
|
71
|
+
typeof exitCode === 'number' &&
|
|
72
|
+
Number.isSafeInteger(exitCode) &&
|
|
73
|
+
exitCode !== 0,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function hasContradictoryCompletion(
|
|
78
|
+
response: SubagentDelegationResponse,
|
|
79
|
+
): boolean {
|
|
80
|
+
return (
|
|
81
|
+
response.status === 'completed' &&
|
|
82
|
+
(nonEmptyTerminalError(response) !== undefined ||
|
|
83
|
+
nonzeroTerminalExitCode(response) !== undefined)
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function recoveryBlocker(
|
|
88
|
+
response: SubagentDelegationResponse,
|
|
89
|
+
): DelegationRecoveryBlocker | undefined {
|
|
90
|
+
const execution = response.execution;
|
|
91
|
+
const fileMutation = response.effects?.fileMutation;
|
|
92
|
+
if (fileMutation?.attempted === true || fileMutation?.status === 'observed') {
|
|
93
|
+
return 'reported-mutation';
|
|
94
|
+
}
|
|
95
|
+
if (execution?.detached === true || execution?.status === 'detached') {
|
|
96
|
+
return 'detached';
|
|
97
|
+
}
|
|
98
|
+
if (execution?.stopped === true || execution?.status === 'stopped') {
|
|
99
|
+
return 'stopped';
|
|
100
|
+
}
|
|
101
|
+
if (
|
|
102
|
+
response.status === 'interrupted' ||
|
|
103
|
+
execution?.interrupted === true ||
|
|
104
|
+
execution?.status === 'paused'
|
|
105
|
+
) {
|
|
106
|
+
return 'interrupted';
|
|
107
|
+
}
|
|
108
|
+
if (response.status === 'cancelled') return 'cancelled';
|
|
109
|
+
if (execution?.timedOut === true && response.status !== 'timed_out') {
|
|
110
|
+
return 'inconsistent-timeout';
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function validateReplayAudit(
|
|
116
|
+
response: SubagentDelegationResponse,
|
|
117
|
+
replayAudit: DelegationFailureDetails['replayAudit'],
|
|
118
|
+
): DelegationFailureDetails['replayAudit'] {
|
|
119
|
+
if (!replayAudit) return undefined;
|
|
120
|
+
if (
|
|
121
|
+
response.toolCount !== undefined &&
|
|
122
|
+
response.toolCount !== replayAudit.toolCount
|
|
123
|
+
) {
|
|
124
|
+
return { ...replayAudit, replaySafe: false };
|
|
125
|
+
}
|
|
126
|
+
return replayAudit;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Creates pure delegation-failure evaluators plus the injected diagnostic
|
|
131
|
+
* reader used at the transcript I/O boundary.
|
|
132
|
+
*/
|
|
133
|
+
export function createDelegationFailureActions(
|
|
134
|
+
dependencies: Pick<
|
|
135
|
+
WorkflowHarnessDependencies,
|
|
136
|
+
'readDelegationReplayAudit' | 'readToolFailureDiagnostic'
|
|
137
|
+
>,
|
|
138
|
+
): DelegationFailureActions {
|
|
139
|
+
const describeDelegationFailure = async (
|
|
140
|
+
active: ActiveDelegation,
|
|
141
|
+
response: SubagentDelegationResponse,
|
|
142
|
+
): Promise<DelegationFailureDetails> => {
|
|
143
|
+
const blocker = recoveryBlocker(response);
|
|
144
|
+
const terminalError = nonEmptyTerminalError(response);
|
|
145
|
+
const error =
|
|
146
|
+
terminalError ?? 'The subagent returned no terminal error details.';
|
|
147
|
+
const isResponseIdentityValid =
|
|
148
|
+
response.childIndex === 0 &&
|
|
149
|
+
(response.agent === undefined || response.agent === active.agent);
|
|
150
|
+
const identity =
|
|
151
|
+
isResponseIdentityValid && response.runId !== undefined
|
|
152
|
+
? { runId: response.runId, childIndex: 0 }
|
|
153
|
+
: undefined;
|
|
154
|
+
const [diagnostic, replayAudit] = await Promise.all([
|
|
155
|
+
dependencies.readToolFailureDiagnostic(
|
|
156
|
+
response.sessionFile,
|
|
157
|
+
active.trustedSessionRoot,
|
|
158
|
+
identity,
|
|
159
|
+
failedToolName(terminalError),
|
|
160
|
+
terminalError,
|
|
161
|
+
),
|
|
162
|
+
dependencies.readDelegationReplayAudit(
|
|
163
|
+
response.sessionFile,
|
|
164
|
+
active.trustedSessionRoot,
|
|
165
|
+
identity,
|
|
166
|
+
{
|
|
167
|
+
task: active.transcriptTask,
|
|
168
|
+
bashPermission: active.policy.permissions.bash,
|
|
169
|
+
approvedBashCommands: active.policy.approvedBashCommands ?? [],
|
|
170
|
+
},
|
|
171
|
+
),
|
|
172
|
+
]);
|
|
173
|
+
const validatedReplayAudit = validateReplayAudit(response, replayAudit);
|
|
174
|
+
const exitCode =
|
|
175
|
+
nonzeroTerminalExitCode(response) ??
|
|
176
|
+
response.exitCode ??
|
|
177
|
+
response.execution?.exitCode;
|
|
178
|
+
const reason = [
|
|
179
|
+
hasContradictoryCompletion(response)
|
|
180
|
+
? `Subagent "${active.agent}" reported terminal failure signals with completed status.`
|
|
181
|
+
: `Subagent "${active.agent}" ${response.status.replaceAll('_', ' ')}.`,
|
|
182
|
+
...(diagnostic ? formatToolFailureDiagnostic(diagnostic) : []),
|
|
183
|
+
...(exitCode !== undefined ? [`Subagent exit code: ${exitCode}`] : []),
|
|
184
|
+
...(blocker
|
|
185
|
+
? [`Automatic recovery blocked by: ${blocker.replaceAll('-', ' ')}`]
|
|
186
|
+
: []),
|
|
187
|
+
`Terminal error: ${boundedFailureField(error)}`,
|
|
188
|
+
...(diagnostic && response.sessionFile
|
|
189
|
+
? [
|
|
190
|
+
`Diagnostic session: ${boundedFailureField(response.sessionFile.replaceAll(/\s+/g, ' '))}`,
|
|
191
|
+
]
|
|
192
|
+
: []),
|
|
193
|
+
].join('\n');
|
|
194
|
+
return {
|
|
195
|
+
reason,
|
|
196
|
+
status: response.status,
|
|
197
|
+
...(terminalError ? { error: terminalError } : {}),
|
|
198
|
+
...(exitCode !== undefined ? { exitCode } : {}),
|
|
199
|
+
...(blocker ? { recoveryBlocker: blocker } : {}),
|
|
200
|
+
...(diagnostic ? { diagnostic } : {}),
|
|
201
|
+
...(validatedReplayAudit ? { replayAudit: validatedReplayAudit } : {}),
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
delegationFailureFingerprint,
|
|
207
|
+
hasContradictoryCompletion,
|
|
208
|
+
isRetryableTerminalFailure,
|
|
209
|
+
isSafeToRetryDelegation,
|
|
210
|
+
rejectedRecoveryReason,
|
|
211
|
+
completionMatchesResult,
|
|
212
|
+
recoveredProjectionError,
|
|
213
|
+
describeDelegationFailure,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import type { LoadedWorkflow, WorkflowStep } from '../config/types.ts';
|
|
3
|
+
import type { WorkflowRun } from '../engine/state.ts';
|
|
4
|
+
import { allowedOutcomes } from '../engine/transitions.ts';
|
|
5
|
+
import { digest } from '../digest.ts';
|
|
6
|
+
import { deriveSubagentSessionRoot } from '../integrations/subagents/diagnostics.ts';
|
|
7
|
+
import {
|
|
8
|
+
encodeChildPolicy,
|
|
9
|
+
type ChildStepPolicy,
|
|
10
|
+
type SubagentDelegationRequest,
|
|
11
|
+
} from '../integrations/subagents/protocol.ts';
|
|
12
|
+
import {
|
|
13
|
+
narrowApprovedBashCommands,
|
|
14
|
+
resolveReviewedRepositoryCwd,
|
|
15
|
+
} from '../policy/approved-commands.ts';
|
|
16
|
+
import { automaticRecoveryTask, buildDelegatedStepTask } from '../prompt.ts';
|
|
17
|
+
import { WORKFLOW_COMPLETION_PARAMETERS } from '../runtime/completion-tool.ts';
|
|
18
|
+
import { MAX_DELEGATION_RECOVERY_ATTEMPTS } from './delegation-retry-policy.ts';
|
|
19
|
+
import type { WorkflowHarnessDependencies } from './dependencies.ts';
|
|
20
|
+
import type { ActiveDelegation, DelegationRecovery } from './types.ts';
|
|
21
|
+
|
|
22
|
+
type DelegationPlanInput = {
|
|
23
|
+
workflow: LoadedWorkflow;
|
|
24
|
+
run: WorkflowRun;
|
|
25
|
+
step: WorkflowStep;
|
|
26
|
+
sessionEpoch: number;
|
|
27
|
+
latestContext: ExtensionContext | undefined;
|
|
28
|
+
recovery: DelegationRecovery | undefined;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type DelegationPlan =
|
|
32
|
+
| {
|
|
33
|
+
kind: 'invalid';
|
|
34
|
+
reason: string;
|
|
35
|
+
}
|
|
36
|
+
| {
|
|
37
|
+
kind: 'ready';
|
|
38
|
+
active: ActiveDelegation;
|
|
39
|
+
request: SubagentDelegationRequest;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function delegationTranscriptBinding(
|
|
43
|
+
requestId: string,
|
|
44
|
+
policyDigest: string,
|
|
45
|
+
): string {
|
|
46
|
+
return `<pi-workflows-delegation-binding-v1>${requestId}:${policyDigest}</pi-workflows-delegation-binding-v1>`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function approvedCommands(
|
|
50
|
+
run: WorkflowRun,
|
|
51
|
+
step: WorkflowStep,
|
|
52
|
+
): ReadonlyArray<string> {
|
|
53
|
+
return narrowApprovedBashCommands(
|
|
54
|
+
run.reviewedArtifact ?? '',
|
|
55
|
+
run.stepHandoff ?? '',
|
|
56
|
+
step.permissions.bash.approvedSources ?? [],
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Creates one immutable subagent request from workflow state and injected
|
|
62
|
+
* identity/workspace effects.
|
|
63
|
+
*/
|
|
64
|
+
export function createDelegationPlan(
|
|
65
|
+
input: DelegationPlanInput,
|
|
66
|
+
dependencies: Pick<
|
|
67
|
+
WorkflowHarnessDependencies,
|
|
68
|
+
'createDelegationWorkspace' | 'createRequestId' | 'currentWorkingDirectory'
|
|
69
|
+
>,
|
|
70
|
+
): DelegationPlan {
|
|
71
|
+
const { workflow, run, step, latestContext, recovery } = input;
|
|
72
|
+
const subagent = step.subagent;
|
|
73
|
+
if (!subagent) {
|
|
74
|
+
return {
|
|
75
|
+
kind: 'invalid',
|
|
76
|
+
reason: `Step "${run.currentStepId}" has no subagent configuration`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const reviewedRepository = resolveReviewedRepositoryCwd(
|
|
81
|
+
run.reviewedArtifact ?? '',
|
|
82
|
+
);
|
|
83
|
+
if (reviewedRepository.kind === 'invalid') {
|
|
84
|
+
return { kind: 'invalid', reason: reviewedRepository.reason };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const delegationCwd =
|
|
88
|
+
reviewedRepository.kind === 'resolved'
|
|
89
|
+
? reviewedRepository.cwd
|
|
90
|
+
: (latestContext?.cwd ?? dependencies.currentWorkingDirectory());
|
|
91
|
+
const requestId =
|
|
92
|
+
`${run.runId}:${run.currentStepId}:` + dependencies.createRequestId();
|
|
93
|
+
const workspace = dependencies.createDelegationWorkspace();
|
|
94
|
+
const outcomes = allowedOutcomes(workflow, run);
|
|
95
|
+
const outcomeSet = new Set(outcomes);
|
|
96
|
+
const approvedBashCommands = approvedCommands(run, step);
|
|
97
|
+
const repositoryPolicy =
|
|
98
|
+
reviewedRepository.kind === 'resolved'
|
|
99
|
+
? {
|
|
100
|
+
repositoryCwd: reviewedRepository.repositoryCwd,
|
|
101
|
+
...(reviewedRepository.bootstrapping
|
|
102
|
+
? { bootstrapCwd: reviewedRepository.cwd }
|
|
103
|
+
: {}),
|
|
104
|
+
}
|
|
105
|
+
: {};
|
|
106
|
+
const policyDigest = digest({
|
|
107
|
+
version: 1,
|
|
108
|
+
requestId,
|
|
109
|
+
agent: subagent.agent,
|
|
110
|
+
runId: run.runId,
|
|
111
|
+
stepId: run.currentStepId,
|
|
112
|
+
stepDigest: run.currentStepDigest,
|
|
113
|
+
capabilityPath: workspace.capabilityPath,
|
|
114
|
+
resultPath: workspace.resultPath,
|
|
115
|
+
approvedBashCommands,
|
|
116
|
+
...repositoryPolicy,
|
|
117
|
+
});
|
|
118
|
+
const policy: ChildStepPolicy = {
|
|
119
|
+
version: 1,
|
|
120
|
+
requestId,
|
|
121
|
+
agent: subagent.agent,
|
|
122
|
+
workflowId: workflow.definition.id,
|
|
123
|
+
runId: run.runId,
|
|
124
|
+
stepId: run.currentStepId,
|
|
125
|
+
stepTitle: step.title,
|
|
126
|
+
policyDigest,
|
|
127
|
+
capabilityPath: workspace.capabilityPath,
|
|
128
|
+
capabilityToken: workspace.capabilityToken,
|
|
129
|
+
resultPath: workspace.resultPath,
|
|
130
|
+
permissions: structuredClone(step.permissions),
|
|
131
|
+
...(approvedBashCommands.length > 0 ? { approvedBashCommands } : {}),
|
|
132
|
+
...repositoryPolicy,
|
|
133
|
+
outcomes,
|
|
134
|
+
pauseOutcomes: Object.entries(step.transitions)
|
|
135
|
+
.filter(
|
|
136
|
+
([outcome, target]) => target === '$pause' && outcomeSet.has(outcome),
|
|
137
|
+
)
|
|
138
|
+
.map(([outcome]) => outcome),
|
|
139
|
+
summaryMaxChars: workflow.definition.summaryMaxChars,
|
|
140
|
+
...(step.gate ? { gateSubmitOutcome: step.gate.submitOutcome } : {}),
|
|
141
|
+
};
|
|
142
|
+
const trustedSessionRoot = deriveSubagentSessionRoot(
|
|
143
|
+
latestContext?.sessionManager.getSessionFile(),
|
|
144
|
+
);
|
|
145
|
+
const transcriptTask = [
|
|
146
|
+
buildDelegatedStepTask(workflow, run, ''),
|
|
147
|
+
delegationTranscriptBinding(requestId, policyDigest),
|
|
148
|
+
...(recovery
|
|
149
|
+
? [
|
|
150
|
+
automaticRecoveryTask(
|
|
151
|
+
recovery.failures.map(({ reason }) => reason),
|
|
152
|
+
recovery.attempt,
|
|
153
|
+
MAX_DELEGATION_RECOVERY_ATTEMPTS,
|
|
154
|
+
),
|
|
155
|
+
]
|
|
156
|
+
: []),
|
|
157
|
+
].join('\n\n');
|
|
158
|
+
const active: ActiveDelegation = {
|
|
159
|
+
requestId,
|
|
160
|
+
runId: run.runId,
|
|
161
|
+
stepId: run.currentStepId,
|
|
162
|
+
stepDigest: run.currentStepDigest,
|
|
163
|
+
sessionEpoch: input.sessionEpoch,
|
|
164
|
+
resultDirectory: workspace.resultDirectory,
|
|
165
|
+
policy,
|
|
166
|
+
transcriptTask,
|
|
167
|
+
agent: subagent.agent,
|
|
168
|
+
...(trustedSessionRoot ? { trustedSessionRoot } : {}),
|
|
169
|
+
broadRecoveryAuthorized: subagent.retryToolFailures,
|
|
170
|
+
recoveryAttemptCount: recovery?.attempt ?? 0,
|
|
171
|
+
recoveryFailures: recovery?.failures ?? [],
|
|
172
|
+
};
|
|
173
|
+
const request: SubagentDelegationRequest = {
|
|
174
|
+
version: 1,
|
|
175
|
+
requestId,
|
|
176
|
+
agent: subagent.agent,
|
|
177
|
+
task: `${encodeChildPolicy(policy)}\n\n${transcriptTask}`,
|
|
178
|
+
// Workflow steps are isolation boundaries. Parent/sibling transcripts are
|
|
179
|
+
// replaced by the explicit workflow handoff.
|
|
180
|
+
context: 'fresh',
|
|
181
|
+
cwd: delegationCwd,
|
|
182
|
+
timeoutMs: subagent.timeoutMs,
|
|
183
|
+
skill:
|
|
184
|
+
step.permissions.skills.length > 0 ? [...step.permissions.skills] : false,
|
|
185
|
+
output: false,
|
|
186
|
+
// TypeBox schemas are structurally JSON objects, but the upstream contract
|
|
187
|
+
// exposes a generic record.
|
|
188
|
+
outputSchema: WORKFLOW_COMPLETION_PARAMETERS as unknown as Record<
|
|
189
|
+
string,
|
|
190
|
+
unknown
|
|
191
|
+
>,
|
|
192
|
+
agentContract: { version: 1 },
|
|
193
|
+
artifacts: subagent.artifacts,
|
|
194
|
+
...(subagent.model ? { model: subagent.model } : {}),
|
|
195
|
+
...(subagent.turnBudget
|
|
196
|
+
? { turnBudget: structuredClone(subagent.turnBudget) }
|
|
197
|
+
: {}),
|
|
198
|
+
...(subagent.toolBudget
|
|
199
|
+
? {
|
|
200
|
+
toolBudget: {
|
|
201
|
+
hard: subagent.toolBudget.hard,
|
|
202
|
+
...(subagent.toolBudget.soft === undefined
|
|
203
|
+
? {}
|
|
204
|
+
: { soft: subagent.toolBudget.soft }),
|
|
205
|
+
...(subagent.toolBudget.block === undefined
|
|
206
|
+
? {}
|
|
207
|
+
: {
|
|
208
|
+
block:
|
|
209
|
+
subagent.toolBudget.block === '*'
|
|
210
|
+
? '*'
|
|
211
|
+
: [...subagent.toolBudget.block],
|
|
212
|
+
}),
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
: {}),
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
return { kind: 'ready', active, request };
|
|
219
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { parseDelegatedStepResult } from '../integrations/subagents/protocol.ts';
|
|
2
|
+
import type {
|
|
3
|
+
ChildStepPolicy,
|
|
4
|
+
SubagentDelegationResponse,
|
|
5
|
+
} from '../integrations/subagents/protocol.ts';
|
|
6
|
+
import type { WorkflowStepResult } from '../runtime/step-result.ts';
|
|
7
|
+
import type { ActiveDelegation, DelegationFailureDetails } from './types.ts';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Verifies that transcript completion data exactly matches a recovered result.
|
|
11
|
+
*
|
|
12
|
+
* @param diagnostic - Correlated tool-failure diagnostic.
|
|
13
|
+
* @param result - Result recovered from the delegated artifact.
|
|
14
|
+
* @param policy - Child policy used to validate the result.
|
|
15
|
+
* @returns Whether the transcript and recovered artifact agree.
|
|
16
|
+
*/
|
|
17
|
+
export function completionMatchesResult(
|
|
18
|
+
diagnostic: NonNullable<DelegationFailureDetails['diagnostic']>,
|
|
19
|
+
result: WorkflowStepResult,
|
|
20
|
+
policy: ChildStepPolicy,
|
|
21
|
+
): boolean {
|
|
22
|
+
const value = diagnostic.completionValue;
|
|
23
|
+
if (!value) return false;
|
|
24
|
+
const expectedKeys = [
|
|
25
|
+
'outcome',
|
|
26
|
+
'summary',
|
|
27
|
+
...(result.artifact === undefined ? [] : ['artifact']),
|
|
28
|
+
].sort();
|
|
29
|
+
const actualKeys = Object.keys(value).sort();
|
|
30
|
+
if (
|
|
31
|
+
actualKeys.length !== expectedKeys.length ||
|
|
32
|
+
!actualKeys.every((key, index) => key === expectedKeys[index])
|
|
33
|
+
) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const completion = parseDelegatedStepResult(
|
|
38
|
+
{
|
|
39
|
+
...value,
|
|
40
|
+
version: 1,
|
|
41
|
+
policyDigest: policy.policyDigest,
|
|
42
|
+
},
|
|
43
|
+
policy,
|
|
44
|
+
);
|
|
45
|
+
return (
|
|
46
|
+
completion.outcome === result.outcome &&
|
|
47
|
+
completion.summary === result.summary &&
|
|
48
|
+
completion.artifact === result.artifact
|
|
49
|
+
);
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Finds contradictory or unsafe fields in a recovered terminal projection.
|
|
57
|
+
*
|
|
58
|
+
* @param active - Active delegation identity.
|
|
59
|
+
* @param response - Terminal response received from the child.
|
|
60
|
+
* @param diagnostic - Transcript evidence correlated to the failure.
|
|
61
|
+
* @returns A rejection reason, or undefined for a valid projection.
|
|
62
|
+
*/
|
|
63
|
+
export function recoveredProjectionError(
|
|
64
|
+
active: ActiveDelegation,
|
|
65
|
+
response: SubagentDelegationResponse,
|
|
66
|
+
diagnostic: NonNullable<DelegationFailureDetails['diagnostic']>,
|
|
67
|
+
): string | undefined {
|
|
68
|
+
if (response.agent !== active.agent) {
|
|
69
|
+
return `terminal agent identity is ${JSON.stringify(response.agent)}; expected ${JSON.stringify(active.agent)}`;
|
|
70
|
+
}
|
|
71
|
+
if (response.childIndex !== 0) {
|
|
72
|
+
return `terminal child index is ${JSON.stringify(response.childIndex)}; expected 0`;
|
|
73
|
+
}
|
|
74
|
+
if (
|
|
75
|
+
typeof response.exitCode !== 'number' ||
|
|
76
|
+
!Number.isSafeInteger(response.exitCode) ||
|
|
77
|
+
response.exitCode <= 0
|
|
78
|
+
) {
|
|
79
|
+
return `terminal exit code is ${JSON.stringify(response.exitCode)}; expected a positive safe integer`;
|
|
80
|
+
}
|
|
81
|
+
const execution = response.execution;
|
|
82
|
+
if (!execution) return 'terminal response has no execution projection';
|
|
83
|
+
if (execution.status !== 'failed' || execution.success) {
|
|
84
|
+
return `execution projection is ${JSON.stringify({
|
|
85
|
+
status: execution.status,
|
|
86
|
+
success: execution.success,
|
|
87
|
+
})}; expected failed/false`;
|
|
88
|
+
}
|
|
89
|
+
if (execution.exitCode !== response.exitCode) {
|
|
90
|
+
return `execution exit code ${JSON.stringify(execution.exitCode)} does not match terminal exit code ${JSON.stringify(response.exitCode)}`;
|
|
91
|
+
}
|
|
92
|
+
if (
|
|
93
|
+
typeof response.error !== 'string' ||
|
|
94
|
+
!response.error ||
|
|
95
|
+
typeof execution.error !== 'string' ||
|
|
96
|
+
execution.error !== response.error
|
|
97
|
+
) {
|
|
98
|
+
return 'terminal and execution errors are missing or do not match exactly';
|
|
99
|
+
}
|
|
100
|
+
const warnings = response.warnings as unknown;
|
|
101
|
+
if (
|
|
102
|
+
warnings !== undefined &&
|
|
103
|
+
(!Array.isArray(warnings) ||
|
|
104
|
+
warnings.some(
|
|
105
|
+
(warning) => typeof warning !== 'string' || warning.trim().length > 0,
|
|
106
|
+
))
|
|
107
|
+
) {
|
|
108
|
+
return `terminal response contains warning evidence: ${JSON.stringify(warnings)}`;
|
|
109
|
+
}
|
|
110
|
+
if (
|
|
111
|
+
diagnostic.transcriptToolCount !== undefined &&
|
|
112
|
+
response.toolCount !== undefined &&
|
|
113
|
+
response.toolCount !== diagnostic.transcriptToolCount
|
|
114
|
+
) {
|
|
115
|
+
return `terminal tool count ${response.toolCount} does not match transcript tool count ${diagnostic.transcriptToolCount}`;
|
|
116
|
+
}
|
|
117
|
+
if (
|
|
118
|
+
diagnostic.transcriptTurnCount !== undefined &&
|
|
119
|
+
response.turns !== undefined &&
|
|
120
|
+
response.turns !== diagnostic.transcriptTurnCount
|
|
121
|
+
) {
|
|
122
|
+
return `terminal turn count ${response.turns} does not match transcript turn count ${diagnostic.transcriptTurnCount}`;
|
|
123
|
+
}
|
|
124
|
+
const toolFailure = response.error.match(
|
|
125
|
+
/^\s*([a-z][\w-]*) failed\s*\(exit\s+(\d+)\)\s*:/i,
|
|
126
|
+
);
|
|
127
|
+
if (!toolFailure) {
|
|
128
|
+
return 'terminal error is not a recognized "<tool> failed (exit N): <detail>" failure';
|
|
129
|
+
}
|
|
130
|
+
const terminalTool = toolFailure[1];
|
|
131
|
+
const terminalExitCodeText = toolFailure[2];
|
|
132
|
+
if (!terminalTool || !terminalExitCodeText) {
|
|
133
|
+
return 'terminal error does not contain a complete tool failure projection';
|
|
134
|
+
}
|
|
135
|
+
const terminalExitCode = Number(terminalExitCodeText);
|
|
136
|
+
if (
|
|
137
|
+
terminalTool.toLowerCase() !== diagnostic.tool.toLowerCase() ||
|
|
138
|
+
terminalExitCode !== response.exitCode
|
|
139
|
+
) {
|
|
140
|
+
return `terminal tool/exit ${JSON.stringify({
|
|
141
|
+
tool: terminalTool,
|
|
142
|
+
exitCode: terminalExitCode,
|
|
143
|
+
})} does not match the correlated failure ${JSON.stringify({
|
|
144
|
+
tool: diagnostic.tool,
|
|
145
|
+
exitCode: response.exitCode,
|
|
146
|
+
})}`;
|
|
147
|
+
}
|
|
148
|
+
const unsafeFlag = (
|
|
149
|
+
[
|
|
150
|
+
['interrupted', execution.interrupted],
|
|
151
|
+
['timedOut', execution.timedOut],
|
|
152
|
+
['stopped', execution.stopped],
|
|
153
|
+
['detached', execution.detached],
|
|
154
|
+
] as const
|
|
155
|
+
).find(([, isEnabled]) => isEnabled === true)?.[0];
|
|
156
|
+
return unsafeFlag
|
|
157
|
+
? `execution projection reports ${unsafeFlag}=true`
|
|
158
|
+
: undefined;
|
|
159
|
+
}
|