agent-relay 3.2.8 → 3.2.10
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/bin/agent-relay-broker-darwin-arm64 +0 -0
- package/bin/agent-relay-broker-darwin-x64 +0 -0
- package/bin/agent-relay-broker-linux-arm64 +0 -0
- package/bin/agent-relay-broker-linux-x64 +0 -0
- package/dist/index.cjs +45361 -23429
- package/dist/src/cli/commands/setup.d.ts +8 -0
- package/dist/src/cli/commands/setup.d.ts.map +1 -1
- package/dist/src/cli/commands/setup.js +42 -0
- package/dist/src/cli/commands/setup.js.map +1 -1
- package/dist/src/cli/relaycast-mcp.d.ts.map +1 -1
- package/dist/src/cli/relaycast-mcp.js +8 -1
- package/dist/src/cli/relaycast-mcp.js.map +1 -1
- package/package.json +13 -11
- package/packages/acp-bridge/package.json +2 -2
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/package.json +3 -3
- package/packages/policy/package.json +2 -2
- package/packages/sdk/dist/workflows/__tests__/cli-session-collector.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/cli-session-collector.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/cli-session-collector.test.js +54 -0
- package/packages/sdk/dist/workflows/__tests__/cli-session-collector.test.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/collectors/claude.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/collectors/claude.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/collectors/claude.test.js +85 -0
- package/packages/sdk/dist/workflows/__tests__/collectors/claude.test.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/collectors/codex.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/collectors/codex.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/collectors/codex.test.js +67 -0
- package/packages/sdk/dist/workflows/__tests__/collectors/codex.test.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/collectors/opencode.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/collectors/opencode.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/collectors/opencode.test.js +119 -0
- package/packages/sdk/dist/workflows/__tests__/collectors/opencode.test.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.js +130 -0
- package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/step-cwd.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/step-cwd.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/step-cwd.test.js +42 -0
- package/packages/sdk/dist/workflows/__tests__/step-cwd.test.js.map +1 -0
- package/packages/sdk/dist/workflows/builder.d.ts +2 -0
- package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/builder.js +4 -0
- package/packages/sdk/dist/workflows/builder.js.map +1 -1
- package/packages/sdk/dist/workflows/cli-session-collector.d.ts +39 -0
- package/packages/sdk/dist/workflows/cli-session-collector.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/cli-session-collector.js +23 -0
- package/packages/sdk/dist/workflows/cli-session-collector.js.map +1 -0
- package/packages/sdk/dist/workflows/cli.js +228 -48
- package/packages/sdk/dist/workflows/cli.js.map +1 -1
- package/packages/sdk/dist/workflows/collectors/claude.d.ts +6 -0
- package/packages/sdk/dist/workflows/collectors/claude.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/collectors/claude.js +330 -0
- package/packages/sdk/dist/workflows/collectors/claude.js.map +1 -0
- package/packages/sdk/dist/workflows/collectors/codex.d.ts +18 -0
- package/packages/sdk/dist/workflows/collectors/codex.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/collectors/codex.js +265 -0
- package/packages/sdk/dist/workflows/collectors/codex.js.map +1 -0
- package/packages/sdk/dist/workflows/collectors/opencode.d.ts +6 -0
- package/packages/sdk/dist/workflows/collectors/opencode.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/collectors/opencode.js +178 -0
- package/packages/sdk/dist/workflows/collectors/opencode.js.map +1 -0
- package/packages/sdk/dist/workflows/index.d.ts +3 -0
- package/packages/sdk/dist/workflows/index.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/index.js +3 -0
- package/packages/sdk/dist/workflows/index.js.map +1 -1
- package/packages/sdk/dist/workflows/listr-renderer.d.ts +26 -0
- package/packages/sdk/dist/workflows/listr-renderer.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/listr-renderer.js +232 -0
- package/packages/sdk/dist/workflows/listr-renderer.js.map +1 -0
- package/packages/sdk/dist/workflows/run-summary-table.d.ts +4 -0
- package/packages/sdk/dist/workflows/run-summary-table.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/run-summary-table.js +98 -0
- package/packages/sdk/dist/workflows/run-summary-table.js.map +1 -0
- package/packages/sdk/dist/workflows/runner.d.ts +11 -0
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +91 -26
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/dist/workflows/types.d.ts +2 -0
- package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/types.js.map +1 -1
- package/packages/sdk/package.json +5 -3
- package/packages/sdk/src/workflows/__tests__/cli-session-collector.test.ts +64 -0
- package/packages/sdk/src/workflows/__tests__/collectors/claude.test.ts +104 -0
- package/packages/sdk/src/workflows/__tests__/collectors/codex.test.ts +82 -0
- package/packages/sdk/src/workflows/__tests__/collectors/opencode.test.ts +178 -0
- package/packages/sdk/src/workflows/__tests__/run-summary-table.test.ts +160 -0
- package/packages/sdk/src/workflows/__tests__/step-cwd.test.ts +72 -0
- package/packages/sdk/src/workflows/builder.ts +4 -0
- package/packages/sdk/src/workflows/cli-session-collector.ts +58 -0
- package/packages/sdk/src/workflows/cli.ts +289 -50
- package/packages/sdk/src/workflows/collectors/claude.ts +415 -0
- package/packages/sdk/src/workflows/collectors/codex.ts +351 -0
- package/packages/sdk/src/workflows/collectors/opencode.ts +279 -0
- package/packages/sdk/src/workflows/index.ts +3 -0
- package/packages/sdk/src/workflows/listr-renderer.ts +278 -0
- package/packages/sdk/src/workflows/run-summary-table.ts +110 -0
- package/packages/sdk/src/workflows/runner.ts +122 -28
- package/packages/sdk/src/workflows/types.ts +2 -0
- package/packages/sdk/vitest.config.ts +1 -1
- package/packages/sdk-py/pyproject.toml +1 -1
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +2 -2
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import type { ListrTask } from 'listr2';
|
|
3
|
+
import type { WorkflowEvent, WorkflowEventListener } from './runner.js';
|
|
4
|
+
|
|
5
|
+
// Filter console.log while listr owns the terminal.
|
|
6
|
+
// Blocks [broker] noise and [workflow HH:MM] timing lines, but lets the
|
|
7
|
+
// observer URL and channel name through so users can track the run.
|
|
8
|
+
function installOutputFilter(): () => void {
|
|
9
|
+
const orig = console.log.bind(console);
|
|
10
|
+
console.log = (...args: unknown[]) => {
|
|
11
|
+
const str = String(args[0] ?? '');
|
|
12
|
+
// Always show the observer URL and channel so users can follow the run
|
|
13
|
+
if (str.includes('Observer:') || str.includes('agentrelay.dev') || str.includes('Channel: wf-')) {
|
|
14
|
+
orig(...args);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
// Block [broker] lines and [workflow HH:MM] timing lines
|
|
18
|
+
if (/\[broker\]/.test(str) || /\[workflow\s+\d{2}:\d{2}\]/.test(str)) return;
|
|
19
|
+
orig(...args);
|
|
20
|
+
};
|
|
21
|
+
return () => {
|
|
22
|
+
console.log = orig;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface RenderableTask {
|
|
27
|
+
title: string;
|
|
28
|
+
output: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface StepHandle {
|
|
32
|
+
resolve: () => void;
|
|
33
|
+
reject: (error: Error) => void;
|
|
34
|
+
setOutput: (text: string) => void;
|
|
35
|
+
markSkipped: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface WorkflowRenderer {
|
|
39
|
+
/** Pass this to `.run({ onEvent })` in your TypeScript workflow. */
|
|
40
|
+
onEvent: WorkflowEventListener;
|
|
41
|
+
/** Start the listr renderer. Run this concurrently with your workflow. */
|
|
42
|
+
start: () => Promise<void>;
|
|
43
|
+
/** Restore console.log after the workflow finishes. */
|
|
44
|
+
unmount: () => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Creates a listr2-based renderer for TypeScript workflows.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* import { workflow, createWorkflowRenderer } from '@agent-relay/sdk/workflows';
|
|
53
|
+
*
|
|
54
|
+
* const renderer = createWorkflowRenderer();
|
|
55
|
+
* const [result] = await Promise.all([
|
|
56
|
+
* workflow('my-workflow').step(...).run({ onEvent: renderer.onEvent }),
|
|
57
|
+
* renderer.start(),
|
|
58
|
+
* ]);
|
|
59
|
+
* renderer.unmount();
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function createWorkflowRenderer(): WorkflowRenderer {
|
|
63
|
+
const stepHandles = new Map<string, StepHandle>();
|
|
64
|
+
|
|
65
|
+
let resolveWorkflow!: () => void;
|
|
66
|
+
let rejectWorkflow!: (error: Error) => void;
|
|
67
|
+
const workflowDone = new Promise<void>((resolve, reject) => {
|
|
68
|
+
resolveWorkflow = resolve;
|
|
69
|
+
rejectWorkflow = reject;
|
|
70
|
+
});
|
|
71
|
+
// Prevent unhandled rejection if run:failed fires before the header task
|
|
72
|
+
// reaches `await workflowDone`.
|
|
73
|
+
workflowDone.catch(() => {});
|
|
74
|
+
|
|
75
|
+
let setHeader: (text: string) => void = () => {};
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
77
|
+
let listr: any = null;
|
|
78
|
+
const pendingAdds: ListrTask[] = [];
|
|
79
|
+
|
|
80
|
+
async function ensureListr(): Promise<any> {
|
|
81
|
+
if (listr) return listr;
|
|
82
|
+
const { Listr } = await import('listr2');
|
|
83
|
+
listr = new (Listr as any)(
|
|
84
|
+
[
|
|
85
|
+
{
|
|
86
|
+
title: chalk.dim('Workflow starting...'),
|
|
87
|
+
task: async (_ctx, task): Promise<void> => {
|
|
88
|
+
setHeader = (text: string): void => {
|
|
89
|
+
task.title = text;
|
|
90
|
+
};
|
|
91
|
+
await workflowDone;
|
|
92
|
+
},
|
|
93
|
+
} as ListrTask,
|
|
94
|
+
],
|
|
95
|
+
{
|
|
96
|
+
concurrent: true,
|
|
97
|
+
renderer: process.stdout.isTTY ? 'default' : 'verbose',
|
|
98
|
+
rendererOptions: {
|
|
99
|
+
collapseErrors: false,
|
|
100
|
+
showErrorMessage: true,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
for (const task of pendingAdds) listr.add(task);
|
|
105
|
+
pendingAdds.length = 0;
|
|
106
|
+
return listr;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const addTask = (task: ListrTask): void => {
|
|
110
|
+
if (listr) listr.add(task);
|
|
111
|
+
else pendingAdds.push(task);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const onEvent: WorkflowEventListener = (event: WorkflowEvent) => {
|
|
115
|
+
switch (event.type) {
|
|
116
|
+
case 'run:started': {
|
|
117
|
+
setHeader(chalk.dim(`[workflow] run ${event.runId.slice(0, 8)}...`));
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
case 'step:started': {
|
|
122
|
+
let resolveStep!: () => void;
|
|
123
|
+
let rejectStep!: (error: Error) => void;
|
|
124
|
+
let taskRef: RenderableTask | null = null;
|
|
125
|
+
let skipped = false;
|
|
126
|
+
|
|
127
|
+
const done = new Promise<void>((resolve, reject) => {
|
|
128
|
+
resolveStep = resolve;
|
|
129
|
+
rejectStep = reject;
|
|
130
|
+
});
|
|
131
|
+
// Prevent unhandled rejection if the step fails before the listr
|
|
132
|
+
// task function has started and reached `await done`.
|
|
133
|
+
done.catch(() => {});
|
|
134
|
+
|
|
135
|
+
stepHandles.set(event.stepName, {
|
|
136
|
+
resolve: resolveStep,
|
|
137
|
+
reject: rejectStep,
|
|
138
|
+
setOutput: (text: string) => {
|
|
139
|
+
if (taskRef) taskRef.output = text;
|
|
140
|
+
},
|
|
141
|
+
markSkipped: () => {
|
|
142
|
+
skipped = true;
|
|
143
|
+
if (taskRef) taskRef.title = chalk.dim(`${event.stepName} (skipped)`);
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
addTask({
|
|
148
|
+
title: chalk.white(event.stepName),
|
|
149
|
+
task: async (_ctx, task): Promise<void> => {
|
|
150
|
+
taskRef = task as RenderableTask;
|
|
151
|
+
if (skipped) taskRef.title = chalk.dim(`${event.stepName} (skipped)`);
|
|
152
|
+
await done;
|
|
153
|
+
},
|
|
154
|
+
rendererOptions: { persistentOutput: true },
|
|
155
|
+
} as ListrTask);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
case 'step:owner-assigned': {
|
|
160
|
+
const handle = stepHandles.get(event.stepName);
|
|
161
|
+
if (handle) {
|
|
162
|
+
handle.setOutput(
|
|
163
|
+
chalk.dim(`> Owner: ${event.ownerName}`) +
|
|
164
|
+
(event.specialistName ? chalk.dim(` · specialist: ${event.specialistName}`) : ''),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
case 'step:retrying': {
|
|
171
|
+
stepHandles.get(event.stepName)?.setOutput(chalk.yellow(`Retrying (attempt ${event.attempt})`));
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
case 'step:nudged': {
|
|
176
|
+
stepHandles.get(event.stepName)?.setOutput(chalk.dim(`> Nudge #${event.nudgeCount}`));
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
case 'step:force-released': {
|
|
181
|
+
stepHandles.get(event.stepName)?.setOutput(chalk.yellow('> Force-released'));
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
case 'step:review-completed': {
|
|
186
|
+
stepHandles
|
|
187
|
+
.get(event.stepName)
|
|
188
|
+
?.setOutput(chalk.dim(`> Review: ${event.decision} by ${event.reviewerName}`));
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
case 'step:owner-timeout': {
|
|
193
|
+
stepHandles
|
|
194
|
+
.get(event.stepName)
|
|
195
|
+
?.setOutput(chalk.red(`> Owner ${event.ownerName} timed out`));
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
case 'step:agent-report': {
|
|
200
|
+
const handle = stepHandles.get(event.stepName);
|
|
201
|
+
if (handle) {
|
|
202
|
+
const model = event.report.model ? `:${event.report.model}` : '';
|
|
203
|
+
handle.setOutput(chalk.dim(`> Report collected (${event.report.cli}${model})`));
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
case 'step:completed': {
|
|
209
|
+
stepHandles.get(event.stepName)?.resolve();
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
case 'step:skipped': {
|
|
214
|
+
const handle = stepHandles.get(event.stepName);
|
|
215
|
+
if (handle) {
|
|
216
|
+
handle.markSkipped();
|
|
217
|
+
handle.resolve();
|
|
218
|
+
} else {
|
|
219
|
+
// Step was skipped without ever being started (downstream of a failure).
|
|
220
|
+
addTask({
|
|
221
|
+
title: chalk.dim(`${event.stepName} (skipped)`),
|
|
222
|
+
task: async (): Promise<void> => {},
|
|
223
|
+
rendererOptions: { persistentOutput: true },
|
|
224
|
+
} as ListrTask);
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
case 'step:failed': {
|
|
230
|
+
stepHandles.get(event.stepName)?.reject(new Error(event.error ?? 'Step failed'));
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
case 'run:completed': {
|
|
235
|
+
setHeader(chalk.green('Workflow completed'));
|
|
236
|
+
resolveWorkflow();
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
case 'run:failed': {
|
|
241
|
+
setHeader(chalk.red(`Workflow failed: ${event.error ?? 'unknown error'}`));
|
|
242
|
+
rejectWorkflow(new Error(event.error ?? 'Workflow failed'));
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
case 'run:cancelled': {
|
|
247
|
+
setHeader(chalk.yellow('Workflow cancelled'));
|
|
248
|
+
resolveWorkflow();
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
case 'broker:event':
|
|
253
|
+
break;
|
|
254
|
+
|
|
255
|
+
default: {
|
|
256
|
+
const _exhaustive: never = event;
|
|
257
|
+
void _exhaustive;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
let restoreConsole: (() => void) | undefined;
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
onEvent,
|
|
266
|
+
start: async () => {
|
|
267
|
+
restoreConsole = installOutputFilter();
|
|
268
|
+
const l = await ensureListr();
|
|
269
|
+
return l.run().catch(() => {
|
|
270
|
+
// Step failures are already represented in the workflow result.
|
|
271
|
+
});
|
|
272
|
+
},
|
|
273
|
+
unmount: () => {
|
|
274
|
+
restoreConsole?.();
|
|
275
|
+
restoreConsole = undefined;
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { CliSessionReport } from './cli-session-collector.js';
|
|
2
|
+
import type { StepOutcome } from './trajectory.js';
|
|
3
|
+
|
|
4
|
+
function formatCurrency(value: number | null | undefined): string {
|
|
5
|
+
return typeof value === 'number' ? `$${value.toFixed(2)}` : '--';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function formatTokens(report: CliSessionReport | undefined): string {
|
|
9
|
+
if (!report?.tokens) return '--';
|
|
10
|
+
const total = report.tokens.input + report.tokens.output + report.tokens.cacheRead;
|
|
11
|
+
return total.toLocaleString('en-US');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function formatDuration(durationMs: number | null | undefined): string {
|
|
15
|
+
if (typeof durationMs !== 'number' || !Number.isFinite(durationMs)) return '--';
|
|
16
|
+
if (durationMs < 1000) return `${durationMs}ms`;
|
|
17
|
+
|
|
18
|
+
const totalSeconds = Math.round(durationMs / 1000);
|
|
19
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
20
|
+
const seconds = totalSeconds % 60;
|
|
21
|
+
return minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function truncate(value: string, length: number): string {
|
|
25
|
+
if (value.length <= length) return value;
|
|
26
|
+
if (length <= 1) return value.slice(0, length);
|
|
27
|
+
return `${value.slice(0, length - 1)}…`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function pad(value: string, width: number, align: 'left' | 'right' = 'left'): string {
|
|
31
|
+
return align === 'right' ? value.padStart(width, ' ') : value.padEnd(width, ' ');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatErrors(outcome: StepOutcome, report: CliSessionReport | undefined): string {
|
|
35
|
+
const count = report?.errors.length ?? 0;
|
|
36
|
+
if (count === 0) return outcome.status === 'failed' && outcome.error ? '1' : '--';
|
|
37
|
+
if (outcome.status === 'completed') return `${count} (fixed)`;
|
|
38
|
+
return String(count);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function formatRunSummaryTable(
|
|
42
|
+
outcomes: StepOutcome[],
|
|
43
|
+
reports: Map<string, CliSessionReport>
|
|
44
|
+
): string {
|
|
45
|
+
// Only show the Cost column when at least one report has reliable cost data
|
|
46
|
+
// (currently only OpenCode populates cost; Claude and Codex return null)
|
|
47
|
+
const hasCost = Array.from(reports.values()).some((r) => typeof r.cost === 'number' && r.cost > 0);
|
|
48
|
+
|
|
49
|
+
const headers = hasCost
|
|
50
|
+
? ['Step', 'Status', 'Model', 'Cost', 'Tokens', 'Duration', 'Errors']
|
|
51
|
+
: ['Step', 'Status', 'Model', 'Tokens', 'Duration', 'Errors'];
|
|
52
|
+
const widths = hasCost
|
|
53
|
+
? [20, 6, 16, 8, 10, 10, 10]
|
|
54
|
+
: [20, 6, 16, 10, 10, 10];
|
|
55
|
+
const lines: string[] = [];
|
|
56
|
+
|
|
57
|
+
lines.push(headers.map((h, i) => {
|
|
58
|
+
const align = i <= 2 ? 'left' : 'right';
|
|
59
|
+
return pad(h, widths[i], align);
|
|
60
|
+
}).join(' '));
|
|
61
|
+
|
|
62
|
+
let totalCost = 0;
|
|
63
|
+
let totalTokens = 0;
|
|
64
|
+
let totalDurationMs = 0;
|
|
65
|
+
|
|
66
|
+
for (const outcome of outcomes) {
|
|
67
|
+
const report = reports.get(outcome.name);
|
|
68
|
+
const reportDuration = report?.durationMs ?? outcome.durationMs;
|
|
69
|
+
const reportTokens = report?.tokens
|
|
70
|
+
? report.tokens.input + report.tokens.output + report.tokens.cacheRead
|
|
71
|
+
: 0;
|
|
72
|
+
|
|
73
|
+
if (typeof report?.cost === 'number') totalCost += report.cost;
|
|
74
|
+
totalTokens += reportTokens;
|
|
75
|
+
if (typeof reportDuration === 'number') totalDurationMs += reportDuration;
|
|
76
|
+
|
|
77
|
+
const cols: string[] = [
|
|
78
|
+
pad(truncate(outcome.name, widths[0]), widths[0]),
|
|
79
|
+
pad(outcome.status === 'failed' ? 'FAIL' : outcome.status === 'completed' ? 'pass' : 'skip', widths[1]),
|
|
80
|
+
pad(truncate(report?.model ?? '--', widths[2]), widths[2]),
|
|
81
|
+
];
|
|
82
|
+
if (hasCost) cols.push(pad(formatCurrency(report?.cost), widths[3], 'right'));
|
|
83
|
+
const tokenIdx = hasCost ? 4 : 3;
|
|
84
|
+
cols.push(pad(formatTokens(report), widths[tokenIdx], 'right'));
|
|
85
|
+
cols.push(pad(formatDuration(reportDuration), widths[tokenIdx + 1], 'right'));
|
|
86
|
+
cols.push(pad(formatErrors(outcome, report), widths[tokenIdx + 2], 'right'));
|
|
87
|
+
|
|
88
|
+
lines.push(cols.join(' '));
|
|
89
|
+
|
|
90
|
+
if (outcome.status === 'failed') {
|
|
91
|
+
const firstError = report?.errors[0];
|
|
92
|
+
if (firstError) {
|
|
93
|
+
lines.push(` └─ Error [turn ${firstError.turn}] ${truncate(firstError.text, 120)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const totalLabelWidth = widths[0] + widths[1] + widths[2] + 4;
|
|
99
|
+
lines.push('─'.repeat(lines[0].length));
|
|
100
|
+
|
|
101
|
+
const totalCols: string[] = [pad('Total', totalLabelWidth)];
|
|
102
|
+
if (hasCost) totalCols.push(pad(formatCurrency(totalCost), widths[3], 'right'));
|
|
103
|
+
const tokenIdx = hasCost ? 4 : 3;
|
|
104
|
+
totalCols.push(pad(totalTokens > 0 ? totalTokens.toLocaleString('en-US') : '--', widths[tokenIdx], 'right'));
|
|
105
|
+
totalCols.push(pad(formatDuration(totalDurationMs), widths[tokenIdx + 1], 'right'));
|
|
106
|
+
totalCols.push(pad('', widths[tokenIdx + 2], 'right'));
|
|
107
|
+
lines.push(totalCols.join(' '));
|
|
108
|
+
|
|
109
|
+
return lines.map((line) => ` ${line}`).join('\n');
|
|
110
|
+
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import type { Dirent, WriteStream } from 'node:fs';
|
|
20
20
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
21
21
|
import path from 'node:path';
|
|
22
|
+
import chalk from 'chalk';
|
|
22
23
|
|
|
23
24
|
import { parse as parseYaml } from 'yaml';
|
|
24
25
|
import { stripAnsi as stripAnsiFn } from '../pty.js';
|
|
@@ -32,7 +33,9 @@ import {
|
|
|
32
33
|
CustomStepsParseError,
|
|
33
34
|
CustomStepResolutionError,
|
|
34
35
|
} from './custom-steps.js';
|
|
36
|
+
import { collectCliSession, type CliSessionReport } from './cli-session-collector.js';
|
|
35
37
|
import { InMemoryWorkflowDb } from './memory-db.js';
|
|
38
|
+
import { formatRunSummaryTable } from './run-summary-table.js';
|
|
36
39
|
import type {
|
|
37
40
|
AgentCli,
|
|
38
41
|
AgentDefinition,
|
|
@@ -157,6 +160,7 @@ export type WorkflowEvent =
|
|
|
157
160
|
decision: 'approved' | 'rejected';
|
|
158
161
|
}
|
|
159
162
|
| { type: 'step:owner-timeout'; runId: string; stepName: string; ownerName: string }
|
|
163
|
+
| { type: 'step:agent-report'; runId: string; stepName: string; report: CliSessionReport }
|
|
160
164
|
| { type: 'step:failed'; runId: string; stepName: string; error: string; exitCode?: number; exitSignal?: string }
|
|
161
165
|
| { type: 'step:skipped'; runId: string; stepName: string }
|
|
162
166
|
| { type: 'step:retrying'; runId: string; stepName: string; attempt: number }
|
|
@@ -360,6 +364,8 @@ export class WorkflowRunner {
|
|
|
360
364
|
private resolvedPaths = new Map<string, string>();
|
|
361
365
|
/** Tracks agent names currently assigned as reviewers (ref-counted to handle concurrent usage). */
|
|
362
366
|
private readonly activeReviewers = new Map<string, number>();
|
|
367
|
+
/** Structured CLI session reports captured during the current run, keyed by step name. */
|
|
368
|
+
private readonly agentReports = new Map<string, CliSessionReport>();
|
|
363
369
|
|
|
364
370
|
constructor(options: WorkflowRunnerOptions = {}) {
|
|
365
371
|
this.db = options.db ?? new InMemoryWorkflowDb();
|
|
@@ -454,6 +460,13 @@ export class WorkflowRunner {
|
|
|
454
460
|
return resolved;
|
|
455
461
|
}
|
|
456
462
|
|
|
463
|
+
private resolveEffectiveCwd(step: WorkflowStep, agentDef?: AgentDefinition): string {
|
|
464
|
+
if (step.cwd) {
|
|
465
|
+
return path.resolve(this.cwd, step.cwd);
|
|
466
|
+
}
|
|
467
|
+
return this.resolveStepWorkdir(step) ?? (agentDef ? this.resolveAgentCwd(agentDef) : this.cwd);
|
|
468
|
+
}
|
|
469
|
+
|
|
457
470
|
private static readonly EVIDENCE_IGNORED_DIRS = new Set([
|
|
458
471
|
'.git',
|
|
459
472
|
'.agent-relay',
|
|
@@ -990,7 +1003,7 @@ export class WorkflowRunner {
|
|
|
990
1003
|
mins > 0
|
|
991
1004
|
? `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
|
|
992
1005
|
: `00:${String(secs).padStart(2, '0')}`;
|
|
993
|
-
console.log(
|
|
1006
|
+
console.log(`${chalk.dim.cyan('[workflow')} ${chalk.dim.cyan(ts)}${chalk.dim.cyan(']')} ${msg}`);
|
|
994
1007
|
}
|
|
995
1008
|
|
|
996
1009
|
// ── Relaycast auto-provisioning ────────────────────────────────────────
|
|
@@ -1993,6 +2006,7 @@ export class WorkflowRunner {
|
|
|
1993
2006
|
this.runStartTime = Date.now();
|
|
1994
2007
|
this.runtimeStepAgents.clear();
|
|
1995
2008
|
this.stepCompletionEvidence.clear();
|
|
2009
|
+
this.agentReports.clear();
|
|
1996
2010
|
|
|
1997
2011
|
this.log(`Starting workflow "${workflow.name}" (${workflow.steps.length} steps)`);
|
|
1998
2012
|
|
|
@@ -2235,7 +2249,7 @@ export class WorkflowRunner {
|
|
|
2235
2249
|
|
|
2236
2250
|
// Wire broker stderr to console for observability
|
|
2237
2251
|
this.unsubBrokerStderr = this.relay.onBrokerStderr((line: string) => {
|
|
2238
|
-
console.log(
|
|
2252
|
+
console.log(`${chalk.dim.yellow('[broker]')} ${line}`);
|
|
2239
2253
|
});
|
|
2240
2254
|
|
|
2241
2255
|
if (!relaycastDisabled) {
|
|
@@ -2771,7 +2785,7 @@ export class WorkflowRunner {
|
|
|
2771
2785
|
});
|
|
2772
2786
|
|
|
2773
2787
|
// Resolve step workdir (named path reference) for deterministic steps
|
|
2774
|
-
const stepCwd = this.
|
|
2788
|
+
const stepCwd = this.resolveEffectiveCwd(step);
|
|
2775
2789
|
this.beginStepEvidence(step.name, [stepCwd], state.row.startedAt);
|
|
2776
2790
|
|
|
2777
2791
|
try {
|
|
@@ -3245,6 +3259,9 @@ export class WorkflowRunner {
|
|
|
3245
3259
|
let lastExitCode: number | undefined;
|
|
3246
3260
|
let lastExitSignal: string | undefined;
|
|
3247
3261
|
let lastCompletionReason: WorkflowStepCompletionReason | undefined;
|
|
3262
|
+
let lastAttemptStartedAt: number | undefined;
|
|
3263
|
+
let lastEffectiveAgentDef: AgentDefinition | undefined;
|
|
3264
|
+
let lastEffectiveCwd: string | undefined;
|
|
3248
3265
|
|
|
3249
3266
|
// OWNER_DECISION: INCOMPLETE_RETRY is enforced here at the attempt-loop level so every
|
|
3250
3267
|
// interactive execution path shares the same contract:
|
|
@@ -3276,6 +3293,7 @@ export class WorkflowRunner {
|
|
|
3276
3293
|
}
|
|
3277
3294
|
|
|
3278
3295
|
try {
|
|
3296
|
+
lastAttemptStartedAt = Date.now();
|
|
3279
3297
|
// Mark step as running
|
|
3280
3298
|
state.row.status = 'running';
|
|
3281
3299
|
state.row.error = undefined;
|
|
@@ -3334,7 +3352,10 @@ export class WorkflowRunner {
|
|
|
3334
3352
|
}
|
|
3335
3353
|
|
|
3336
3354
|
// Apply step-level workdir override to agent definitions if present
|
|
3337
|
-
const
|
|
3355
|
+
const applyStepCwd = (def: AgentDefinition): AgentDefinition => {
|
|
3356
|
+
if (step.cwd) {
|
|
3357
|
+
return { ...def, cwd: step.cwd, workdir: undefined };
|
|
3358
|
+
}
|
|
3338
3359
|
if (step.workdir) {
|
|
3339
3360
|
const stepWorkdir = this.resolveStepWorkdir(step);
|
|
3340
3361
|
if (stepWorkdir) {
|
|
@@ -3343,9 +3364,11 @@ export class WorkflowRunner {
|
|
|
3343
3364
|
}
|
|
3344
3365
|
return def;
|
|
3345
3366
|
};
|
|
3346
|
-
const effectiveSpecialist =
|
|
3347
|
-
const effectiveOwner =
|
|
3348
|
-
const effectiveReviewer = reviewDef ?
|
|
3367
|
+
const effectiveSpecialist = applyStepCwd(specialistDef);
|
|
3368
|
+
const effectiveOwner = applyStepCwd(ownerDef);
|
|
3369
|
+
const effectiveReviewer = reviewDef ? applyStepCwd(reviewDef) : undefined;
|
|
3370
|
+
lastEffectiveAgentDef = effectiveSpecialist;
|
|
3371
|
+
lastEffectiveCwd = this.resolveAgentCwd(effectiveSpecialist);
|
|
3349
3372
|
this.beginStepEvidence(
|
|
3350
3373
|
step.name,
|
|
3351
3374
|
[
|
|
@@ -3518,6 +3541,15 @@ export class WorkflowRunner {
|
|
|
3518
3541
|
}
|
|
3519
3542
|
}
|
|
3520
3543
|
|
|
3544
|
+
await this.captureAgentReport(
|
|
3545
|
+
runId,
|
|
3546
|
+
step.name,
|
|
3547
|
+
lastEffectiveAgentDef,
|
|
3548
|
+
lastEffectiveCwd,
|
|
3549
|
+
lastAttemptStartedAt,
|
|
3550
|
+
Date.now()
|
|
3551
|
+
);
|
|
3552
|
+
|
|
3521
3553
|
// Mark completed
|
|
3522
3554
|
state.row.status = 'completed';
|
|
3523
3555
|
state.row.output = combinedOutput;
|
|
@@ -3570,6 +3602,14 @@ export class WorkflowRunner {
|
|
|
3570
3602
|
typeof step.verification === 'object' && 'value' in step.verification
|
|
3571
3603
|
? String(step.verification.value)
|
|
3572
3604
|
: undefined;
|
|
3605
|
+
await this.captureAgentReport(
|
|
3606
|
+
runId,
|
|
3607
|
+
step.name,
|
|
3608
|
+
lastEffectiveAgentDef,
|
|
3609
|
+
lastEffectiveCwd,
|
|
3610
|
+
lastAttemptStartedAt,
|
|
3611
|
+
Date.now()
|
|
3612
|
+
);
|
|
3573
3613
|
await this.trajectory?.stepFailed(step, lastError ?? 'Unknown error', maxRetries + 1, maxRetries, {
|
|
3574
3614
|
agent: agentName,
|
|
3575
3615
|
nonInteractive,
|
|
@@ -4856,7 +4896,7 @@ export class WorkflowRunner {
|
|
|
4856
4896
|
const { stdout: output, exitCode, exitSignal } = await new Promise<{ stdout: string; exitCode?: number; exitSignal?: string }>((resolve, reject) => {
|
|
4857
4897
|
const child = cpSpawn(cmd, args, {
|
|
4858
4898
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
4859
|
-
cwd: this.
|
|
4899
|
+
cwd: this.resolveEffectiveCwd(step, agentDef),
|
|
4860
4900
|
env: this.getRelayEnv() ?? { ...process.env },
|
|
4861
4901
|
});
|
|
4862
4902
|
|
|
@@ -5728,6 +5768,35 @@ export class WorkflowRunner {
|
|
|
5728
5768
|
this.finalizeStepEvidence(state.row.stepName, 'failed', state.row.completedAt, completionReason);
|
|
5729
5769
|
}
|
|
5730
5770
|
|
|
5771
|
+
private async captureAgentReport(
|
|
5772
|
+
runId: string,
|
|
5773
|
+
stepName: string,
|
|
5774
|
+
agentDef: AgentDefinition | undefined,
|
|
5775
|
+
cwd: string | undefined,
|
|
5776
|
+
startedAt: number | undefined,
|
|
5777
|
+
completedAt: number
|
|
5778
|
+
): Promise<void> {
|
|
5779
|
+
if (!agentDef || !cwd || !startedAt) return;
|
|
5780
|
+
|
|
5781
|
+
try {
|
|
5782
|
+
const report = await collectCliSession({
|
|
5783
|
+
cli: agentDef.cli,
|
|
5784
|
+
cwd,
|
|
5785
|
+
startedAt,
|
|
5786
|
+
completedAt,
|
|
5787
|
+
});
|
|
5788
|
+
if (!report) return;
|
|
5789
|
+
|
|
5790
|
+
this.agentReports.set(stepName, report);
|
|
5791
|
+
this.emit({ type: 'step:agent-report', runId, stepName, report });
|
|
5792
|
+
await this.persistAgentReport(runId, stepName, report);
|
|
5793
|
+
} catch (error) {
|
|
5794
|
+
this.log(
|
|
5795
|
+
`[${stepName}] CLI session collection failed: ${error instanceof Error ? error.message : String(error)}`
|
|
5796
|
+
);
|
|
5797
|
+
}
|
|
5798
|
+
}
|
|
5799
|
+
|
|
5731
5800
|
private async markDownstreamSkipped(
|
|
5732
5801
|
failedStepName: string,
|
|
5733
5802
|
allSteps: WorkflowStep[],
|
|
@@ -6003,26 +6072,33 @@ export class WorkflowRunner {
|
|
|
6003
6072
|
const skipped = outcomes.filter((o) => o.status === 'skipped');
|
|
6004
6073
|
|
|
6005
6074
|
console.log('');
|
|
6006
|
-
console.log('━'.repeat(70));
|
|
6007
|
-
console.log(` Workflow "${workflowName}" — ${failed.length === 0 ? 'COMPLETED' : 'FAILED'}`);
|
|
6008
|
-
console.log(
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
console.log(
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6075
|
+
console.log(chalk.dim('━'.repeat(70)));
|
|
6076
|
+
console.log(` Workflow "${workflowName}" — ${failed.length === 0 ? chalk.green('COMPLETED') : chalk.red('FAILED')}`);
|
|
6077
|
+
console.log(
|
|
6078
|
+
` ${chalk.green(`${completed.length} passed`)}, ${chalk.red(`${failed.length} failed`)}, ${chalk.dim(`${skipped.length} skipped`)}`
|
|
6079
|
+
);
|
|
6080
|
+
console.log(chalk.dim('━'.repeat(70)));
|
|
6081
|
+
|
|
6082
|
+
if (this.agentReports.size > 0) {
|
|
6083
|
+
console.log(formatRunSummaryTable(outcomes, this.agentReports));
|
|
6084
|
+
} else {
|
|
6085
|
+
for (const outcome of outcomes) {
|
|
6086
|
+
const icon =
|
|
6087
|
+
outcome.status === 'completed' ? chalk.green('✓') : outcome.status === 'failed' ? chalk.red('✗') : chalk.dim('⊘');
|
|
6088
|
+
const retryNote = outcome.attempts > 1 ? ` (${outcome.attempts} attempts)` : '';
|
|
6089
|
+
console.log(` ${icon} ${outcome.name} [${outcome.agent}]${retryNote}`);
|
|
6090
|
+
|
|
6091
|
+
if (outcome.error) {
|
|
6092
|
+
console.log(` Error: ${outcome.error}`);
|
|
6093
|
+
}
|
|
6019
6094
|
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6095
|
+
// Extract last meaningful lines from raw PTY output
|
|
6096
|
+
if (outcome.output) {
|
|
6097
|
+
const excerpt = this.extractOutputExcerpt(outcome.output);
|
|
6098
|
+
if (excerpt) {
|
|
6099
|
+
for (const line of excerpt.split('\n')) {
|
|
6100
|
+
console.log(` ${line}`);
|
|
6101
|
+
}
|
|
6026
6102
|
}
|
|
6027
6103
|
}
|
|
6028
6104
|
}
|
|
@@ -6032,9 +6108,10 @@ export class WorkflowRunner {
|
|
|
6032
6108
|
const outputDir = this.getStepOutputDir(runId);
|
|
6033
6109
|
const logsDir = path.join(this.cwd, '.agent-relay', 'team', 'worker-logs');
|
|
6034
6110
|
console.log('');
|
|
6111
|
+
console.log(` Run ID: ${runId}`);
|
|
6035
6112
|
console.log(` Step output: ${outputDir}`);
|
|
6036
6113
|
console.log(` Agent logs: ${logsDir}`);
|
|
6037
|
-
console.log('━'.repeat(70));
|
|
6114
|
+
console.log(chalk.dim('━'.repeat(70)));
|
|
6038
6115
|
console.log('');
|
|
6039
6116
|
}
|
|
6040
6117
|
|
|
@@ -6095,6 +6172,12 @@ export class WorkflowRunner {
|
|
|
6095
6172
|
const stepsWithVerification = new Set(steps?.filter((s) => s.verification).map((s) => s.name) ?? []);
|
|
6096
6173
|
const outcomes: StepOutcome[] = [];
|
|
6097
6174
|
for (const [name, state] of stepStates) {
|
|
6175
|
+
const startedAtMs = state.row.startedAt ? Date.parse(state.row.startedAt) : Number.NaN;
|
|
6176
|
+
const completedAtMs = state.row.completedAt ? Date.parse(state.row.completedAt) : Number.NaN;
|
|
6177
|
+
const durationMs =
|
|
6178
|
+
Number.isFinite(startedAtMs) && Number.isFinite(completedAtMs)
|
|
6179
|
+
? Math.max(0, completedAtMs - startedAtMs)
|
|
6180
|
+
: undefined;
|
|
6098
6181
|
outcomes.push({
|
|
6099
6182
|
name,
|
|
6100
6183
|
agent: state.row.agentName ?? 'deterministic',
|
|
@@ -6108,6 +6191,7 @@ export class WorkflowRunner {
|
|
|
6108
6191
|
output: state.row.output,
|
|
6109
6192
|
error: state.row.error,
|
|
6110
6193
|
verificationPassed: state.row.status === 'completed' && stepsWithVerification.has(name),
|
|
6194
|
+
durationMs,
|
|
6111
6195
|
completionMode: state.row.completionReason
|
|
6112
6196
|
? this.buildStepCompletionDecision(name, state.row.completionReason)?.mode
|
|
6113
6197
|
: undefined,
|
|
@@ -6290,6 +6374,16 @@ export class WorkflowRunner {
|
|
|
6290
6374
|
this.postToChannel(`**[${stepName}] Output:**\n\`\`\`\n${preview}\n\`\`\``, { stepName });
|
|
6291
6375
|
}
|
|
6292
6376
|
|
|
6377
|
+
private async persistAgentReport(runId: string, stepName: string, report: CliSessionReport): Promise<void> {
|
|
6378
|
+
const reportPath = path.join(this.getStepOutputDir(runId), `${stepName}.report.json`);
|
|
6379
|
+
try {
|
|
6380
|
+
mkdirSync(this.getStepOutputDir(runId), { recursive: true });
|
|
6381
|
+
await writeFile(reportPath, JSON.stringify(report, null, 2), 'utf8');
|
|
6382
|
+
} catch {
|
|
6383
|
+
// Non-critical
|
|
6384
|
+
}
|
|
6385
|
+
}
|
|
6386
|
+
|
|
6293
6387
|
/** Scan .agent-relay/step-outputs/ for the most recent run directory containing the needed steps. */
|
|
6294
6388
|
private findMostRecentRunWithSteps(stepNames: Set<string>): string | undefined {
|
|
6295
6389
|
try {
|