agent-relay 4.0.0 → 4.0.2

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 (140) hide show
  1. package/README.md +5 -5
  2. package/bin/agent-relay-broker-darwin-arm64 +0 -0
  3. package/bin/agent-relay-broker-darwin-x64 +0 -0
  4. package/bin/agent-relay-broker-linux-arm64 +0 -0
  5. package/bin/agent-relay-broker-linux-x64 +0 -0
  6. package/dist/index.cjs +1980 -1024
  7. package/dist/src/cli/commands/core.d.ts.map +1 -1
  8. package/dist/src/cli/commands/core.js +4 -3
  9. package/dist/src/cli/commands/core.js.map +1 -1
  10. package/dist/src/cli/commands/on/relayfile-binary.d.ts +2 -0
  11. package/dist/src/cli/commands/on/relayfile-binary.d.ts.map +1 -0
  12. package/dist/src/cli/commands/on/relayfile-binary.js +208 -0
  13. package/dist/src/cli/commands/on/relayfile-binary.js.map +1 -0
  14. package/dist/src/cli/commands/on/start.d.ts.map +1 -1
  15. package/dist/src/cli/commands/on/start.js +62 -26
  16. package/dist/src/cli/commands/on/start.js.map +1 -1
  17. package/dist/src/cli/commands/on/workspace.d.ts.map +1 -1
  18. package/dist/src/cli/commands/on/workspace.js +4 -0
  19. package/dist/src/cli/commands/on/workspace.js.map +1 -1
  20. package/dist/src/cli/commands/on.js +1 -1
  21. package/dist/src/cli/commands/on.js.map +1 -1
  22. package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
  23. package/dist/src/cli/lib/broker-lifecycle.js +15 -8
  24. package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
  25. package/dist/src/cli/lib/client-factory.d.ts +2 -2
  26. package/dist/src/cli/lib/client-factory.d.ts.map +1 -1
  27. package/dist/src/cli/lib/client-factory.js.map +1 -1
  28. package/dist/src/cost/pricing.d.ts +18 -0
  29. package/dist/src/cost/pricing.d.ts.map +1 -0
  30. package/dist/src/cost/pricing.js +111 -0
  31. package/dist/src/cost/pricing.js.map +1 -0
  32. package/dist/src/cost/tracker.d.ts +13 -0
  33. package/dist/src/cost/tracker.d.ts.map +1 -0
  34. package/dist/src/cost/tracker.js +152 -0
  35. package/dist/src/cost/tracker.js.map +1 -0
  36. package/dist/src/cost/types.d.ts +23 -0
  37. package/dist/src/cost/types.d.ts.map +1 -0
  38. package/dist/src/cost/types.js +2 -0
  39. package/dist/src/cost/types.js.map +1 -0
  40. package/package.json +10 -10
  41. package/packages/acp-bridge/package.json +2 -2
  42. package/packages/brand/package.json +1 -1
  43. package/packages/cloud/package.json +3 -3
  44. package/packages/config/package.json +1 -1
  45. package/packages/hooks/package.json +4 -4
  46. package/packages/memory/package.json +2 -2
  47. package/packages/openclaw/package.json +2 -2
  48. package/packages/policy/package.json +2 -2
  49. package/packages/sdk/dist/broker-path.d.ts +3 -2
  50. package/packages/sdk/dist/broker-path.d.ts.map +1 -1
  51. package/packages/sdk/dist/broker-path.js +119 -32
  52. package/packages/sdk/dist/broker-path.js.map +1 -1
  53. package/packages/sdk/dist/client.d.ts +12 -2
  54. package/packages/sdk/dist/client.d.ts.map +1 -1
  55. package/packages/sdk/dist/client.js +20 -1
  56. package/packages/sdk/dist/client.js.map +1 -1
  57. package/packages/sdk/dist/index.d.ts +1 -1
  58. package/packages/sdk/dist/index.d.ts.map +1 -1
  59. package/packages/sdk/dist/index.js.map +1 -1
  60. package/packages/sdk/dist/relay.d.ts +2 -1
  61. package/packages/sdk/dist/relay.d.ts.map +1 -1
  62. package/packages/sdk/dist/relay.js +1 -1
  63. package/packages/sdk/dist/relay.js.map +1 -1
  64. package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.d.ts +2 -0
  65. package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.d.ts.map +1 -0
  66. package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.js +117 -0
  67. package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.js.map +1 -0
  68. package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.js +4 -3
  69. package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.js.map +1 -1
  70. package/packages/sdk/dist/workflows/__tests__/step-executor.test.d.ts +2 -0
  71. package/packages/sdk/dist/workflows/__tests__/step-executor.test.d.ts.map +1 -0
  72. package/packages/sdk/dist/workflows/__tests__/step-executor.test.js +378 -0
  73. package/packages/sdk/dist/workflows/__tests__/step-executor.test.js.map +1 -0
  74. package/packages/sdk/dist/workflows/__tests__/template-resolver.test.d.ts +2 -0
  75. package/packages/sdk/dist/workflows/__tests__/template-resolver.test.d.ts.map +1 -0
  76. package/packages/sdk/dist/workflows/__tests__/template-resolver.test.js +145 -0
  77. package/packages/sdk/dist/workflows/__tests__/template-resolver.test.js.map +1 -0
  78. package/packages/sdk/dist/workflows/__tests__/verification.test.d.ts +2 -0
  79. package/packages/sdk/dist/workflows/__tests__/verification.test.d.ts.map +1 -0
  80. package/packages/sdk/dist/workflows/__tests__/verification.test.js +170 -0
  81. package/packages/sdk/dist/workflows/__tests__/verification.test.js.map +1 -0
  82. package/packages/sdk/dist/workflows/builder.d.ts +3 -2
  83. package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
  84. package/packages/sdk/dist/workflows/builder.js +1 -3
  85. package/packages/sdk/dist/workflows/builder.js.map +1 -1
  86. package/packages/sdk/dist/workflows/channel-messenger.d.ts +28 -0
  87. package/packages/sdk/dist/workflows/channel-messenger.d.ts.map +1 -0
  88. package/packages/sdk/dist/workflows/channel-messenger.js +255 -0
  89. package/packages/sdk/dist/workflows/channel-messenger.js.map +1 -0
  90. package/packages/sdk/dist/workflows/index.d.ts +7 -0
  91. package/packages/sdk/dist/workflows/index.d.ts.map +1 -1
  92. package/packages/sdk/dist/workflows/index.js +7 -0
  93. package/packages/sdk/dist/workflows/index.js.map +1 -1
  94. package/packages/sdk/dist/workflows/process-spawner.d.ts +35 -0
  95. package/packages/sdk/dist/workflows/process-spawner.d.ts.map +1 -0
  96. package/packages/sdk/dist/workflows/process-spawner.js +141 -0
  97. package/packages/sdk/dist/workflows/process-spawner.js.map +1 -0
  98. package/packages/sdk/dist/workflows/run.d.ts +2 -1
  99. package/packages/sdk/dist/workflows/run.d.ts.map +1 -1
  100. package/packages/sdk/dist/workflows/run.js.map +1 -1
  101. package/packages/sdk/dist/workflows/runner.d.ts +6 -6
  102. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  103. package/packages/sdk/dist/workflows/runner.js +443 -719
  104. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  105. package/packages/sdk/dist/workflows/step-executor.d.ts +95 -0
  106. package/packages/sdk/dist/workflows/step-executor.d.ts.map +1 -0
  107. package/packages/sdk/dist/workflows/step-executor.js +393 -0
  108. package/packages/sdk/dist/workflows/step-executor.js.map +1 -0
  109. package/packages/sdk/dist/workflows/template-resolver.d.ts +33 -0
  110. package/packages/sdk/dist/workflows/template-resolver.d.ts.map +1 -0
  111. package/packages/sdk/dist/workflows/template-resolver.js +144 -0
  112. package/packages/sdk/dist/workflows/template-resolver.js.map +1 -0
  113. package/packages/sdk/dist/workflows/verification.d.ts +33 -0
  114. package/packages/sdk/dist/workflows/verification.d.ts.map +1 -0
  115. package/packages/sdk/dist/workflows/verification.js +122 -0
  116. package/packages/sdk/dist/workflows/verification.js.map +1 -0
  117. package/packages/sdk/package.json +2 -2
  118. package/packages/sdk/src/broker-path.ts +136 -30
  119. package/packages/sdk/src/client.ts +37 -3
  120. package/packages/sdk/src/index.ts +1 -0
  121. package/packages/sdk/src/relay.ts +6 -2
  122. package/packages/sdk/src/workflows/__tests__/channel-messenger.test.ts +137 -0
  123. package/packages/sdk/src/workflows/__tests__/run-summary-table.test.ts +4 -3
  124. package/packages/sdk/src/workflows/__tests__/step-executor.test.ts +444 -0
  125. package/packages/sdk/src/workflows/__tests__/template-resolver.test.ts +162 -0
  126. package/packages/sdk/src/workflows/__tests__/verification.test.ts +229 -0
  127. package/packages/sdk/src/workflows/builder.ts +6 -6
  128. package/packages/sdk/src/workflows/channel-messenger.ts +314 -0
  129. package/packages/sdk/src/workflows/index.ts +12 -0
  130. package/packages/sdk/src/workflows/process-spawner.ts +201 -0
  131. package/packages/sdk/src/workflows/run.ts +2 -1
  132. package/packages/sdk/src/workflows/runner.ts +636 -951
  133. package/packages/sdk/src/workflows/step-executor.ts +579 -0
  134. package/packages/sdk/src/workflows/template-resolver.ts +180 -0
  135. package/packages/sdk/src/workflows/verification.ts +184 -0
  136. package/packages/sdk-py/pyproject.toml +1 -1
  137. package/packages/telemetry/package.json +1 -1
  138. package/packages/trajectory/package.json +2 -2
  139. package/packages/user-directory/package.json +2 -2
  140. package/packages/utils/package.json +2 -2
@@ -0,0 +1,579 @@
1
+ import { ChannelMessenger } from './channel-messenger.js';
2
+ import type { ProcessSpawner } from './process-spawner.js';
3
+ import { TemplateResolver } from './template-resolver.js';
4
+ import type { StepOutcome } from './trajectory.js';
5
+ import type {
6
+ AgentDefinition,
7
+ ErrorHandlingConfig,
8
+ StepCompletionMode,
9
+ VerificationCheck,
10
+ WorkflowStep,
11
+ WorkflowStepCompletionReason,
12
+ WorkflowStepRow,
13
+ WorkflowStepStatus,
14
+ } from './types.js';
15
+ import {
16
+ runVerification,
17
+ type VerificationOptions,
18
+ type VerificationResult,
19
+ type VerificationSideEffects,
20
+ } from './verification.js';
21
+
22
+ type StateLike = {
23
+ row: WorkflowStepRow;
24
+ };
25
+
26
+ export interface StepResult {
27
+ status: WorkflowStepStatus;
28
+ output: string;
29
+ exitCode?: number;
30
+ exitSignal?: string;
31
+ duration: number;
32
+ retries: number;
33
+ completionReason?: WorkflowStepCompletionReason;
34
+ error?: string;
35
+ }
36
+
37
+ export interface StepSchedule {
38
+ step: WorkflowStep;
39
+ readyAt: number;
40
+ staggerDelay: number;
41
+ }
42
+
43
+ export interface StepExecutorDeps<TState extends StateLike = StateLike> {
44
+ cwd: string;
45
+ runId?: string;
46
+ postToChannel?: (text: string) => void;
47
+ persistStepRow?: (stepId: string, patch: Partial<WorkflowStepRow>) => Promise<void>;
48
+ persistStepOutput?: (runId: string, stepName: string, output: string) => Promise<void>;
49
+ resolveTemplate?: (template: string, context: Record<string, unknown>) => string;
50
+ getStepOutput?: (stepName: string) => string | undefined;
51
+ loadStepOutput?: (runId: string, stepName: string) => string | undefined;
52
+ checkAborted?: () => void;
53
+ waitIfPaused?: () => Promise<void>;
54
+ log?: (message: string) => void;
55
+ processSpawner?: ProcessSpawner;
56
+ templateResolver?: TemplateResolver;
57
+ channelMessenger?: ChannelMessenger;
58
+ verificationRunner?: (
59
+ check: VerificationCheck,
60
+ output: string,
61
+ stepName: string,
62
+ injectedTaskText?: string,
63
+ options?: VerificationOptions,
64
+ sideEffects?: VerificationSideEffects
65
+ ) => VerificationResult;
66
+ executeStep?: (
67
+ step: WorkflowStep,
68
+ state: TState,
69
+ agentMap: Map<string, AgentDefinition>,
70
+ errorHandling?: ErrorHandlingConfig
71
+ ) => Promise<Partial<StepResult> | void>;
72
+ onStepStarted?: (step: WorkflowStep, state: TState) => Promise<void> | void;
73
+ onStepRetried?: (
74
+ step: WorkflowStep,
75
+ state: TState,
76
+ attempt: number,
77
+ maxRetries: number
78
+ ) => Promise<void> | void;
79
+ onStepCompleted?: (step: WorkflowStep, state: TState, result: StepResult) => Promise<void> | void;
80
+ onStepFailed?: (step: WorkflowStep, state: TState, result: StepResult) => Promise<void> | void;
81
+ onBeginTrack?: (steps: WorkflowStep[]) => Promise<void> | void;
82
+ onConverge?: (steps: WorkflowStep[], outcomes: StepOutcome[]) => Promise<void> | void;
83
+ markDownstreamSkipped?: (failedStepName: string) => Promise<void>;
84
+ buildCompletionMode?: (
85
+ stepName: string,
86
+ completionReason?: WorkflowStepCompletionReason
87
+ ) => StepCompletionMode | undefined;
88
+ }
89
+
90
+ export interface MonitorStepOptions<TState extends StateLike, TResult> {
91
+ maxRetries?: number;
92
+ retryDelayMs?: number;
93
+ startMessage?: string;
94
+ onStart?: (attempt: number, state: TState) => Promise<void> | void;
95
+ onRetry?: (attempt: number, maxRetries: number, state: TState) => Promise<void> | void;
96
+ execute: (attempt: number, state: TState) => Promise<TResult>;
97
+ toCompletionResult: (result: TResult, attempt: number, state: TState) => Partial<StepResult>;
98
+ onAttemptFailed?: (error: unknown, attempt: number, state: TState) => Promise<void> | void;
99
+ getFailureResult?: (error: unknown, attempt: number, state: TState) => Partial<StepResult>;
100
+ }
101
+
102
+ export class StepExecutor<TState extends StateLike = StateLike> {
103
+ private readonly templateResolver: TemplateResolver;
104
+ private readonly channelMessenger: ChannelMessenger;
105
+ private readonly verificationRunner: NonNullable<StepExecutorDeps<TState>['verificationRunner']>;
106
+
107
+ constructor(private readonly deps: StepExecutorDeps<TState>) {
108
+ this.templateResolver = deps.templateResolver ?? new TemplateResolver();
109
+ this.channelMessenger = deps.channelMessenger ?? new ChannelMessenger({ postFn: deps.postToChannel });
110
+ this.verificationRunner = deps.verificationRunner ?? runVerification;
111
+ }
112
+
113
+ findReady(
114
+ steps: WorkflowStep[],
115
+ statuses: Map<string, WorkflowStepStatus> | Map<string, TState>
116
+ ): WorkflowStep[] {
117
+ return steps.filter((step) => {
118
+ const state = statuses.get(step.name);
119
+ const status = this.getStatus(state);
120
+ if (status !== 'pending') return false;
121
+
122
+ return (step.dependsOn ?? []).every((dependency) => {
123
+ const depState = statuses.get(dependency);
124
+ const depStatus = this.getStatus(depState);
125
+ return depStatus === 'completed' || depStatus === 'skipped';
126
+ });
127
+ });
128
+ }
129
+
130
+ /** @deprecated Use {@link findReady} instead. This is an alias kept for backward compatibility. */
131
+ findReadySteps(
132
+ steps: WorkflowStep[],
133
+ statuses: Map<string, WorkflowStepStatus> | Map<string, TState>
134
+ ): WorkflowStep[] {
135
+ return this.findReady(steps, statuses);
136
+ }
137
+
138
+ scheduleStep(step: WorkflowStep, options: { readyAt?: number; staggerDelay?: number } = {}): StepSchedule {
139
+ return {
140
+ step,
141
+ readyAt: options.readyAt ?? Date.now(),
142
+ staggerDelay: options.staggerDelay ?? 0,
143
+ };
144
+ }
145
+
146
+ async startStep(step: WorkflowStep, state: TState, startMessage?: string): Promise<void> {
147
+ const startedAt = new Date().toISOString();
148
+ state.row.status = 'running';
149
+ state.row.error = undefined;
150
+ state.row.completionReason = undefined;
151
+ state.row.startedAt = startedAt;
152
+
153
+ await this.deps.persistStepRow?.(state.row.id, {
154
+ status: 'running',
155
+ error: undefined,
156
+ completionReason: undefined,
157
+ startedAt,
158
+ updatedAt: new Date().toISOString(),
159
+ });
160
+
161
+ if (startMessage) {
162
+ this.deps.postToChannel?.(startMessage);
163
+ }
164
+ await this.deps.onStepStarted?.(step, state);
165
+ }
166
+
167
+ async retryStep(step: WorkflowStep, state: TState, attempt: number, maxRetries: number): Promise<void> {
168
+ state.row.retryCount = attempt;
169
+ await this.deps.persistStepRow?.(state.row.id, {
170
+ retryCount: attempt,
171
+ updatedAt: new Date().toISOString(),
172
+ });
173
+ await this.deps.onStepRetried?.(step, state, attempt, maxRetries);
174
+ }
175
+
176
+ async completeStep(step: WorkflowStep, state: TState, result: Partial<StepResult>): Promise<StepResult> {
177
+ const completedAt = new Date().toISOString();
178
+ const finalResult: StepResult = {
179
+ status: result.status ?? 'completed',
180
+ output: result.output ?? '',
181
+ exitCode: result.exitCode,
182
+ exitSignal: result.exitSignal,
183
+ duration: result.duration ?? 0,
184
+ retries: result.retries ?? state.row.retryCount,
185
+ completionReason: result.completionReason,
186
+ error: result.error,
187
+ };
188
+
189
+ state.row.status = finalResult.status;
190
+ state.row.output = finalResult.output;
191
+ state.row.error = finalResult.error;
192
+ state.row.completionReason = finalResult.completionReason;
193
+ state.row.completedAt = completedAt;
194
+
195
+ await this.deps.persistStepRow?.(state.row.id, {
196
+ status: finalResult.status,
197
+ output: finalResult.output,
198
+ error: finalResult.error,
199
+ completionReason: finalResult.completionReason,
200
+ completedAt,
201
+ updatedAt: new Date().toISOString(),
202
+ });
203
+ if (finalResult.status === 'completed' && this.deps.runId && finalResult.output) {
204
+ await this.deps.persistStepOutput?.(this.deps.runId, step.name, finalResult.output);
205
+ }
206
+
207
+ if (finalResult.status === 'failed') {
208
+ await this.deps.onStepFailed?.(step, state, finalResult);
209
+ } else {
210
+ await this.deps.onStepCompleted?.(step, state, finalResult);
211
+ }
212
+ return finalResult;
213
+ }
214
+
215
+ async monitorStep<TResult>(
216
+ step: WorkflowStep,
217
+ state: TState,
218
+ options: MonitorStepOptions<TState, TResult>
219
+ ): Promise<StepResult> {
220
+ const maxRetries = options.maxRetries ?? 0;
221
+ const retryDelayMs = options.retryDelayMs ?? 1000;
222
+ let lastError: unknown;
223
+
224
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
225
+ this.deps.checkAborted?.();
226
+ await this.deps.waitIfPaused?.();
227
+
228
+ if (attempt > 0) {
229
+ await this.retryStep(step, state, attempt, maxRetries);
230
+ await options.onRetry?.(attempt, maxRetries, state);
231
+ if (retryDelayMs > 0) {
232
+ await delay(retryDelayMs);
233
+ }
234
+ }
235
+
236
+ const attemptStartedAt = Date.now();
237
+ await this.startStep(step, state, options.startMessage);
238
+ await options.onStart?.(attempt, state);
239
+
240
+ try {
241
+ const rawResult = await options.execute(attempt, state);
242
+ const completion = options.toCompletionResult(rawResult, attempt, state);
243
+ return await this.completeStep(step, state, {
244
+ ...completion,
245
+ duration: completion.duration ?? Date.now() - attemptStartedAt,
246
+ retries: completion.retries ?? attempt,
247
+ });
248
+ } catch (error) {
249
+ lastError = error;
250
+ await options.onAttemptFailed?.(error, attempt, state);
251
+ }
252
+ }
253
+
254
+ const failure = options.getFailureResult?.(lastError, maxRetries, state) ?? {
255
+ status: 'failed' as const,
256
+ output: '',
257
+ error: lastError instanceof Error ? lastError.message : String(lastError ?? 'Unknown error'),
258
+ retries: maxRetries,
259
+ };
260
+ return this.completeStep(step, state, {
261
+ ...failure,
262
+ status: 'failed',
263
+ });
264
+ }
265
+
266
+ async executeAll(
267
+ steps: WorkflowStep[],
268
+ agentMap: Map<string, AgentDefinition>,
269
+ errorHandling?: ErrorHandlingConfig,
270
+ providedStates?: Map<string, TState>
271
+ ): Promise<Map<string, StepResult>> {
272
+ const states = providedStates ?? this.createEphemeralStates(steps);
273
+ const strategy = normalizeStrategy(errorHandling?.strategy ?? 'fail-fast');
274
+ const results = new Map<string, StepResult>();
275
+
276
+ while (true) {
277
+ this.deps.checkAborted?.();
278
+ await this.deps.waitIfPaused?.();
279
+
280
+ const readySteps = this.findReady(steps, states);
281
+ if (readySteps.length === 0) break;
282
+
283
+ const schedules = readySteps.map((step, index) =>
284
+ this.scheduleStep(step, {
285
+ readyAt: Date.now(),
286
+ staggerDelay: readySteps.length > 3 ? index * 2_000 : 0,
287
+ })
288
+ );
289
+
290
+ if (schedules.length > 1) {
291
+ await this.deps.onBeginTrack?.(readySteps);
292
+ }
293
+
294
+ const settled = await Promise.allSettled(
295
+ schedules.map(async (schedule) => {
296
+ if (schedule.staggerDelay > 0) {
297
+ await delay(schedule.staggerDelay);
298
+ }
299
+ return this.executeScheduledStep(schedule.step, states, agentMap, errorHandling);
300
+ })
301
+ );
302
+
303
+ const batchOutcomes: StepOutcome[] = [];
304
+
305
+ for (let index = 0; index < settled.length; index += 1) {
306
+ const settledResult = settled[index];
307
+ const step = readySteps[index];
308
+ const state = states.get(step.name);
309
+
310
+ if (settledResult.status === 'fulfilled') {
311
+ const result = settledResult.value;
312
+ const outcomeStatus =
313
+ result.status === 'completed' || result.status === 'skipped' ? result.status : 'failed';
314
+ results.set(step.name, result);
315
+ batchOutcomes.push({
316
+ name: step.name,
317
+ agent: step.agent ?? 'deterministic',
318
+ status: outcomeStatus,
319
+ attempts: result.retries + 1,
320
+ output: result.output,
321
+ error: result.error,
322
+ verificationPassed: outcomeStatus === 'completed' && step.verification !== undefined,
323
+ completionMode:
324
+ result.completionReason !== undefined
325
+ ? this.deps.buildCompletionMode?.(step.name, result.completionReason)
326
+ : undefined,
327
+ });
328
+
329
+ if (result.status === 'failed') {
330
+ await this.deps.markDownstreamSkipped?.(step.name);
331
+ if (strategy === 'fail-fast') {
332
+ throw new Error(`Step "${step.name}" failed: ${result.error ?? 'unknown error'}`);
333
+ }
334
+ }
335
+ continue;
336
+ }
337
+
338
+ const error =
339
+ settledResult.reason instanceof Error ? settledResult.reason.message : String(settledResult.reason);
340
+ if (state) {
341
+ const failed =
342
+ state.row.status === 'failed'
343
+ ? {
344
+ status: 'failed' as const,
345
+ output: state.row.output ?? '',
346
+ duration: 0,
347
+ retries: state.row.retryCount,
348
+ completionReason: state.row.completionReason,
349
+ error: state.row.error ?? error,
350
+ }
351
+ : await this.completeStep(step, state, {
352
+ status: 'failed',
353
+ output: '',
354
+ error,
355
+ retries: state.row.retryCount,
356
+ });
357
+ results.set(step.name, failed);
358
+ }
359
+ batchOutcomes.push({
360
+ name: step.name,
361
+ agent: step.agent ?? 'deterministic',
362
+ status: 'failed',
363
+ attempts: (state?.row.retryCount ?? 0) + 1,
364
+ error,
365
+ });
366
+ await this.deps.markDownstreamSkipped?.(step.name);
367
+ if (strategy === 'fail-fast') {
368
+ throw new Error(`Step "${step.name}" failed: ${error}`);
369
+ }
370
+ }
371
+
372
+ if (readySteps.length > 1 && batchOutcomes.length > 0) {
373
+ await this.deps.onConverge?.(readySteps, batchOutcomes);
374
+ }
375
+ }
376
+
377
+ return results;
378
+ }
379
+
380
+ async executeOne(
381
+ step: WorkflowStep,
382
+ agentMap: Map<string, AgentDefinition>,
383
+ errorHandling?: ErrorHandlingConfig,
384
+ providedState?: TState
385
+ ): Promise<StepResult> {
386
+ const state = providedState ?? this.createEphemeralState(step);
387
+ if (this.deps.executeStep) {
388
+ const result = await this.deps.executeStep(step, state, agentMap, errorHandling);
389
+ if (state.row.status !== 'pending' && state.row.status !== 'running') {
390
+ return {
391
+ status: state.row.status,
392
+ output: state.row.output ?? '',
393
+ duration: result?.duration ?? 0,
394
+ retries: result?.retries ?? state.row.retryCount,
395
+ exitCode: result?.exitCode,
396
+ exitSignal: result?.exitSignal,
397
+ completionReason: state.row.completionReason ?? result?.completionReason,
398
+ error: state.row.error ?? result?.error,
399
+ };
400
+ }
401
+ return this.completeStep(step, state, {
402
+ status: result?.status ?? 'completed',
403
+ output: result?.output ?? '',
404
+ exitCode: result?.exitCode,
405
+ exitSignal: result?.exitSignal,
406
+ completionReason: result?.completionReason,
407
+ retries: result?.retries ?? state.row.retryCount,
408
+ duration: result?.duration ?? 0,
409
+ error: result?.error,
410
+ });
411
+ }
412
+
413
+ return this.executeWithProcessSpawner(step, state, agentMap, errorHandling);
414
+ }
415
+
416
+ async markFailed(stepName: string, error: string): Promise<void> {
417
+ this.deps.postToChannel?.(`**[${stepName}]** Failed: ${error}`);
418
+ }
419
+
420
+ buildStepOutputContext(stepStates: Map<string, TState>): Record<string, { output: string }> {
421
+ const steps: Record<string, { output: string }> = {};
422
+ for (const [name, state] of stepStates) {
423
+ if (state.row.status === 'completed' && state.row.output !== undefined) {
424
+ steps[name] = { output: state.row.output };
425
+ continue;
426
+ }
427
+ if (state.row.status === 'completed' && this.deps.runId) {
428
+ const persisted = this.deps.loadStepOutput?.(this.deps.runId, name);
429
+ if (persisted !== undefined) {
430
+ state.row.output = persisted;
431
+ steps[name] = { output: persisted };
432
+ }
433
+ }
434
+ }
435
+ return steps;
436
+ }
437
+
438
+ resolveStepTemplate(template: string, context: Record<string, unknown>): string {
439
+ if (this.deps.resolveTemplate) {
440
+ return this.deps.resolveTemplate(template, context);
441
+ }
442
+ return this.templateResolver.interpolateStepTask(template, context);
443
+ }
444
+
445
+ getChannelMessenger(): ChannelMessenger {
446
+ return this.channelMessenger;
447
+ }
448
+
449
+ runVerification(
450
+ check: VerificationCheck,
451
+ output: string,
452
+ stepName: string,
453
+ injectedTaskText?: string,
454
+ options?: VerificationOptions
455
+ ): VerificationResult {
456
+ return this.verificationRunner(check, output, stepName, injectedTaskText, {
457
+ ...options,
458
+ cwd: options?.cwd ?? this.deps.cwd,
459
+ });
460
+ }
461
+
462
+ private async executeScheduledStep(
463
+ step: WorkflowStep,
464
+ states: Map<string, TState>,
465
+ agentMap: Map<string, AgentDefinition>,
466
+ errorHandling?: ErrorHandlingConfig
467
+ ): Promise<StepResult> {
468
+ const state = states.get(step.name) ?? this.createEphemeralState(step);
469
+ if (!states.has(step.name)) {
470
+ states.set(step.name, state);
471
+ }
472
+ return this.executeOne(step, agentMap, errorHandling, state);
473
+ }
474
+
475
+ private async executeWithProcessSpawner(
476
+ step: WorkflowStep,
477
+ state: TState,
478
+ agentMap: Map<string, AgentDefinition>,
479
+ errorHandling?: ErrorHandlingConfig
480
+ ): Promise<StepResult> {
481
+ const spawner = this.deps.processSpawner;
482
+ if (!spawner) {
483
+ throw new Error(`No step execution callback or process spawner configured for step "${step.name}"`);
484
+ }
485
+
486
+ const maxRetries = step.retries ?? errorHandling?.maxRetries ?? 0;
487
+ return this.monitorStep(step, state, {
488
+ maxRetries,
489
+ retryDelayMs: errorHandling?.retryDelayMs ?? 1000,
490
+ startMessage: `**[${step.name}]** Started`,
491
+ onRetry: (attempt, total) => {
492
+ this.deps.postToChannel?.(`**[${step.name}]** Retrying (attempt ${attempt + 1}/${total + 1})`);
493
+ },
494
+ execute: async () => {
495
+ if (step.type === 'deterministic') {
496
+ const command = step.command ?? '';
497
+ return spawner.spawnShell(command, { cwd: this.deps.cwd, timeoutMs: step.timeoutMs });
498
+ }
499
+
500
+ const agent = step.agent ? agentMap.get(step.agent) : undefined;
501
+ if (!agent) {
502
+ throw new Error(`Agent "${step.agent ?? '(missing)'}" not found in config`);
503
+ }
504
+
505
+ const task = step.task ?? '';
506
+ if (agent.interactive === false) {
507
+ return spawner.spawnAgent(agent, task, { cwd: this.deps.cwd, timeoutMs: step.timeoutMs });
508
+ }
509
+ return spawner.spawnInteractive(agent, task, { cwd: this.deps.cwd, timeoutMs: step.timeoutMs });
510
+ },
511
+ toCompletionResult: (spawnResult, attempt) => {
512
+ const failOnError = step.failOnError !== false;
513
+ const failed =
514
+ failOnError &&
515
+ ((spawnResult.exitCode ?? 0) !== 0 ||
516
+ (spawnResult.exitCode === undefined && spawnResult.exitSignal !== undefined));
517
+ const output =
518
+ step.captureOutput === false
519
+ ? `Command completed (exit code ${spawnResult.exitCode ?? 0})`
520
+ : spawnResult.output;
521
+
522
+ if (failed) {
523
+ return {
524
+ status: 'failed' as const,
525
+ output,
526
+ exitCode: spawnResult.exitCode,
527
+ exitSignal: spawnResult.exitSignal,
528
+ retries: attempt,
529
+ error: spawnResult.output || `Command failed with exit code ${spawnResult.exitCode ?? 'unknown'}`,
530
+ };
531
+ }
532
+
533
+ return {
534
+ status: 'completed' as const,
535
+ output,
536
+ exitCode: spawnResult.exitCode,
537
+ exitSignal: spawnResult.exitSignal,
538
+ retries: attempt,
539
+ };
540
+ },
541
+ });
542
+ }
543
+
544
+ private createEphemeralStates(steps: WorkflowStep[]): Map<string, TState> {
545
+ return new Map(steps.map((step) => [step.name, this.createEphemeralState(step)]));
546
+ }
547
+
548
+ private createEphemeralState(step: WorkflowStep): TState {
549
+ return {
550
+ row: {
551
+ id: `step-${step.name}`,
552
+ runId: this.deps.runId ?? 'run',
553
+ stepName: step.name,
554
+ agentName: step.agent ?? null,
555
+ stepType: step.type ?? 'agent',
556
+ status: 'pending',
557
+ task: step.task ?? step.command ?? step.branch ?? '',
558
+ dependsOn: step.dependsOn ?? [],
559
+ retryCount: 0,
560
+ createdAt: new Date().toISOString(),
561
+ updatedAt: new Date().toISOString(),
562
+ },
563
+ } as TState;
564
+ }
565
+
566
+ private getStatus(state: WorkflowStepStatus | TState | undefined): WorkflowStepStatus | undefined {
567
+ if (typeof state === 'string') return state;
568
+ return state?.row.status;
569
+ }
570
+ }
571
+
572
+ function normalizeStrategy(strategy: ErrorHandlingConfig['strategy']): 'fail-fast' | 'continue' {
573
+ if (strategy === 'continue') return 'continue';
574
+ return 'fail-fast';
575
+ }
576
+
577
+ function delay(ms: number): Promise<void> {
578
+ return new Promise((resolve) => setTimeout(resolve, ms));
579
+ }