agent-relay 3.2.9 → 3.2.11
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 +9183 -698
- 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 +11 -9
- 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 +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/sdk/dist/cli-registry.d.ts +42 -0
- package/packages/sdk/dist/cli-registry.d.ts.map +1 -0
- package/packages/sdk/dist/cli-registry.js +126 -0
- package/packages/sdk/dist/cli-registry.js.map +1 -0
- package/packages/sdk/dist/cli-resolver.d.ts +30 -0
- package/packages/sdk/dist/cli-resolver.d.ts.map +1 -0
- package/packages/sdk/dist/cli-resolver.js +132 -0
- package/packages/sdk/dist/cli-resolver.js.map +1 -0
- package/packages/sdk/dist/index.d.ts +2 -0
- package/packages/sdk/dist/index.d.ts.map +1 -1
- package/packages/sdk/dist/index.js +2 -0
- package/packages/sdk/dist/index.js.map +1 -1
- package/packages/sdk/dist/spawn-from-env.d.ts.map +1 -1
- package/packages/sdk/dist/spawn-from-env.js +6 -15
- package/packages/sdk/dist/spawn-from-env.js.map +1 -1
- 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 +7 -0
- package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/builder.js +40 -5
- 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 +204 -0
- package/packages/sdk/dist/workflows/collectors/opencode.js.map +1 -0
- package/packages/sdk/dist/workflows/default-logger.d.ts +9 -0
- package/packages/sdk/dist/workflows/default-logger.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/default-logger.js +104 -0
- package/packages/sdk/dist/workflows/default-logger.js.map +1 -0
- package/packages/sdk/dist/workflows/index.d.ts +4 -0
- package/packages/sdk/dist/workflows/index.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/index.js +4 -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 +12 -1
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +107 -71
- 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 +4 -2
- package/packages/sdk/src/cli-registry.ts +148 -0
- package/packages/sdk/src/cli-resolver.ts +155 -0
- package/packages/sdk/src/index.ts +2 -0
- package/packages/sdk/src/spawn-from-env.ts +6 -17
- 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 +48 -4
- 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 +305 -0
- package/packages/sdk/src/workflows/default-logger.ts +120 -0
- package/packages/sdk/src/workflows/index.ts +4 -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 +138 -71
- 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,120 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import type { WorkflowEvent, WorkflowEventListener } from './runner.js';
|
|
3
|
+
|
|
4
|
+
export type LogLevel = 'verbose' | 'normal' | 'quiet' | false;
|
|
5
|
+
|
|
6
|
+
const noop: WorkflowEventListener = () => {};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a default event logger that writes workflow progress to the console.
|
|
10
|
+
*
|
|
11
|
+
* @param level - Log verbosity: "verbose" | "normal" (default) | "quiet" | false (no-op)
|
|
12
|
+
*/
|
|
13
|
+
export function createDefaultEventLogger(level: LogLevel = 'normal'): WorkflowEventListener {
|
|
14
|
+
if (level === false) return noop;
|
|
15
|
+
|
|
16
|
+
return (event: WorkflowEvent) => {
|
|
17
|
+
switch (event.type) {
|
|
18
|
+
// ── Run lifecycle ──
|
|
19
|
+
case 'run:started':
|
|
20
|
+
if (level !== 'quiet') {
|
|
21
|
+
console.log(chalk.cyan(`[workflow] run ${event.runId.slice(0, 8)}...`));
|
|
22
|
+
}
|
|
23
|
+
break;
|
|
24
|
+
|
|
25
|
+
case 'run:completed':
|
|
26
|
+
console.log(chalk.green(`[workflow] completed`));
|
|
27
|
+
break;
|
|
28
|
+
|
|
29
|
+
case 'run:failed':
|
|
30
|
+
console.log(chalk.red(`[workflow] FAILED: ${event.error}`));
|
|
31
|
+
break;
|
|
32
|
+
|
|
33
|
+
case 'run:cancelled':
|
|
34
|
+
if (level !== 'quiet') {
|
|
35
|
+
console.log(chalk.yellow(`[workflow] cancelled`));
|
|
36
|
+
}
|
|
37
|
+
break;
|
|
38
|
+
|
|
39
|
+
// ── Step lifecycle ──
|
|
40
|
+
case 'step:started':
|
|
41
|
+
if (level !== 'quiet') {
|
|
42
|
+
console.log(chalk.blue(` ● ${event.stepName} — started`));
|
|
43
|
+
}
|
|
44
|
+
break;
|
|
45
|
+
|
|
46
|
+
case 'step:completed':
|
|
47
|
+
if (level !== 'quiet') {
|
|
48
|
+
console.log(chalk.green(` ✓ ${event.stepName} — completed`));
|
|
49
|
+
}
|
|
50
|
+
break;
|
|
51
|
+
|
|
52
|
+
case 'step:failed':
|
|
53
|
+
console.log(chalk.red(` ✗ ${event.stepName} — FAILED: ${event.error}`));
|
|
54
|
+
break;
|
|
55
|
+
|
|
56
|
+
case 'step:skipped':
|
|
57
|
+
if (level !== 'quiet') {
|
|
58
|
+
console.log(chalk.gray(` ○ ${event.stepName} — skipped`));
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
|
|
62
|
+
case 'step:retrying':
|
|
63
|
+
if (level !== 'quiet') {
|
|
64
|
+
console.log(chalk.yellow(` ↻ ${event.stepName} — retrying (attempt ${event.attempt})`));
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
|
|
68
|
+
case 'step:nudged':
|
|
69
|
+
if (level !== 'quiet') {
|
|
70
|
+
console.log(chalk.yellow(` ⚡ ${event.stepName} — nudged (${event.nudgeCount})`));
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
|
|
74
|
+
case 'step:agent-report': {
|
|
75
|
+
if (level !== 'quiet') {
|
|
76
|
+
const r = event.report;
|
|
77
|
+
const parts: string[] = [];
|
|
78
|
+
if (r.model) parts.push(r.model);
|
|
79
|
+
if (r.cost != null) parts.push(`$${r.cost.toFixed(2)}`);
|
|
80
|
+
if (r.tokens) parts.push(`${r.tokens.input}+${r.tokens.output} tokens`);
|
|
81
|
+
parts.push(`${r.errors.length} errors`);
|
|
82
|
+
console.log(chalk.dim(` 📊 ${event.stepName} — ${parts.join(' · ')}`));
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Broker-level events (verbose only) ──
|
|
88
|
+
case 'broker:event':
|
|
89
|
+
if (level === 'verbose') {
|
|
90
|
+
console.log(chalk.dim(` [broker] ${JSON.stringify(event.event)}`));
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
|
|
94
|
+
// ── Other events (verbose only) ──
|
|
95
|
+
case 'step:owner-assigned':
|
|
96
|
+
if (level === 'verbose') {
|
|
97
|
+
console.log(chalk.dim(` ${event.stepName} — owner: ${event.ownerName}, specialist: ${event.specialistName}`));
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
|
|
101
|
+
case 'step:review-completed':
|
|
102
|
+
if (level === 'verbose') {
|
|
103
|
+
console.log(chalk.dim(` ${event.stepName} — review: ${event.decision} by ${event.reviewerName}`));
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
|
|
107
|
+
case 'step:owner-timeout':
|
|
108
|
+
if (level !== 'quiet') {
|
|
109
|
+
console.log(chalk.yellow(` ⏱ ${event.stepName} — owner timeout (${event.ownerName})`));
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
|
|
113
|
+
case 'step:force-released':
|
|
114
|
+
if (level === 'verbose') {
|
|
115
|
+
console.log(chalk.dim(` ${event.stepName} — force-released`));
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from './types.js';
|
|
2
2
|
export * from './runner.js';
|
|
3
3
|
export * from './custom-steps.js';
|
|
4
|
+
export * from './cli-session-collector.js';
|
|
5
|
+
export * from './run-summary-table.js';
|
|
4
6
|
export {
|
|
5
7
|
Models,
|
|
6
8
|
ClaudeModels,
|
|
@@ -22,3 +24,5 @@ export * from './state.js';
|
|
|
22
24
|
export * from './templates.js';
|
|
23
25
|
export { WorkflowTrajectory, type StepOutcome } from './trajectory.js';
|
|
24
26
|
export { formatDryRunReport } from './dry-run-format.js';
|
|
27
|
+
export { createWorkflowRenderer, type WorkflowRenderer } from './listr-renderer.js';
|
|
28
|
+
export { createDefaultEventLogger } from './default-logger.js';
|
|
@@ -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
|
+
}
|