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,82 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
6
|
+
|
|
7
|
+
import { CodexCollector } from '../../collectors/codex.js';
|
|
8
|
+
|
|
9
|
+
const tempDirs: string[] = [];
|
|
10
|
+
|
|
11
|
+
function makeTempDir(prefix: string): string {
|
|
12
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
13
|
+
tempDirs.push(dir);
|
|
14
|
+
return dir;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createCodexFixture(tempDir: string, cwd: string, createdAtSeconds: number) {
|
|
18
|
+
const statePath = path.join(tempDir, 'state_5.sqlite');
|
|
19
|
+
const historyPath = path.join(tempDir, 'history.jsonl');
|
|
20
|
+
const db = new DatabaseSync(statePath);
|
|
21
|
+
|
|
22
|
+
db.exec(`
|
|
23
|
+
CREATE TABLE threads (
|
|
24
|
+
id TEXT PRIMARY KEY,
|
|
25
|
+
cwd TEXT,
|
|
26
|
+
model_provider TEXT,
|
|
27
|
+
tokens_used INTEGER,
|
|
28
|
+
created_at INTEGER,
|
|
29
|
+
updated_at INTEGER
|
|
30
|
+
);
|
|
31
|
+
CREATE TABLE logs (
|
|
32
|
+
thread_id TEXT,
|
|
33
|
+
ts INTEGER,
|
|
34
|
+
level TEXT,
|
|
35
|
+
message TEXT,
|
|
36
|
+
line INTEGER
|
|
37
|
+
);
|
|
38
|
+
`);
|
|
39
|
+
|
|
40
|
+
db.prepare(
|
|
41
|
+
'INSERT INTO threads (id, cwd, model_provider, tokens_used, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
|
|
42
|
+
).run('thread-1', cwd, 'openai/gpt-5', 321, createdAtSeconds, createdAtSeconds + 3);
|
|
43
|
+
db.prepare(
|
|
44
|
+
'INSERT INTO logs (thread_id, ts, level, message, line) VALUES (?, ?, ?, ?, ?)',
|
|
45
|
+
).run('thread-1', createdAtSeconds + 1, 'error', 'Command failed: bad exit code', 12);
|
|
46
|
+
db.close();
|
|
47
|
+
|
|
48
|
+
writeFileSync(historyPath, `${JSON.stringify({ session_id: 'thread-1', ts: createdAtSeconds, text: 'history' })}\n`);
|
|
49
|
+
|
|
50
|
+
return { statePath, historyPath };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
while (tempDirs.length > 0) {
|
|
55
|
+
rmSync(tempDirs.pop()!, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('CodexCollector', () => {
|
|
60
|
+
it('matches by cwd and time window and extracts errors from logs', async () => {
|
|
61
|
+
const tempDir = makeTempDir('codex-fixture-');
|
|
62
|
+
const cwd = '/repo/codex-project';
|
|
63
|
+
const createdAtSeconds = 100;
|
|
64
|
+
const { statePath, historyPath } = createCodexFixture(tempDir, cwd, createdAtSeconds);
|
|
65
|
+
const collector = new CodexCollector({ statePath, historyPath });
|
|
66
|
+
|
|
67
|
+
const report = await collector.collect({
|
|
68
|
+
cli: 'codex',
|
|
69
|
+
cwd,
|
|
70
|
+
startedAt: 100_000,
|
|
71
|
+
completedAt: 105_000,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(report).not.toBeNull();
|
|
75
|
+
expect(report?.sessionId).toBe('thread-1');
|
|
76
|
+
expect(report?.provider).toBe('openai');
|
|
77
|
+
expect(report?.model).toBe('gpt-5');
|
|
78
|
+
expect(report?.tokens).toEqual({ input: 321, output: 0, cacheRead: 0 });
|
|
79
|
+
expect(report?.errors).toEqual([{ turn: 1, text: 'Command failed: bad exit code' }]);
|
|
80
|
+
expect(report?.finalStatus).toBe('failed');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
6
|
+
|
|
7
|
+
const tempDirs: string[] = [];
|
|
8
|
+
const originalHome = process.env.HOME;
|
|
9
|
+
|
|
10
|
+
function makeTempDir(prefix: string): string {
|
|
11
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
12
|
+
tempDirs.push(dir);
|
|
13
|
+
return dir;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createOpenCodeFixture(homeDir: string, cwd: string, sessionCreatedAt: number): string {
|
|
17
|
+
const dbDir = path.join(homeDir, '.local', 'share', 'opencode');
|
|
18
|
+
mkdirSync(dbDir, { recursive: true });
|
|
19
|
+
const dbPath = path.join(dbDir, 'opencode.db');
|
|
20
|
+
const db = new DatabaseSync(dbPath);
|
|
21
|
+
|
|
22
|
+
db.exec(`
|
|
23
|
+
CREATE TABLE session (id TEXT PRIMARY KEY, directory TEXT, time_created INTEGER);
|
|
24
|
+
CREATE TABLE message (id TEXT PRIMARY KEY, session_id TEXT, time_created INTEGER, data TEXT);
|
|
25
|
+
CREATE TABLE part (id TEXT PRIMARY KEY, message_id TEXT, session_id TEXT, time_created INTEGER, data TEXT);
|
|
26
|
+
`);
|
|
27
|
+
|
|
28
|
+
const insertSession = db.prepare('INSERT INTO session (id, directory, time_created) VALUES (?, ?, ?)');
|
|
29
|
+
const insertMessage = db.prepare('INSERT INTO message (id, session_id, time_created, data) VALUES (?, ?, ?, ?)');
|
|
30
|
+
const insertPart = db.prepare('INSERT INTO part (id, message_id, session_id, time_created, data) VALUES (?, ?, ?, ?, ?)');
|
|
31
|
+
|
|
32
|
+
insertSession.run('session-1', cwd, sessionCreatedAt);
|
|
33
|
+
insertSession.run('session-2', '/other/project', sessionCreatedAt + 1000);
|
|
34
|
+
|
|
35
|
+
insertMessage.run(
|
|
36
|
+
'msg-1',
|
|
37
|
+
'session-1',
|
|
38
|
+
sessionCreatedAt + 10,
|
|
39
|
+
JSON.stringify({ role: 'user', tokens: { input: 10, output: 0, cache: { read: 1 } } }),
|
|
40
|
+
);
|
|
41
|
+
insertMessage.run(
|
|
42
|
+
'msg-2',
|
|
43
|
+
'session-1',
|
|
44
|
+
sessionCreatedAt + 20,
|
|
45
|
+
JSON.stringify({
|
|
46
|
+
role: 'assistant',
|
|
47
|
+
modelID: 'gpt-5',
|
|
48
|
+
providerID: 'openai',
|
|
49
|
+
finish: 'error',
|
|
50
|
+
cost: 1.25,
|
|
51
|
+
tokens: { input: 15, output: 20, cache: { read: 4 } },
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
insertMessage.run(
|
|
55
|
+
'msg-other',
|
|
56
|
+
'session-2',
|
|
57
|
+
sessionCreatedAt + 30,
|
|
58
|
+
JSON.stringify({ role: 'assistant', modelID: 'ignore-me', providerID: 'other', finish: 'completed' }),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
insertPart.run(
|
|
62
|
+
'part-1',
|
|
63
|
+
'msg-1',
|
|
64
|
+
'session-1',
|
|
65
|
+
sessionCreatedAt + 11,
|
|
66
|
+
JSON.stringify({ type: 'text', text: 'Planning work' }),
|
|
67
|
+
);
|
|
68
|
+
insertPart.run(
|
|
69
|
+
'part-2',
|
|
70
|
+
'msg-2',
|
|
71
|
+
'session-1',
|
|
72
|
+
sessionCreatedAt + 21,
|
|
73
|
+
JSON.stringify({ type: 'tool_call', name: 'write_file' }),
|
|
74
|
+
);
|
|
75
|
+
insertPart.run(
|
|
76
|
+
'part-3',
|
|
77
|
+
'msg-2',
|
|
78
|
+
'session-1',
|
|
79
|
+
sessionCreatedAt + 22,
|
|
80
|
+
JSON.stringify({ type: 'text', text: 'Error: database locked\nCleanup afterwards' }),
|
|
81
|
+
);
|
|
82
|
+
insertPart.run(
|
|
83
|
+
'part-4',
|
|
84
|
+
'msg-2',
|
|
85
|
+
'session-1',
|
|
86
|
+
sessionCreatedAt + 23,
|
|
87
|
+
JSON.stringify({ type: 'text', text: 'Completed summary output' }),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
db.close();
|
|
91
|
+
return dbPath;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function importCollectorWithHome(homeDir: string) {
|
|
95
|
+
process.env.HOME = homeDir;
|
|
96
|
+
vi.resetModules();
|
|
97
|
+
vi.doMock('node:module', () => ({
|
|
98
|
+
createRequire: () => (id: string) => {
|
|
99
|
+
if (id !== 'better-sqlite3') {
|
|
100
|
+
throw new Error(`Unexpected module request: ${id}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return class BetterSqliteCompat {
|
|
104
|
+
private readonly db: DatabaseSync;
|
|
105
|
+
|
|
106
|
+
constructor(filename: string) {
|
|
107
|
+
this.db = new DatabaseSync(filename, { open: true, readOnly: true });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
prepare(sql: string) {
|
|
111
|
+
const statement = this.db.prepare(sql);
|
|
112
|
+
return {
|
|
113
|
+
get<T>(params?: unknown): T | undefined {
|
|
114
|
+
return statement.get(params as never) as T | undefined;
|
|
115
|
+
},
|
|
116
|
+
all<T>(params?: unknown): T[] {
|
|
117
|
+
return statement.all(params as never) as T[];
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
pragma(_source: string) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
close() {
|
|
127
|
+
this.db.close();
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
}));
|
|
132
|
+
const module = await import('../../collectors/opencode.js');
|
|
133
|
+
return module.OpenCodeCollector;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
afterEach(() => {
|
|
137
|
+
vi.resetModules();
|
|
138
|
+
process.env.HOME = originalHome;
|
|
139
|
+
while (tempDirs.length > 0) {
|
|
140
|
+
rmSync(tempDirs.pop()!, { recursive: true, force: true });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('OpenCodeCollector', () => {
|
|
145
|
+
it('matches by directory and time window, aggregates tokens, and extracts errors', async () => {
|
|
146
|
+
const homeDir = makeTempDir('opencode-home-');
|
|
147
|
+
const cwd = path.join(homeDir, 'workspace');
|
|
148
|
+
const sessionCreatedAt = 10_000;
|
|
149
|
+
createOpenCodeFixture(homeDir, cwd, sessionCreatedAt);
|
|
150
|
+
const OpenCodeCollector = await importCollectorWithHome(homeDir);
|
|
151
|
+
|
|
152
|
+
const collector = new OpenCodeCollector();
|
|
153
|
+
const report = await collector.collect({
|
|
154
|
+
cli: 'opencode',
|
|
155
|
+
cwd,
|
|
156
|
+
startedAt: sessionCreatedAt + 100,
|
|
157
|
+
completedAt: sessionCreatedAt + 500,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(report).not.toBeNull();
|
|
161
|
+
expect(report?.sessionId).toBe('session-1');
|
|
162
|
+
expect(report?.model).toBe('gpt-5');
|
|
163
|
+
expect(report?.provider).toBe('openai');
|
|
164
|
+
expect(report?.tokens).toEqual({ input: 25, output: 20, cacheRead: 5 });
|
|
165
|
+
expect(report?.cost).toBe(1.25);
|
|
166
|
+
expect(report?.toolCalls).toEqual([{ name: 'write_file', count: 1 }]);
|
|
167
|
+
expect(report?.errors).toEqual([{ turn: 3, text: 'Error: database locked' }]);
|
|
168
|
+
expect(report?.finalStatus).toBe('failed');
|
|
169
|
+
expect(report?.summary).toBe('Completed summary output');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('returns false from canCollect when the database is missing', async () => {
|
|
173
|
+
const homeDir = makeTempDir('opencode-missing-home-');
|
|
174
|
+
const OpenCodeCollector = await importCollectorWithHome(homeDir);
|
|
175
|
+
|
|
176
|
+
expect(new OpenCodeCollector().canCollect()).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { formatRunSummaryTable } from '../run-summary-table.js';
|
|
4
|
+
|
|
5
|
+
vi.mock('@relaycast/sdk', () => ({
|
|
6
|
+
RelayCast: vi.fn(),
|
|
7
|
+
RelayError: class RelayError extends Error {},
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock('../../relay.js', () => ({
|
|
11
|
+
AgentRelay: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const { WorkflowRunner } = await import('../runner.js');
|
|
15
|
+
|
|
16
|
+
describe('formatRunSummaryTable', () => {
|
|
17
|
+
it('renders all-passing steps', () => {
|
|
18
|
+
const output = formatRunSummaryTable(
|
|
19
|
+
[
|
|
20
|
+
{ name: 'plan', agent: 'lead', status: 'completed', attempts: 1, durationMs: 1_000 },
|
|
21
|
+
{ name: 'implement', agent: 'worker', status: 'completed', attempts: 1, durationMs: 2_000 },
|
|
22
|
+
],
|
|
23
|
+
new Map([
|
|
24
|
+
[
|
|
25
|
+
'plan',
|
|
26
|
+
{
|
|
27
|
+
cli: 'claude',
|
|
28
|
+
sessionId: 's1',
|
|
29
|
+
model: 'claude-sonnet-4',
|
|
30
|
+
provider: 'anthropic',
|
|
31
|
+
durationMs: 1_200,
|
|
32
|
+
cost: 0.75,
|
|
33
|
+
tokens: { input: 100, output: 50, cacheRead: 10 },
|
|
34
|
+
turns: 2,
|
|
35
|
+
toolCalls: [],
|
|
36
|
+
errors: [],
|
|
37
|
+
finalStatus: 'completed',
|
|
38
|
+
summary: 'planned',
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
[
|
|
42
|
+
'implement',
|
|
43
|
+
{
|
|
44
|
+
cli: 'codex',
|
|
45
|
+
sessionId: 's2',
|
|
46
|
+
model: 'gpt-5',
|
|
47
|
+
provider: 'openai',
|
|
48
|
+
durationMs: 3_400,
|
|
49
|
+
cost: 1.25,
|
|
50
|
+
tokens: { input: 300, output: 90, cacheRead: 20 },
|
|
51
|
+
turns: 4,
|
|
52
|
+
toolCalls: [],
|
|
53
|
+
errors: [{ turn: 2, text: 'Error: recovered after retry' }],
|
|
54
|
+
finalStatus: 'completed',
|
|
55
|
+
summary: 'implemented',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
]),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(output).toMatchInlineSnapshot(`
|
|
62
|
+
" Step Status Model Cost Tokens Duration Errors
|
|
63
|
+
plan pass claude-sonnet-4 $0.75 160 1s --
|
|
64
|
+
implement pass gpt-5 $1.25 410 3s 1 (fixed)
|
|
65
|
+
────────────────────────────────────────────────────────────────────────────────────────────
|
|
66
|
+
Total $2.00 570 5s "
|
|
67
|
+
`);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('renders a failed step with the first error line', () => {
|
|
71
|
+
const output = formatRunSummaryTable(
|
|
72
|
+
[{ name: 'broken-step', agent: 'worker', status: 'failed', attempts: 1, durationMs: 1_500, error: 'boom' }],
|
|
73
|
+
new Map([
|
|
74
|
+
[
|
|
75
|
+
'broken-step',
|
|
76
|
+
{
|
|
77
|
+
cli: 'opencode',
|
|
78
|
+
sessionId: 's3',
|
|
79
|
+
model: 'gpt-5',
|
|
80
|
+
provider: 'openai',
|
|
81
|
+
durationMs: 1_500,
|
|
82
|
+
cost: 0.01,
|
|
83
|
+
tokens: { input: 10, output: 5, cacheRead: 0 },
|
|
84
|
+
turns: 1,
|
|
85
|
+
toolCalls: [],
|
|
86
|
+
errors: [{ turn: 1, text: 'Error: database locked' }],
|
|
87
|
+
finalStatus: 'failed',
|
|
88
|
+
summary: null,
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
]),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(output).toContain('broken-step FAIL');
|
|
95
|
+
expect(output).toContain(' └─ Error [turn 1] Error: database locked');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('renders deterministic steps without reports using placeholder columns', () => {
|
|
99
|
+
const output = formatRunSummaryTable(
|
|
100
|
+
[{ name: 'lint', agent: 'shell', status: 'completed', attempts: 1, durationMs: 900 }],
|
|
101
|
+
new Map(),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(output).toContain('lint pass --');
|
|
105
|
+
expect(output).toContain('--');
|
|
106
|
+
// No reports means no cost column
|
|
107
|
+
expect(output).not.toContain('Cost');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('hides Cost column when no report has reliable cost data', () => {
|
|
111
|
+
const output = formatRunSummaryTable(
|
|
112
|
+
[
|
|
113
|
+
{ name: 'gen-code', agent: 'worker', status: 'completed', attempts: 1, durationMs: 5_000 },
|
|
114
|
+
],
|
|
115
|
+
new Map([
|
|
116
|
+
[
|
|
117
|
+
'gen-code',
|
|
118
|
+
{
|
|
119
|
+
cli: 'claude',
|
|
120
|
+
sessionId: 's1',
|
|
121
|
+
model: 'claude-sonnet-4',
|
|
122
|
+
provider: 'anthropic',
|
|
123
|
+
durationMs: 5_000,
|
|
124
|
+
cost: null,
|
|
125
|
+
tokens: { input: 200, output: 80, cacheRead: 0 },
|
|
126
|
+
turns: 3,
|
|
127
|
+
toolCalls: [],
|
|
128
|
+
errors: [],
|
|
129
|
+
finalStatus: 'completed',
|
|
130
|
+
summary: 'done',
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
]),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
expect(output).not.toContain('Cost');
|
|
137
|
+
expect(output).toContain('Tokens');
|
|
138
|
+
expect(output).toContain('280');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('WorkflowRunner logRunSummary', () => {
|
|
143
|
+
it('falls back to the legacy summary format when no reports exist', () => {
|
|
144
|
+
const runner = new WorkflowRunner({ cwd: '/tmp/workflow-runner' });
|
|
145
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
146
|
+
|
|
147
|
+
(runner as any).logRunSummary(
|
|
148
|
+
'sample-workflow',
|
|
149
|
+
[{ name: 'lint', agent: 'shell', status: 'completed', attempts: 1, output: 'ok' }],
|
|
150
|
+
'run-1',
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const combined = logSpy.mock.calls.flat().join('\n');
|
|
154
|
+
expect(combined).toContain('Workflow "sample-workflow"');
|
|
155
|
+
expect(combined).toContain('✓ lint [shell]');
|
|
156
|
+
expect(combined).not.toContain('Step Status');
|
|
157
|
+
|
|
158
|
+
logSpy.mockRestore();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
vi.mock('@relaycast/sdk', () => ({
|
|
5
|
+
RelayCast: vi.fn(),
|
|
6
|
+
RelayError: class RelayError extends Error {},
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
vi.mock('../../relay.js', () => ({
|
|
10
|
+
AgentRelay: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const { WorkflowRunner } = await import('../runner.js');
|
|
14
|
+
|
|
15
|
+
describe('WorkflowRunner step cwd resolution', () => {
|
|
16
|
+
it('prefers step.cwd over agent.cwd and runner cwd', () => {
|
|
17
|
+
const runnerRoot = '/runner-root';
|
|
18
|
+
const runner = new WorkflowRunner({ cwd: runnerRoot });
|
|
19
|
+
|
|
20
|
+
const resolved = (runner as any).resolveEffectiveCwd(
|
|
21
|
+
{ name: 'generate', agent: 'worker', task: 'Generate', cwd: 'steps/generate' },
|
|
22
|
+
{ name: 'worker', cli: 'claude', cwd: 'agents/worker' },
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
expect(resolved).toBe(path.resolve(runnerRoot, 'steps/generate'));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('respects step.cwd for deterministic steps', () => {
|
|
29
|
+
const runnerRoot = '/runner-root';
|
|
30
|
+
const runner = new WorkflowRunner({ cwd: runnerRoot });
|
|
31
|
+
|
|
32
|
+
const resolved = (runner as any).resolveEffectiveCwd({
|
|
33
|
+
name: 'scaffold',
|
|
34
|
+
type: 'deterministic',
|
|
35
|
+
command: 'mkdir -p out',
|
|
36
|
+
cwd: 'deterministic/setup',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(resolved).toBe(path.resolve(runnerRoot, 'deterministic/setup'));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('falls back through step.cwd to step.workdir to agent.cwd to runner.cwd', () => {
|
|
43
|
+
const runnerRoot = '/runner-root';
|
|
44
|
+
const namedPath = '/named/workdir';
|
|
45
|
+
const runner = new WorkflowRunner({ cwd: runnerRoot });
|
|
46
|
+
(runner as any).resolvedPaths.set('generated', namedPath);
|
|
47
|
+
|
|
48
|
+
const agentDef = { name: 'worker', cli: 'claude', cwd: 'agents/worker' } as const;
|
|
49
|
+
|
|
50
|
+
expect(
|
|
51
|
+
(runner as any).resolveEffectiveCwd(
|
|
52
|
+
{ name: 's1', agent: 'worker', task: 'Do work', cwd: 'steps/explicit', workdir: 'generated' },
|
|
53
|
+
agentDef,
|
|
54
|
+
),
|
|
55
|
+
).toBe(path.resolve(runnerRoot, 'steps/explicit'));
|
|
56
|
+
|
|
57
|
+
expect(
|
|
58
|
+
(runner as any).resolveEffectiveCwd(
|
|
59
|
+
{ name: 's2', agent: 'worker', task: 'Do work', workdir: 'generated' },
|
|
60
|
+
agentDef,
|
|
61
|
+
),
|
|
62
|
+
).toBe(namedPath);
|
|
63
|
+
|
|
64
|
+
expect(
|
|
65
|
+
(runner as any).resolveEffectiveCwd({ name: 's3', agent: 'worker', task: 'Do work' }, agentDef),
|
|
66
|
+
).toBe(path.resolve(runnerRoot, 'agents/worker'));
|
|
67
|
+
|
|
68
|
+
expect(
|
|
69
|
+
(runner as any).resolveEffectiveCwd({ name: 's4', type: 'deterministic', command: 'pwd' }),
|
|
70
|
+
).toBe(runnerRoot);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
} from './types.js';
|
|
23
23
|
import { WorkflowRunner, type WorkflowEventListener, type VariableContext, type StepExecutor } from './runner.js';
|
|
24
24
|
import { formatDryRunReport } from './dry-run-format.js';
|
|
25
|
+
import { createDefaultEventLogger, type LogLevel } from './default-logger.js';
|
|
25
26
|
|
|
26
27
|
// ── Option types for the builder API ────────────────────────────────────────
|
|
27
28
|
|
|
@@ -47,6 +48,7 @@ export interface AgentOptions {
|
|
|
47
48
|
export interface AgentStepOptions {
|
|
48
49
|
agent: string;
|
|
49
50
|
task: string;
|
|
51
|
+
cwd?: string;
|
|
50
52
|
dependsOn?: string[];
|
|
51
53
|
verification?: VerificationCheck;
|
|
52
54
|
timeoutMs?: number;
|
|
@@ -57,6 +59,7 @@ export interface AgentStepOptions {
|
|
|
57
59
|
export interface DeterministicStepOptions {
|
|
58
60
|
type: 'deterministic';
|
|
59
61
|
command: string;
|
|
62
|
+
cwd?: string;
|
|
60
63
|
/** Capture stdout as step output for downstream steps. Default: true. */
|
|
61
64
|
captureOutput?: boolean;
|
|
62
65
|
/** Fail if command exit code is non-zero. Default: true. */
|
|
@@ -104,6 +107,10 @@ export interface WorkflowRunOptions {
|
|
|
104
107
|
startFrom?: string;
|
|
105
108
|
/** Previous run ID whose cached outputs are used with startFrom. */
|
|
106
109
|
previousRunId?: string;
|
|
110
|
+
/** Console log verbosity: "verbose" | "normal" (default) | "quiet" | false (silent). */
|
|
111
|
+
logLevel?: LogLevel;
|
|
112
|
+
/** Renderer: "listr" for listr2 UI, "default" for console logger, false to disable. */
|
|
113
|
+
renderer?: 'listr' | 'default' | false;
|
|
107
114
|
}
|
|
108
115
|
|
|
109
116
|
// ── WorkflowBuilder ─────────────────────────────────────────────────────────
|
|
@@ -256,6 +263,7 @@ export class WorkflowBuilder {
|
|
|
256
263
|
}
|
|
257
264
|
step.type = 'deterministic';
|
|
258
265
|
step.command = options.command;
|
|
266
|
+
if (options.cwd !== undefined) step.cwd = options.cwd;
|
|
259
267
|
if (options.captureOutput !== undefined) step.captureOutput = options.captureOutput;
|
|
260
268
|
if (options.failOnError !== undefined) step.failOnError = options.failOnError;
|
|
261
269
|
if (options.dependsOn !== undefined) step.dependsOn = options.dependsOn;
|
|
@@ -280,6 +288,7 @@ export class WorkflowBuilder {
|
|
|
280
288
|
}
|
|
281
289
|
step.agent = agentOpts.agent;
|
|
282
290
|
step.task = agentOpts.task;
|
|
291
|
+
if (agentOpts.cwd !== undefined) step.cwd = agentOpts.cwd;
|
|
283
292
|
if (agentOpts.dependsOn !== undefined) step.dependsOn = agentOpts.dependsOn;
|
|
284
293
|
if (agentOpts.verification !== undefined) step.verification = agentOpts.verification;
|
|
285
294
|
if (agentOpts.timeoutMs !== undefined) step.timeoutMs = agentOpts.timeoutMs;
|
|
@@ -329,7 +338,11 @@ export class WorkflowBuilder {
|
|
|
329
338
|
if (this._timeoutMs !== undefined) config.swarm.timeoutMs = this._timeoutMs;
|
|
330
339
|
if (this._channel !== undefined) config.swarm.channel = this._channel;
|
|
331
340
|
if (this._idleNudge !== undefined) config.swarm.idleNudge = this._idleNudge;
|
|
332
|
-
|
|
341
|
+
config.errorHandling = this._errorHandling ?? {
|
|
342
|
+
strategy: 'retry',
|
|
343
|
+
maxRetries: 2,
|
|
344
|
+
retryDelayMs: 10_000,
|
|
345
|
+
};
|
|
333
346
|
if (this._coordination !== undefined) config.coordination = this._coordination;
|
|
334
347
|
if (this._state !== undefined) config.state = this._state;
|
|
335
348
|
if (this._trajectories !== undefined) config.trajectories = this._trajectories;
|
|
@@ -363,15 +376,23 @@ export class WorkflowBuilder {
|
|
|
363
376
|
return report;
|
|
364
377
|
}
|
|
365
378
|
|
|
379
|
+
// Wire up default console logger unless explicitly disabled
|
|
380
|
+
// renderer: "listr" owns the terminal — skip console logger to avoid garbled output
|
|
381
|
+
// renderer: false implies no output at all
|
|
382
|
+
const logLevel = options.renderer === 'listr' || options.renderer === false
|
|
383
|
+
? false
|
|
384
|
+
: (options.logLevel ?? 'normal');
|
|
385
|
+
if (logLevel !== false) {
|
|
386
|
+
runner.on(createDefaultEventLogger(logLevel));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Wire up user-provided event handler (additive — does not replace the default logger)
|
|
366
390
|
if (options.onEvent) {
|
|
367
391
|
runner.on(options.onEvent);
|
|
368
392
|
}
|
|
369
393
|
|
|
370
394
|
// Auto-detect RESUME_RUN_ID env var for resuming failed runs
|
|
371
395
|
const resumeRunId = process.env.RESUME_RUN_ID;
|
|
372
|
-
if (resumeRunId) {
|
|
373
|
-
return runner.resume(resumeRunId, options.vars);
|
|
374
|
-
}
|
|
375
396
|
|
|
376
397
|
const startFrom = this._startFrom ?? options.startFrom ?? process.env.START_FROM;
|
|
377
398
|
const previousRunId = this._previousRunId ?? options.previousRunId ?? process.env.PREVIOUS_RUN_ID;
|
|
@@ -379,6 +400,29 @@ export class WorkflowBuilder {
|
|
|
379
400
|
? { startFrom, previousRunId }
|
|
380
401
|
: undefined;
|
|
381
402
|
|
|
403
|
+
// If listr renderer requested, wire it up and run concurrently
|
|
404
|
+
// Must be set up BEFORE the resume check so resume runs also get event output
|
|
405
|
+
if (options.renderer === 'listr') {
|
|
406
|
+
const { createWorkflowRenderer } = await import('./listr-renderer.js');
|
|
407
|
+
const renderer = createWorkflowRenderer();
|
|
408
|
+
runner.on(renderer.onEvent);
|
|
409
|
+
|
|
410
|
+
const runPromise = resumeRunId
|
|
411
|
+
? runner.resume(resumeRunId, options.vars)
|
|
412
|
+
: runner.execute(config, options.workflow, options.vars, executeOptions);
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
const [result] = await Promise.all([runPromise, renderer.start()]);
|
|
416
|
+
return result;
|
|
417
|
+
} finally {
|
|
418
|
+
renderer.unmount();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (resumeRunId) {
|
|
423
|
+
return runner.resume(resumeRunId, options.vars);
|
|
424
|
+
}
|
|
425
|
+
|
|
382
426
|
return runner.execute(config, options.workflow, options.vars, executeOptions);
|
|
383
427
|
}
|
|
384
428
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { AgentCli } from './types.js';
|
|
2
|
+
import { ClaudeCodeCollector } from './collectors/claude.js';
|
|
3
|
+
import { CodexCollector } from './collectors/codex.js';
|
|
4
|
+
import { OpenCodeCollector } from './collectors/opencode.js';
|
|
5
|
+
|
|
6
|
+
export interface CliSessionReport {
|
|
7
|
+
cli: AgentCli;
|
|
8
|
+
sessionId: string | null;
|
|
9
|
+
model: string | null;
|
|
10
|
+
provider: string | null;
|
|
11
|
+
durationMs: number | null;
|
|
12
|
+
cost: number | null;
|
|
13
|
+
tokens: {
|
|
14
|
+
input: number;
|
|
15
|
+
output: number;
|
|
16
|
+
cacheRead: number;
|
|
17
|
+
} | null;
|
|
18
|
+
turns: number;
|
|
19
|
+
toolCalls: { name: string; count: number }[];
|
|
20
|
+
errors: { turn: number; text: string }[];
|
|
21
|
+
finalStatus: 'completed' | 'failed' | 'unknown';
|
|
22
|
+
summary: string | null;
|
|
23
|
+
raw?: object;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CliSessionQuery {
|
|
27
|
+
cli: AgentCli;
|
|
28
|
+
cwd: string;
|
|
29
|
+
startedAt: number;
|
|
30
|
+
completedAt: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CliSessionCollector {
|
|
34
|
+
canCollect(): boolean;
|
|
35
|
+
collect(query: CliSessionQuery): Promise<CliSessionReport | null>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createCollector(cli: AgentCli): CliSessionCollector | null {
|
|
39
|
+
switch (cli) {
|
|
40
|
+
case 'opencode':
|
|
41
|
+
return new OpenCodeCollector();
|
|
42
|
+
case 'claude':
|
|
43
|
+
return new ClaudeCodeCollector();
|
|
44
|
+
case 'codex':
|
|
45
|
+
return new CodexCollector();
|
|
46
|
+
default:
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function collectCliSession(query: CliSessionQuery): Promise<CliSessionReport | null> {
|
|
52
|
+
const collector = createCollector(query.cli);
|
|
53
|
+
if (!collector || !collector.canCollect()) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return collector.collect(query);
|
|
58
|
+
}
|