@wichayutdew/pi-workflows 2.5.1 → 2.7.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.
Files changed (103) hide show
  1. package/README.md +34 -1
  2. package/dist/index.js +1315 -2467
  3. package/examples/starter-kit/agents/planner.md +11 -0
  4. package/examples/starter-kit/agents/reviewer.md +10 -0
  5. package/examples/starter-kit/agents/scout.md +10 -0
  6. package/examples/starter-kit/agents/worker.md +11 -0
  7. package/examples/starter-kit/agents/workspace-preparer.md +10 -0
  8. package/examples/starter-kit/investigate.workflow.yaml +84 -0
  9. package/examples/starter-kit/jira.workflow.yaml +75 -0
  10. package/examples/starter-kit/mr-comment.workflow.yaml +86 -93
  11. package/examples/starter-kit/mr-review.workflow.yaml +52 -88
  12. package/examples/starter-kit/settings.yaml +4 -0
  13. package/examples/starter-kit/steps/investigate/investigate.md +40 -0
  14. package/examples/starter-kit/steps/investigate/retrieve.md +28 -0
  15. package/examples/starter-kit/steps/investigate/validate.md +20 -0
  16. package/examples/starter-kit/steps/jira/create.md +25 -0
  17. package/examples/starter-kit/steps/jira/draft.md +18 -0
  18. package/examples/starter-kit/steps/jira/plan.md +30 -0
  19. package/examples/starter-kit/steps/mr-comment/checkout-source.md +14 -0
  20. package/examples/starter-kit/steps/mr-comment/fetch.md +11 -28
  21. package/examples/starter-kit/steps/mr-comment/implement.md +9 -26
  22. package/examples/starter-kit/steps/mr-comment/plan.md +46 -55
  23. package/examples/starter-kit/steps/mr-comment/publish.md +11 -31
  24. package/examples/starter-kit/steps/mr-comment/verify.md +7 -34
  25. package/examples/starter-kit/steps/mr-review/fetch.md +13 -21
  26. package/examples/starter-kit/steps/mr-review/publish-approved.md +18 -0
  27. package/examples/starter-kit/steps/mr-review/review-for-approval.md +53 -0
  28. package/examples/starter-kit/steps/mr-review/verify-published.md +16 -0
  29. package/examples/starter-kit/steps/shared/prepare-workspace.md +11 -89
  30. package/examples/starter-kit/steps/shared/publish-remote.md +16 -0
  31. package/examples/starter-kit/steps/ticket/implement.md +10 -23
  32. package/examples/starter-kit/steps/ticket/plan.md +49 -98
  33. package/examples/starter-kit/steps/ticket/verify.md +14 -65
  34. package/examples/starter-kit/steps/work/implement.md +11 -22
  35. package/examples/starter-kit/steps/work/plan.md +43 -59
  36. package/examples/starter-kit/steps/work/verify.md +14 -25
  37. package/examples/starter-kit/ticket.workflow.yaml +57 -61
  38. package/examples/starter-kit/work.workflow.yaml +49 -57
  39. package/package.json +9 -19
  40. package/schemas/workflow.schema.json +63 -15
  41. package/scripts/patch-herdr-agent-state.mjs +36 -0
  42. package/src/agents/profile.ts +98 -0
  43. package/src/config/ceiling.ts +0 -82
  44. package/src/config/types.ts +15 -53
  45. package/src/config/validation/settings.ts +2 -14
  46. package/src/config/validation/step.ts +129 -8
  47. package/src/config/validation/workflow.ts +1 -10
  48. package/src/engine/run-workflow-validation.ts +0 -3
  49. package/src/engine/state-types.ts +1 -1
  50. package/src/harness/action-context.ts +1 -13
  51. package/src/harness/artifact-contract.ts +46 -0
  52. package/src/harness/delegation-control-actions.ts +4 -63
  53. package/src/harness/delegation-plan.ts +26 -80
  54. package/src/harness/delegation-response-actions.ts +22 -135
  55. package/src/harness/dependencies.ts +1 -12
  56. package/src/harness/gate-submission-action.ts +28 -0
  57. package/src/harness/status-actions.ts +20 -0
  58. package/src/harness/step-execution-actions.ts +2 -9
  59. package/src/harness/types.ts +2 -40
  60. package/src/harness.ts +2 -17
  61. package/src/herdr-workflow-state.ts +65 -0
  62. package/src/index.ts +4 -9
  63. package/src/integrations/subagents/child-policy-validation.ts +6 -7
  64. package/src/integrations/subagents/child-runtime-dependencies.ts +1 -1
  65. package/src/integrations/subagents/child-runtime-policy.ts +1 -7
  66. package/src/integrations/subagents/child-runtime.ts +18 -9
  67. package/src/integrations/subagents/client.ts +240 -98
  68. package/src/integrations/subagents/protocol-events.ts +32 -15
  69. package/src/integrations/subagents/protocol.ts +1 -1
  70. package/src/preflight.ts +0 -8
  71. package/src/prompt/main-workflow-notice.ts +8 -15
  72. package/src/prompt/step-task.ts +7 -6
  73. package/src/workflow-status/format-status.ts +5 -1
  74. package/src/workflow-status/render-step-detail.ts +94 -0
  75. package/src/workflow-status/types.ts +1 -0
  76. package/src/workflow-status/view.ts +49 -14
  77. package/agents/step.md +0 -32
  78. package/examples/mr-comments.workflow.yaml +0 -125
  79. package/examples/prompts/mr-comments/implement.md +0 -17
  80. package/examples/prompts/mr-comments/inspect.md +0 -5
  81. package/examples/prompts/mr-comments/plan.md +0 -54
  82. package/examples/prompts/mr-comments/verify.md +0 -9
  83. package/examples/settings.yaml +0 -27
  84. package/examples/starter-kit/steps/mr-review/publish.md +0 -48
  85. package/examples/starter-kit/steps/mr-review/review.md +0 -76
  86. package/examples/starter-kit/steps/mr-review/verify.md +0 -35
  87. package/src/config/validation/subagent.ts +0 -288
  88. package/src/harness/delegation-failure.ts +0 -248
  89. package/src/harness/delegation-recovery-validation.ts +0 -161
  90. package/src/harness/delegation-retry-policy.ts +0 -120
  91. package/src/integrations/subagents/client-delegation.ts +0 -181
  92. package/src/integrations/subagents/client-messages.ts +0 -66
  93. package/src/integrations/subagents/client-types.ts +0 -36
  94. package/src/integrations/subagents/diagnostic-format.ts +0 -45
  95. package/src/integrations/subagents/diagnostic-text.ts +0 -114
  96. package/src/integrations/subagents/diagnostic-types.ts +0 -83
  97. package/src/integrations/subagents/diagnostics.ts +0 -26
  98. package/src/integrations/subagents/failure-correlation.ts +0 -285
  99. package/src/integrations/subagents/failure-transcript.ts +0 -252
  100. package/src/integrations/subagents/hidden-bash-failure.ts +0 -98
  101. package/src/integrations/subagents/replay-audit.ts +0 -146
  102. package/src/integrations/subagents/replay-safety.ts +0 -67
  103. package/src/integrations/subagents/session-diagnostics.ts +0 -357
@@ -0,0 +1,46 @@
1
+ import type { ArtifactContract } from '../config/types.ts';
2
+
3
+ function countOccurrences(value: string, substring: string): number {
4
+ let count = 0;
5
+ let offset = 0;
6
+ while (true) {
7
+ const match = value.indexOf(substring, offset);
8
+ if (match === -1) return count;
9
+ count += 1;
10
+ offset = match + substring.length;
11
+ }
12
+ }
13
+
14
+ export function validateArtifactContract(
15
+ artifact: string,
16
+ contract: ArtifactContract | undefined,
17
+ ): string | undefined {
18
+ if (!contract) return undefined;
19
+ if (artifact.length > contract.maxChars) {
20
+ return `gate artifact exceeds ${contract.maxChars} characters`;
21
+ }
22
+ const required = contract.requiredSubstrings.find(
23
+ (substring) => !artifact.includes(substring),
24
+ );
25
+ if (required) {
26
+ return `gate artifact is missing required text: ${JSON.stringify(required)}`;
27
+ }
28
+ const forbidden = contract.forbiddenSubstrings.find((substring) =>
29
+ artifact.includes(substring),
30
+ );
31
+ if (forbidden) {
32
+ return `gate artifact contains forbidden text: ${JSON.stringify(forbidden)}`;
33
+ }
34
+ for (const group of contract.equalOccurrenceGroups) {
35
+ const counts = group.map((substring) =>
36
+ countOccurrences(artifact, substring),
37
+ );
38
+ if (counts.some((count) => count === 0)) {
39
+ return `gate artifact is missing required repeated text: ${JSON.stringify(group)}`;
40
+ }
41
+ if (!counts.every((count) => count === counts[0])) {
42
+ return `gate artifact has unequal repeated text counts: ${JSON.stringify(group)}`;
43
+ }
44
+ }
45
+ return undefined;
46
+ }
@@ -1,33 +1,28 @@
1
1
  import { failRun } from '../engine/transitions.ts';
2
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
3
  import {
8
4
  conciseStepFailureSummary,
9
5
  reportFailedStep,
10
6
  } from './step-reporting.ts';
11
- import type { ActiveDelegation, DelegationFailureDetails } from './types.ts';
7
+ import type { ActiveDelegation } from './types.ts';
8
+
9
+ const boundedFailureField = (value: string): string =>
10
+ value.length <= 500 ? value : `${value.slice(0, 499)}…`;
12
11
 
13
12
  type HarnessActionContext = Pick<
14
13
  FullHarnessActionContext,
15
14
  | 'activeDelegation'
16
15
  | 'catalog'
17
16
  | 'cleanupDelegation'
18
- | 'delegationFailures'
19
17
  | 'dependencies'
20
- | 'isSessionActive'
21
18
  | 'isolateMainSessionTools'
22
19
  | 'latestContext'
23
- | 'launchCurrentStep'
24
20
  | 'mainSteps'
25
21
  | 'pauseForExecutionFailure'
26
22
  | 'persist'
27
23
  | 'pi'
28
24
  | 'restoreBaselineTools'
29
25
  | 'run'
30
- | 'sessionEpoch'
31
26
  | 'subagents'
32
27
  | 'updateStatus'
33
28
  >;
@@ -41,12 +36,6 @@ export type DelegationControlActions = {
41
36
  this: HarnessActionContext,
42
37
  active: ActiveDelegation,
43
38
  ) => Promise<void>;
44
- retryDelegationAfterFailure: (
45
- this: HarnessActionContext,
46
- active: ActiveDelegation,
47
- failure: DelegationFailureDetails | undefined,
48
- reason: string,
49
- ) => boolean;
50
39
  pauseForDelegationFailure: (
51
40
  this: HarnessActionContext,
52
41
  reason: string,
@@ -119,53 +108,6 @@ async function cleanupDelegation(
119
108
  }
120
109
  }
121
110
 
122
- function retryDelegationAfterFailure(
123
- this: HarnessActionContext,
124
- active: ActiveDelegation,
125
- failure: DelegationFailureDetails | undefined,
126
- reason: string,
127
- ): boolean {
128
- if (!failure) return false;
129
- const fingerprint =
130
- this.delegationFailures.delegationFailureFingerprint(failure);
131
- const isRepeatedFailure = active.recoveryFailures.some(
132
- (previousFailure) => previousFailure.fingerprint === fingerprint,
133
- );
134
- if (
135
- active.recoveryAttemptCount >= MAX_DELEGATION_RECOVERY_ATTEMPTS ||
136
- isRepeatedFailure ||
137
- !this.delegationFailures.isRetryableTerminalFailure(failure) ||
138
- !this.delegationFailures.isSafeToRetryDelegation(
139
- active.policy,
140
- active.broadRecoveryAuthorized,
141
- failure.replayAudit,
142
- ) ||
143
- !this.isSessionActive ||
144
- this.sessionEpoch !== active.sessionEpoch ||
145
- !this.run ||
146
- this.run.status !== 'running' ||
147
- this.run.runId !== active.runId ||
148
- this.run.currentStepId !== active.stepId ||
149
- this.run.currentStepDigest !== active.stepDigest ||
150
- this.activeDelegation
151
- ) {
152
- return false;
153
- }
154
- const workflow = this.catalog.workflows.get(this.run.workflowId);
155
- if (!workflow) return false;
156
-
157
- const attempt = active.recoveryAttemptCount + 1;
158
- this.latestContext?.ui.notify(
159
- `Automatic recovery for "${active.stepId}" after a subagent failure (${attempt}/${MAX_DELEGATION_RECOVERY_ATTEMPTS})`,
160
- 'warning',
161
- );
162
- this.launchCurrentStep(workflow, {
163
- attempt,
164
- failures: [...active.recoveryFailures, { fingerprint, reason }],
165
- });
166
- return true;
167
- }
168
-
169
111
  function pauseForDelegationFailure(
170
112
  this: HarnessActionContext,
171
113
  reason: string,
@@ -266,7 +208,6 @@ export function createDelegationControlActions(): DelegationControlActions {
266
208
  return {
267
209
  cancelActiveDelegation,
268
210
  cleanupDelegation,
269
- retryDelegationAfterFailure,
270
211
  pauseForDelegationFailure,
271
212
  pauseForExecutionFailure,
272
213
  retainUnconfirmedDelegation,
@@ -1,19 +1,17 @@
1
1
  import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
2
+ import { loadAgentProfile } from '../agents/profile.ts';
2
3
  import type { LoadedWorkflow, WorkflowStep } from '../config/types.ts';
3
4
  import type { WorkflowRun } from '../engine/state.ts';
4
5
  import { allowedOutcomes } from '../engine/transitions.ts';
5
6
  import { digest } from '../digest.ts';
6
- import { deriveSubagentSessionRoot } from '../integrations/subagents/diagnostics.ts';
7
7
  import {
8
8
  encodeChildPolicy,
9
9
  type ChildStepPolicy,
10
10
  type SubagentDelegationRequest,
11
11
  } from '../integrations/subagents/protocol.ts';
12
- import { automaticRecoveryTask, buildDelegatedStepTask } from '../prompt.ts';
13
- import { WORKFLOW_COMPLETION_PARAMETERS } from '../runtime/completion-tool.ts';
14
- import { MAX_DELEGATION_RECOVERY_ATTEMPTS } from './delegation-retry-policy.ts';
12
+ import { buildDelegatedStepTask } from '../prompt.ts';
15
13
  import type { WorkflowHarnessDependencies } from './dependencies.ts';
16
- import type { ActiveDelegation, DelegationRecovery } from './types.ts';
14
+ import type { ActiveDelegation } from './types.ts';
17
15
 
18
16
  type DelegationPlanInput = {
19
17
  workflow: LoadedWorkflow;
@@ -21,7 +19,6 @@ type DelegationPlanInput = {
21
19
  step: WorkflowStep;
22
20
  sessionEpoch: number;
23
21
  latestContext: ExtensionContext | undefined;
24
- recovery: DelegationRecovery | undefined;
25
22
  };
26
23
 
27
24
  export type DelegationPlan =
@@ -35,13 +32,6 @@ export type DelegationPlan =
35
32
  request: SubagentDelegationRequest;
36
33
  };
37
34
 
38
- function delegationTranscriptBinding(
39
- requestId: string,
40
- policyDigest: string,
41
- ): string {
42
- return `<pi-workflows-delegation-binding-v1>${requestId}:${policyDigest}</pi-workflows-delegation-binding-v1>`;
43
- }
44
-
45
35
  /**
46
36
  * Creates one immutable subagent request from workflow state and injected
47
37
  * identity/workspace effects.
@@ -55,12 +45,21 @@ export function createDelegationPlan(
55
45
  | 'resolveWorkspaceDirectory'
56
46
  >,
57
47
  ): DelegationPlan {
58
- const { workflow, run, step, latestContext, recovery } = input;
59
- const subagent = step.subagent;
60
- if (!subagent) {
48
+ const { workflow, run, step, latestContext } = input;
49
+ const agent = step.agent?.name;
50
+ if (!agent) {
51
+ return {
52
+ kind: 'invalid',
53
+ reason: `Step "${run.currentStepId}" has no agent profile`,
54
+ };
55
+ }
56
+ let agentProfile;
57
+ try {
58
+ agentProfile = loadAgentProfile(agent);
59
+ } catch (error) {
61
60
  return {
62
61
  kind: 'invalid',
63
- reason: `Step "${run.currentStepId}" has no subagent configuration`,
62
+ reason: error instanceof Error ? error.message : String(error),
64
63
  };
65
64
  }
66
65
  if (!run.cwd) {
@@ -160,7 +159,7 @@ export function createDelegationPlan(
160
159
  const policyDigest = digest({
161
160
  version: 1,
162
161
  requestId,
163
- agent: subagent.agent,
162
+ agent,
164
163
  runId: run.runId,
165
164
  stepId: run.currentStepId,
166
165
  stepDigest: run.currentStepDigest,
@@ -174,7 +173,7 @@ export function createDelegationPlan(
174
173
  const policy: ChildStepPolicy = {
175
174
  version: 1,
176
175
  requestId,
177
- agent: subagent.agent,
176
+ agent,
178
177
  workflowId: workflow.definition.id,
179
178
  runId: run.runId,
180
179
  stepId: run.currentStepId,
@@ -195,22 +194,7 @@ export function createDelegationPlan(
195
194
  ...(step.gate ? { gateSubmitOutcome: step.gate.submitOutcome } : {}),
196
195
  ...(step.workspace ? { workspace: structuredClone(step.workspace) } : {}),
197
196
  };
198
- const trustedSessionRoot = deriveSubagentSessionRoot(
199
- latestContext.sessionManager.getSessionFile(),
200
- );
201
- const transcriptTask = [
202
- buildDelegatedStepTask(workflow, run, ''),
203
- delegationTranscriptBinding(requestId, policyDigest),
204
- ...(recovery
205
- ? [
206
- automaticRecoveryTask(
207
- recovery.failures.map(({ reason }) => reason),
208
- recovery.attempt,
209
- MAX_DELEGATION_RECOVERY_ATTEMPTS,
210
- ),
211
- ]
212
- : []),
213
- ].join('\n\n');
197
+ const task = buildDelegatedStepTask(workflow, run, '');
214
198
  const active: ActiveDelegation = {
215
199
  requestId,
216
200
  runId: run.runId,
@@ -219,56 +203,18 @@ export function createDelegationPlan(
219
203
  sessionEpoch: input.sessionEpoch,
220
204
  resultDirectory: workspace.resultDirectory,
221
205
  policy,
222
- transcriptTask,
223
- agent: subagent.agent,
224
- ...(trustedSessionRoot ? { trustedSessionRoot } : {}),
225
- broadRecoveryAuthorized: subagent.retryToolFailures,
226
- recoveryAttemptCount: recovery?.attempt ?? 0,
227
- recoveryFailures: recovery?.failures ?? [],
206
+ transcriptTask: task,
207
+ agent,
228
208
  };
229
209
  const request: SubagentDelegationRequest = {
230
210
  version: 1,
231
211
  requestId,
232
- agent: subagent.agent,
233
- task: `${encodeChildPolicy(policy)}\n\n${transcriptTask}`,
234
- // Workflow steps are isolation boundaries. Parent/sibling transcripts are
235
- // replaced by the explicit workflow handoff.
236
- context: 'fresh',
212
+ agent,
213
+ task: `${encodeChildPolicy(policy)}\n\n${task}`,
237
214
  cwd: delegationCwd,
238
- timeoutMs: subagent.timeoutMs,
239
- skill:
240
- step.permissions.skills.length > 0 ? [...step.permissions.skills] : false,
241
- output: false,
242
- // TypeBox schemas are structurally JSON objects, but the upstream contract
243
- // exposes a generic record.
244
- outputSchema: WORKFLOW_COMPLETION_PARAMETERS as unknown as Record<
245
- string,
246
- unknown
247
- >,
248
- agentContract: { version: 1 },
249
- artifacts: subagent.artifacts,
250
- ...(subagent.model ? { model: subagent.model } : {}),
251
- ...(subagent.turnBudget
252
- ? { turnBudget: structuredClone(subagent.turnBudget) }
253
- : {}),
254
- ...(subagent.toolBudget
255
- ? {
256
- toolBudget: {
257
- hard: subagent.toolBudget.hard,
258
- ...(subagent.toolBudget.soft === undefined
259
- ? {}
260
- : { soft: subagent.toolBudget.soft }),
261
- ...(subagent.toolBudget.block === undefined
262
- ? {}
263
- : {
264
- block:
265
- subagent.toolBudget.block === '*'
266
- ? '*'
267
- : [...subagent.toolBudget.block],
268
- }),
269
- },
270
- }
271
- : {}),
215
+ timeoutMs: 900_000,
216
+ ...(agentProfile.model ? { model: agentProfile.model } : {}),
217
+ ...(agentProfile.thinking ? { thinking: agentProfile.thinking } : {}),
272
218
  };
273
219
 
274
220
  return { kind: 'ready', active, request };
@@ -4,13 +4,10 @@ import {
4
4
  type SubagentDelegationUpdate,
5
5
  } from '../integrations/subagents/protocol.ts';
6
6
  import { advanceRun } from '../engine/transitions.ts';
7
- import {
8
- attachSubagentTranscript,
9
- recordCurrentStepResult,
10
- } from '../engine/step-trace.ts';
7
+ import { recordCurrentStepResult } from '../engine/step-trace.ts';
11
8
  import type { WorkflowStepResult } from '../runtime/step-result.ts';
12
9
  import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
13
- import type { ActiveDelegation, DelegationFailureDetails } from './types.ts';
10
+ import type { ActiveDelegation } from './types.ts';
14
11
  import { resolveStepEffects } from './step-effects.ts';
15
12
 
16
13
  type HarnessActionContext = Pick<
@@ -18,7 +15,6 @@ type HarnessActionContext = Pick<
18
15
  | 'activeDelegation'
19
16
  | 'catalog'
20
17
  | 'cleanupDelegation'
21
- | 'delegationFailures'
22
18
  | 'dependencies'
23
19
  | 'finishDelegation'
24
20
  | 'isSessionActive'
@@ -27,7 +23,6 @@ type HarnessActionContext = Pick<
27
23
  | 'pauseForDelegationFailure'
28
24
  | 'releaseMainAfterCancellation'
29
25
  | 'retainUnconfirmedDelegation'
30
- | 'retryDelegationAfterFailure'
31
26
  | 'run'
32
27
  | 'sessionEpoch'
33
28
  | 'settleAfterTransition'
@@ -70,11 +65,22 @@ function handleDelegationUpdate(
70
65
  ): void {
71
66
  if (this.activeDelegation !== active) return;
72
67
  const progress = [
68
+ update.activity,
73
69
  update.currentTool ? `tool ${update.currentTool}` : undefined,
74
70
  update.toolCount !== undefined ? `${update.toolCount} calls` : undefined,
75
71
  update.tokens !== undefined ? `${update.tokens} tokens` : undefined,
76
72
  ].filter((part): part is string => part !== undefined);
77
73
  active.progress = progress.join(', ') || 'running';
74
+ if (update.detail) {
75
+ const previous = active.activityLog ?? [];
76
+ const replacesPreviousResponse =
77
+ update.detail.startsWith('response: ') &&
78
+ previous.at(-1)?.startsWith('response: ');
79
+ active.activityLog = [
80
+ ...(replacesPreviousResponse ? previous.slice(0, -1) : previous),
81
+ update.detail,
82
+ ].slice(-8);
83
+ }
78
84
  this.updateStatus();
79
85
  }
80
86
 
@@ -128,7 +134,6 @@ async function finishDelegation(
128
134
  return;
129
135
  }
130
136
  this.activeDelegation = undefined;
131
- let terminalFailure: DelegationFailureDetails | undefined;
132
137
  let cleanupAttempted = false;
133
138
 
134
139
  try {
@@ -144,80 +149,15 @@ async function finishDelegation(
144
149
  return;
145
150
  }
146
151
  const terminalAt = this.dependencies.now();
147
- if (
148
- response.requestId === active.requestId &&
149
- (response.agent === undefined || response.agent === active.agent) &&
150
- response.sessionFile &&
151
- active.trustedSessionRoot &&
152
- typeof response.runId === 'string' &&
153
- response.runId &&
154
- response.childIndex === 0
155
- ) {
156
- try {
157
- this.run = attachSubagentTranscript(
158
- this.run,
159
- active.requestId,
160
- {
161
- trustedRoot: active.trustedSessionRoot,
162
- sessionFile: response.sessionFile,
163
- runId: response.runId,
164
- childIndex: response.childIndex,
165
- },
166
- terminalAt,
167
- );
168
- } catch {
169
- // Status evidence is best-effort and cannot alter terminal handling.
170
- }
171
- }
172
152
  const workflow = this.catalog.workflows.get(this.run.workflowId);
173
153
  const step = workflow?.definition.steps[this.run.currentStepId];
174
154
  if (!workflow || !step) {
175
155
  throw new Error('Active workflow configuration is unavailable');
176
156
  }
177
- if (response.status === 'completed') {
178
- const transcriptAudit =
179
- await this.delegationFailures.completedResponseAudit(active, response);
180
- if (!transcriptAudit.verified) {
181
- throw new Error(
182
- `Subagent "${active.agent}" completed without a verifiable terminal transcript: ${transcriptAudit.reason}`,
183
- );
184
- }
185
- if (transcriptAudit.warning) {
186
- throw new Error(
187
- `Subagent "${active.agent}" completed with an unresolved post-completion watchdog warning: ${transcriptAudit.warning}`,
188
- );
189
- }
190
- }
191
- let recoveredTerminalFailure: DelegationFailureDetails | undefined;
192
- if (
193
- response.status !== 'completed' ||
194
- this.delegationFailures.hasContradictoryCompletion(response)
195
- ) {
196
- const failure = await this.delegationFailures.describeDelegationFailure(
197
- active,
198
- response,
199
- );
200
- terminalFailure = failure;
201
- if (
202
- response.status !== 'failed' ||
203
- failure.diagnostic?.completionAfterFailure !== true
204
- ) {
205
- throw new Error(failure.reason);
206
- }
207
- const projectionError = this.delegationFailures.recoveredProjectionError(
208
- active,
209
- response,
210
- failure.diagnostic,
157
+ if (response.status !== 'completed') {
158
+ throw new Error(
159
+ `Workflow worker "${active.agent}" ${response.status.replaceAll('_', ' ')}${response.error ? `: ${response.error}` : ''}`,
211
160
  );
212
- if (projectionError) {
213
- throw new Error(
214
- this.delegationFailures.rejectedRecoveryReason(
215
- failure,
216
- projectionError,
217
- ),
218
- );
219
- }
220
- recoveredTerminalFailure = failure;
221
161
  }
222
162
  const requiredSkillWarning =
223
163
  step.requires.skills.length > 0
@@ -233,15 +173,6 @@ async function finishDelegation(
233
173
  try {
234
174
  serializedResult = await this.dependencies.readDelegatedResult(active);
235
175
  } catch (error) {
236
- if (recoveredTerminalFailure) {
237
- throw new Error(
238
- this.delegationFailures.rejectedRecoveryReason(
239
- recoveredTerminalFailure,
240
- error,
241
- ),
242
- { cause: error },
243
- );
244
- }
245
176
  if (hasErrorCode(error, 'ENOENT')) {
246
177
  throw new Error(
247
178
  `Subagent "${active.agent}" completed without producing the required correlated structured_output result`,
@@ -251,49 +182,12 @@ async function finishDelegation(
251
182
  throw error;
252
183
  }
253
184
 
254
- let result: WorkflowStepResult;
255
- try {
256
- const rawResult: unknown = JSON.parse(serializedResult);
257
- result = parseDelegatedStepResult(rawResult, active.policy);
258
- } catch (error) {
259
- if (recoveredTerminalFailure) {
260
- throw new Error(
261
- this.delegationFailures.rejectedRecoveryReason(
262
- recoveredTerminalFailure,
263
- error,
264
- ),
265
- { cause: error },
266
- );
267
- }
268
- throw error;
269
- }
270
- if (
271
- recoveredTerminalFailure?.diagnostic &&
272
- !this.delegationFailures.completionMatchesResult(
273
- recoveredTerminalFailure.diagnostic,
274
- result,
275
- active.policy,
276
- )
277
- ) {
278
- throw new Error(
279
- this.delegationFailures.rejectedRecoveryReason(
280
- recoveredTerminalFailure,
281
- 'structured_output transcript value does not match the correlated result',
282
- ),
283
- );
284
- }
185
+ const rawResult: unknown = JSON.parse(serializedResult);
186
+ const result: WorkflowStepResult = parseDelegatedStepResult(
187
+ rawResult,
188
+ active.policy,
189
+ );
285
190
  const acceptedAt = terminalAt;
286
- if (recoveredTerminalFailure) {
287
- const isFalsePositive =
288
- recoveredTerminalFailure.diagnostic?.correlation ===
289
- 'successful-output-before-completion';
290
- this.latestContext?.ui.notify(
291
- isFalsePositive
292
- ? `Accepted "${active.stepId}" because the trusted child transcript proved the terminal tool error was a false positive and produced a matching structured result`
293
- : `Accepted "${active.stepId}" because the child resolved an earlier tool failure and produced a valid structured result`,
294
- 'warning',
295
- );
296
- }
297
191
  if (step.gate?.submitOutcome === result.outcome) {
298
192
  this.run = recordCurrentStepResult(this.run, result, acceptedAt);
299
193
  await this.submitGate(
@@ -335,14 +229,7 @@ async function finishDelegation(
335
229
  const reason = error instanceof Error ? error.message : String(error);
336
230
  cleanupAttempted = true;
337
231
  await this.cleanupDelegation(active);
338
- if (!this.retryDelegationAfterFailure(active, terminalFailure, reason)) {
339
- const failureSummary = terminalFailure?.error
340
- ? `Subagent ${terminalFailure.status.replaceAll('_', ' ')}: ${terminalFailure.error}`
341
- : terminalFailure
342
- ? `Subagent ${terminalFailure.status.replaceAll('_', ' ')}`
343
- : reason;
344
- this.pauseForDelegationFailure(reason, failureSummary);
345
- }
232
+ this.pauseForDelegationFailure(reason);
346
233
  } finally {
347
234
  try {
348
235
  if (!cleanupAttempted) await this.cleanupDelegation(active);
@@ -14,11 +14,6 @@ import {
14
14
  createSubagentDelegationClient,
15
15
  type SubagentDelegationClientController,
16
16
  } from '../integrations/subagents/client.ts';
17
- import {
18
- auditCompletedDelegationTranscript,
19
- readDelegationReplayAudit,
20
- readToolFailureDiagnostic,
21
- } from '../integrations/subagents/diagnostics.ts';
22
17
  import {
23
18
  createMainStepRuntime,
24
19
  type MainStepRuntimeController,
@@ -61,9 +56,6 @@ export type WorkflowHarnessDependencies = {
61
56
  readonly requestPlannotatorReview: typeof requestPlannotatorReview;
62
57
  readonly requestPlannotatorReviewStatus: typeof requestPlannotatorReviewStatus;
63
58
  readonly requestPromptGateReview: typeof requestPromptGateReview;
64
- readonly readDelegationReplayAudit: typeof readDelegationReplayAudit;
65
- readonly readToolFailureDiagnostic: typeof readToolFailureDiagnostic;
66
- readonly auditCompletedDelegationTranscript: typeof auditCompletedDelegationTranscript;
67
59
  readonly showWorkflowStatus: typeof showWorkflowStatus;
68
60
  readonly createSubagentClient: (
69
61
  pi: ExtensionAPI,
@@ -150,11 +142,8 @@ const DEFAULT_DEPENDENCIES: WorkflowHarnessDependencies = {
150
142
  requestPlannotatorReview,
151
143
  requestPlannotatorReviewStatus,
152
144
  requestPromptGateReview,
153
- readDelegationReplayAudit,
154
- readToolFailureDiagnostic,
155
- auditCompletedDelegationTranscript,
156
145
  showWorkflowStatus,
157
- createSubagentClient: (pi) => createSubagentDelegationClient(pi.events),
146
+ createSubagentClient: () => createSubagentDelegationClient(),
158
147
  createMainStepRuntime: (pi) => createMainStepRuntime({ pi }),
159
148
  createMutationQueue: createSerialTaskQueue,
160
149
  flushUnwrittenSession,
@@ -1,7 +1,9 @@
1
1
  import type { LoadedWorkflow } from '../config/types.ts';
2
2
  import type { WorkflowRun } from '../engine/state.ts';
3
+ import { validateArtifactContract } from './artifact-contract.ts';
3
4
  import {
4
5
  attachGateReviewId,
6
+ advanceRun,
5
7
  beginGate,
6
8
  failGate,
7
9
  failRun,
@@ -19,6 +21,7 @@ type HarnessActionContext = Pick<
19
21
  | 'pi'
20
22
  | 'restoreBaselineTools'
21
23
  | 'run'
24
+ | 'settleAfterTransition'
22
25
  | 'sessionEpoch'
23
26
  | 'updateStatus'
24
27
  >;
@@ -63,6 +66,31 @@ async function submitGate(
63
66
  this.dependencies.createRequestId();
64
67
  const step = workflow.definition.steps[originalRun.currentStepId];
65
68
  if (!step?.gate) throw new Error('Current step has no gate');
69
+ const contractError = validateArtifactContract(
70
+ artifact,
71
+ step.gate.artifactContract,
72
+ );
73
+ if (contractError) {
74
+ if (step.gate.artifactContract?.onValidationFailure !== 'retry') {
75
+ throw new Error(contractError);
76
+ }
77
+ const retrySummary = `Artifact contract failed: ${contractError}`;
78
+ this.run = advanceRun(
79
+ workflow,
80
+ originalRun,
81
+ 'retry',
82
+ retrySummary,
83
+ this.dependencies.now(),
84
+ );
85
+ this.persist();
86
+ this.updateStatus();
87
+ this.settleAfterTransition(workflow, {
88
+ stepId: originalRun.currentStepId,
89
+ outcome: 'retry',
90
+ summary: retrySummary,
91
+ });
92
+ return;
93
+ }
66
94
 
67
95
  this.run = beginGate(
68
96
  workflow,
@@ -63,6 +63,7 @@ function workflowStatusSnapshot(
63
63
  agent: this.activeDelegation.agent,
64
64
  requestId: this.activeDelegation.requestId,
65
65
  progress: this.activeDelegation.progress ?? 'starting',
66
+ activityLog: this.activeDelegation.activityLog ?? [],
66
67
  };
67
68
  } else if (this.mainSteps.activeStepId) {
68
69
  execution = { kind: 'main' };
@@ -77,6 +78,25 @@ function workflowStatusSnapshot(
77
78
 
78
79
  function updateStatus(this: StatusContext): void {
79
80
  refreshStatusWhileRunning.call(this);
81
+ const run = this.run;
82
+ this.pi.events.emit('pi-workflows:state', {
83
+ state:
84
+ run?.status === 'running'
85
+ ? 'working'
86
+ : run?.status === 'awaiting-gate' || run?.status === 'paused'
87
+ ? 'blocked'
88
+ : run?.status === 'completed'
89
+ ? 'completed'
90
+ : 'interrupted',
91
+ workflowId: run?.workflowId,
92
+ stepId: run?.currentStepId,
93
+ message:
94
+ run?.status === 'completed'
95
+ ? `Workflow "${run.workflowId}" completed`
96
+ : run?.status === 'paused'
97
+ ? run.pauseReason
98
+ : undefined,
99
+ });
80
100
  if (!this.latestContext) return;
81
101
  if (this.legacyProgressWidgetContext !== this.latestContext) {
82
102
  this.latestContext.ui.setWidget(LEGACY_PROGRESS_WIDGET_KEY, undefined);