claude-dashboard 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +10 -0
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/README.zh-TW.md +99 -0
- package/bin/cdb.ts +60 -0
- package/bun.lock +1612 -0
- package/bunfig.toml +4 -0
- package/components.json +20 -0
- package/next.config.ts +19 -0
- package/package.json +62 -0
- package/postcss.config.mjs +9 -0
- package/prompts/pm-system.md +61 -0
- package/prompts/rd-system.md +68 -0
- package/prompts/sec-system.md +93 -0
- package/prompts/test-system.md +71 -0
- package/prompts/ui-system.md +72 -0
- package/server.ts +118 -0
- package/sql.js.d.ts +33 -0
- package/src/__tests__/api/usage/route.test.ts +193 -0
- package/src/__tests__/components/layout/TopNav.test.tsx +155 -0
- package/src/__tests__/components/layout/UsageIndicator.test.tsx +503 -0
- package/src/__tests__/hooks/useUsage.test.tsx +174 -0
- package/src/__tests__/lib/usage/get-token.test.ts +117 -0
- package/src/__tests__/react-sanity.test.tsx +14 -0
- package/src/__tests__/sanity.test.ts +7 -0
- package/src/__tests__/setup.ts +1 -0
- package/src/app/api/health/route.ts +8 -0
- package/src/app/api/usage/route.ts +86 -0
- package/src/app/api/workflows/[id]/route.ts +17 -0
- package/src/app/api/workflows/route.ts +14 -0
- package/src/app/globals.css +74 -0
- package/src/app/history/page.tsx +15 -0
- package/src/app/layout.tsx +24 -0
- package/src/app/page.tsx +112 -0
- package/src/components/agent/AgentCard.tsx +117 -0
- package/src/components/agent/AgentCardGrid.tsx +14 -0
- package/src/components/agent/AgentOutput.tsx +87 -0
- package/src/components/agent/AgentStatusBadge.tsx +20 -0
- package/src/components/events/EventLog.tsx +65 -0
- package/src/components/events/EventLogItem.tsx +39 -0
- package/src/components/history/HistoryTable.tsx +105 -0
- package/src/components/layout/DashboardShell.tsx +12 -0
- package/src/components/layout/TopNav.tsx +86 -0
- package/src/components/layout/UsageIndicator.tsx +163 -0
- package/src/components/pipeline/PipelineBar.tsx +59 -0
- package/src/components/pipeline/PipelineNode.tsx +55 -0
- package/src/components/terminal/TerminalPanel.tsx +138 -0
- package/src/components/terminal/XTermRenderer.tsx +129 -0
- package/src/components/ui/badge.tsx +37 -0
- package/src/components/ui/button.tsx +55 -0
- package/src/components/ui/card.tsx +80 -0
- package/src/components/ui/input.tsx +26 -0
- package/src/components/ui/scroll-area.tsx +52 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/textarea.tsx +25 -0
- package/src/components/ui/tooltip.tsx +73 -0
- package/src/components/workflow/WorkflowLauncher.tsx +102 -0
- package/src/hooks/useAgentStream.ts +27 -0
- package/src/hooks/useAutoScroll.ts +24 -0
- package/src/hooks/useUsage.ts +66 -0
- package/src/hooks/useWebSocket.ts +289 -0
- package/src/lib/agents/prompts.ts +341 -0
- package/src/lib/db/connection.ts +263 -0
- package/src/lib/db/queries.ts +257 -0
- package/src/lib/db/schema.ts +39 -0
- package/src/lib/output-buffer.ts +41 -0
- package/src/lib/terminal/pty-manager.ts +106 -0
- package/src/lib/usage/get-token.ts +48 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/websocket/connection-manager.ts +71 -0
- package/src/lib/websocket/protocol.ts +90 -0
- package/src/lib/websocket/server.ts +231 -0
- package/src/lib/workflow/agent-runner.ts +254 -0
- package/src/lib/workflow/context-builder.ts +62 -0
- package/src/lib/workflow/engine.ts +310 -0
- package/src/lib/workflow/pipeline.ts +28 -0
- package/src/lib/workflow/types.ts +111 -0
- package/src/stores/agentStore.ts +152 -0
- package/src/stores/eventStore.ts +35 -0
- package/src/stores/terminalStore.ts +20 -0
- package/src/stores/uiStore.ts +35 -0
- package/src/stores/workflowStore.ts +57 -0
- package/src/types/css.d.ts +4 -0
- package/src/types/index.ts +12 -0
- package/tailwind.config.ts +65 -0
- package/tsconfig.json +25 -0
- package/tsconfig.server.json +21 -0
- package/vitest.config.ts +25 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
2
|
+
import { IncomingMessage } from 'http';
|
|
3
|
+
import { ConnectionManager } from './connection-manager.ts';
|
|
4
|
+
import { WorkflowEngine } from '../workflow/engine.ts';
|
|
5
|
+
import { PtyManager } from '../terminal/pty-manager.ts';
|
|
6
|
+
|
|
7
|
+
export function setupWebSocketHandlers(
|
|
8
|
+
wss: WebSocketServer,
|
|
9
|
+
connectionManager: ConnectionManager,
|
|
10
|
+
engine: WorkflowEngine,
|
|
11
|
+
ptyManager: PtyManager,
|
|
12
|
+
projectPath: string
|
|
13
|
+
) {
|
|
14
|
+
// Wire workflow engine events to WebSocket broadcasts
|
|
15
|
+
engine.on('workflow:created', (workflowId: string, title: string) => {
|
|
16
|
+
connectionManager.broadcastAll({
|
|
17
|
+
type: 'workflow:created',
|
|
18
|
+
payload: { workflowId, title },
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
engine.on('workflow:completed', (workflowId: string) => {
|
|
23
|
+
connectionManager.broadcastToWorkflow(workflowId, {
|
|
24
|
+
type: 'workflow:completed',
|
|
25
|
+
payload: { workflowId },
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
engine.on('workflow:failed', (workflowId: string, error: string) => {
|
|
30
|
+
connectionManager.broadcastToWorkflow(workflowId, {
|
|
31
|
+
type: 'workflow:failed',
|
|
32
|
+
payload: { workflowId, error },
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
engine.on('workflow:paused', (workflowId: string) => {
|
|
37
|
+
connectionManager.broadcastToWorkflow(workflowId, {
|
|
38
|
+
type: 'workflow:paused',
|
|
39
|
+
payload: { workflowId },
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
engine.on('workflow:cancelled', (workflowId: string) => {
|
|
44
|
+
connectionManager.broadcastToWorkflow(workflowId, {
|
|
45
|
+
type: 'workflow:cancelled',
|
|
46
|
+
payload: { workflowId },
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
engine.on('step:started', (workflowId: string, stepId: string, role: string) => {
|
|
51
|
+
connectionManager.broadcastToWorkflow(workflowId, {
|
|
52
|
+
type: 'step:started',
|
|
53
|
+
payload: { workflowId, stepId, role },
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
engine.on('step:stream', (workflowId: string, stepId: string, role: string, chunk: string) => {
|
|
58
|
+
connectionManager.broadcastToWorkflow(workflowId, {
|
|
59
|
+
type: 'step:stream',
|
|
60
|
+
payload: { workflowId, stepId, role, chunk },
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
engine.on('step:completed', (workflowId: string, stepId: string, role: string, output: string, durationMs: number, tokensIn?: number, tokensOut?: number) => {
|
|
65
|
+
connectionManager.broadcastToWorkflow(workflowId, {
|
|
66
|
+
type: 'step:completed',
|
|
67
|
+
payload: { workflowId, stepId, role, output, durationMs, tokensIn, tokensOut },
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
engine.on('step:failed', (workflowId: string, stepId: string, role: string, error: string) => {
|
|
72
|
+
connectionManager.broadcastToWorkflow(workflowId, {
|
|
73
|
+
type: 'step:failed',
|
|
74
|
+
payload: { workflowId, stepId, role, error },
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
engine.on('step:activity', (workflowId: string, stepId: string, role: string, activity: any) => {
|
|
79
|
+
connectionManager.broadcastToWorkflow(workflowId, {
|
|
80
|
+
type: 'step:activity',
|
|
81
|
+
payload: { workflowId, stepId, role, activity },
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
engine.on('step:retry', (workflowId: string, stepId: string, role: string, attempt: number, maxRetries: number, reason: string) => {
|
|
86
|
+
connectionManager.broadcastToWorkflow(workflowId, {
|
|
87
|
+
type: 'step:retry',
|
|
88
|
+
payload: { workflowId, stepId, role, attempt, maxRetries, reason },
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Handle new WebSocket connections
|
|
93
|
+
wss.on('connection', (ws: WebSocket, req: IncomingMessage) => {
|
|
94
|
+
const clientId = connectionManager.addClient(ws);
|
|
95
|
+
console.log(`[WS] Client connected: ${clientId}`);
|
|
96
|
+
|
|
97
|
+
ws.on('message', (raw: Buffer) => {
|
|
98
|
+
try {
|
|
99
|
+
const msg = JSON.parse(raw.toString());
|
|
100
|
+
handleClientMessage(clientId, ws, msg, connectionManager, engine, ptyManager, projectPath);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error('[WS] Invalid message:', err);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
ws.on('close', () => {
|
|
107
|
+
console.log(`[WS] Client disconnected: ${clientId}`);
|
|
108
|
+
connectionManager.removeClient(clientId);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
ws.on('error', (err) => {
|
|
112
|
+
console.error(`[WS] Client ${clientId} error:`, err.message);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function handleClientMessage(
|
|
118
|
+
clientId: string,
|
|
119
|
+
ws: WebSocket,
|
|
120
|
+
msg: any,
|
|
121
|
+
connectionManager: ConnectionManager,
|
|
122
|
+
engine: WorkflowEngine,
|
|
123
|
+
ptyManager: PtyManager,
|
|
124
|
+
defaultProjectPath: string
|
|
125
|
+
) {
|
|
126
|
+
switch (msg.type) {
|
|
127
|
+
case 'ping':
|
|
128
|
+
ws.send(JSON.stringify({ type: 'pong' }));
|
|
129
|
+
break;
|
|
130
|
+
|
|
131
|
+
case 'workflow:start': {
|
|
132
|
+
const { prompt, projectPath } = msg.payload || {};
|
|
133
|
+
if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
|
|
134
|
+
ws.send(JSON.stringify({
|
|
135
|
+
type: 'workflow:failed',
|
|
136
|
+
payload: { workflowId: null, error: 'Prompt is required' },
|
|
137
|
+
}));
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
const path = projectPath || defaultProjectPath;
|
|
141
|
+
engine.startWorkflow(prompt, path).then((workflowId) => {
|
|
142
|
+
connectionManager.subscribeToWorkflow(clientId, workflowId);
|
|
143
|
+
}).catch((err: any) => {
|
|
144
|
+
console.error('[WS] Failed to start workflow:', err);
|
|
145
|
+
ws.send(JSON.stringify({
|
|
146
|
+
type: 'workflow:failed',
|
|
147
|
+
payload: { workflowId: null, error: err.message },
|
|
148
|
+
}));
|
|
149
|
+
});
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
case 'workflow:subscribe': {
|
|
154
|
+
const { workflowId } = msg.payload || {};
|
|
155
|
+
if (workflowId) {
|
|
156
|
+
connectionManager.subscribeToWorkflow(clientId, workflowId);
|
|
157
|
+
}
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
case 'workflow:pause': {
|
|
162
|
+
const { workflowId } = msg.payload || {};
|
|
163
|
+
if (workflowId) engine.pauseWorkflow(workflowId);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
case 'workflow:resume': {
|
|
168
|
+
const { workflowId } = msg.payload || {};
|
|
169
|
+
if (workflowId) engine.resumeWorkflow(workflowId);
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case 'workflow:cancel': {
|
|
174
|
+
const { workflowId } = msg.payload || {};
|
|
175
|
+
if (workflowId) engine.cancelWorkflow(workflowId);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
case 'terminal:create': {
|
|
180
|
+
const { projectPath } = msg.payload || {};
|
|
181
|
+
const path = projectPath || defaultProjectPath;
|
|
182
|
+
try {
|
|
183
|
+
const terminalId = ptyManager.create(path, (data: string) => {
|
|
184
|
+
connectionManager.sendTo(clientId, {
|
|
185
|
+
type: 'terminal:output',
|
|
186
|
+
payload: { terminalId, data },
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
connectionManager.sendTo(clientId, {
|
|
190
|
+
type: 'terminal:created',
|
|
191
|
+
payload: { terminalId },
|
|
192
|
+
});
|
|
193
|
+
} catch (err: any) {
|
|
194
|
+
console.error('[WS] Failed to create terminal:', err.message);
|
|
195
|
+
connectionManager.sendTo(clientId, {
|
|
196
|
+
type: 'terminal:error',
|
|
197
|
+
payload: { error: err.message },
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
case 'terminal:input': {
|
|
204
|
+
const { terminalId, data } = msg.payload || {};
|
|
205
|
+
if (terminalId && data != null) {
|
|
206
|
+
ptyManager.write(terminalId, data);
|
|
207
|
+
}
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
case 'terminal:resize': {
|
|
212
|
+
const { terminalId, cols, rows } = msg.payload || {};
|
|
213
|
+
if (terminalId && cols && rows) {
|
|
214
|
+
ptyManager.resize(terminalId, cols, rows);
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
case 'terminal:close': {
|
|
220
|
+
const { terminalId } = msg.payload || {};
|
|
221
|
+
if (terminalId) {
|
|
222
|
+
ptyManager.kill(terminalId);
|
|
223
|
+
connectionManager.sendTo(clientId, {
|
|
224
|
+
type: 'terminal:closed',
|
|
225
|
+
payload: { terminalId },
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { spawn, ChildProcess } from 'child_process';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import { type AgentRole, type AgentActivity, AGENT_CONFIG } from './types.ts';
|
|
4
|
+
|
|
5
|
+
export interface AgentRunnerEvents {
|
|
6
|
+
stream: (chunk: string) => void;
|
|
7
|
+
activity: (activity: AgentActivity) => void;
|
|
8
|
+
result: (output: string, tokensIn?: number, tokensOut?: number) => void;
|
|
9
|
+
error: (error: string) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class AgentRunner extends EventEmitter {
|
|
13
|
+
private process: ChildProcess | null = null;
|
|
14
|
+
private killed = false;
|
|
15
|
+
private output = '';
|
|
16
|
+
private resultEmitted = false;
|
|
17
|
+
private role: AgentRole;
|
|
18
|
+
private activityTimer: ReturnType<typeof setTimeout> | null = null;
|
|
19
|
+
private hardTimeoutTimer: ReturnType<typeof setTimeout> | null = null;
|
|
20
|
+
|
|
21
|
+
constructor(role: AgentRole) {
|
|
22
|
+
super();
|
|
23
|
+
this.role = role;
|
|
24
|
+
// Prevent Node from throwing on unhandled 'error' events
|
|
25
|
+
this.on('error', () => {});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async run(prompt: string, systemPrompt: string, projectPath: string): Promise<void> {
|
|
29
|
+
const config = AGENT_CONFIG[this.role];
|
|
30
|
+
|
|
31
|
+
return new Promise<void>((resolve, reject) => {
|
|
32
|
+
// Guard against double resolve/reject
|
|
33
|
+
let settled = false;
|
|
34
|
+
const settle = (fn: () => void) => {
|
|
35
|
+
if (settled) return;
|
|
36
|
+
settled = true;
|
|
37
|
+
this.clearAllTimers();
|
|
38
|
+
fn();
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const args = [
|
|
42
|
+
'-p', prompt,
|
|
43
|
+
'--verbose',
|
|
44
|
+
'--output-format', 'stream-json',
|
|
45
|
+
'--max-turns', '50',
|
|
46
|
+
'--dangerously-skip-permissions',
|
|
47
|
+
'--include-partial-messages',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
if (systemPrompt) {
|
|
51
|
+
args.push('--system-prompt', systemPrompt);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Restrict tools based on role
|
|
55
|
+
const allowedTools = config.tools;
|
|
56
|
+
if (allowedTools.length > 0) {
|
|
57
|
+
args.push('--allowedTools', allowedTools.join(','));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.process = spawn('claude', args, {
|
|
61
|
+
cwd: projectPath,
|
|
62
|
+
env: { ...process.env },
|
|
63
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Close stdin so `claude -p` receives EOF and starts processing
|
|
67
|
+
this.process.stdin?.end();
|
|
68
|
+
|
|
69
|
+
// Hard timeout: 2x configured timeout, absolute upper bound
|
|
70
|
+
this.hardTimeoutTimer = setTimeout(() => {
|
|
71
|
+
this.kill();
|
|
72
|
+
const err = `Agent ${config.label} hard timeout after ${(config.timeoutMs * 2) / 1000}s`;
|
|
73
|
+
this.emit('error', err);
|
|
74
|
+
settle(() => reject(new Error(err)));
|
|
75
|
+
}, config.timeoutMs * 2);
|
|
76
|
+
|
|
77
|
+
// Activity-based timeout: resets on every stream event
|
|
78
|
+
this.resetActivityTimeout(settle, reject);
|
|
79
|
+
|
|
80
|
+
let buffer = '';
|
|
81
|
+
|
|
82
|
+
this.process.stdout?.on('data', (data: Buffer) => {
|
|
83
|
+
buffer += data.toString();
|
|
84
|
+
const lines = buffer.split('\n');
|
|
85
|
+
buffer = lines.pop() || '';
|
|
86
|
+
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
if (!line.trim()) continue;
|
|
89
|
+
try {
|
|
90
|
+
const event = JSON.parse(line);
|
|
91
|
+
this.handleStreamEvent(event);
|
|
92
|
+
} catch {
|
|
93
|
+
// Non-JSON output, treat as raw text — still counts as activity
|
|
94
|
+
this.resetActivityTimeout();
|
|
95
|
+
this.output += line + '\n';
|
|
96
|
+
this.emit('stream', line + '\n');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
this.process.stderr?.on('data', (data: Buffer) => {
|
|
102
|
+
const text = data.toString();
|
|
103
|
+
console.error(`[${config.label}] stderr:`, text.trim());
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
this.process.on('close', (code) => {
|
|
107
|
+
if (this.killed) {
|
|
108
|
+
settle(() => reject(new Error('Agent was killed')));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (code === 0) {
|
|
112
|
+
if (!this.resultEmitted) {
|
|
113
|
+
this.emit('result', this.output);
|
|
114
|
+
}
|
|
115
|
+
settle(() => resolve());
|
|
116
|
+
} else {
|
|
117
|
+
const err = `Agent ${config.label} exited with code ${code}`;
|
|
118
|
+
this.emit('error', err);
|
|
119
|
+
settle(() => reject(new Error(err)));
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
this.process.on('error', (err) => {
|
|
124
|
+
this.emit('error', err.message);
|
|
125
|
+
settle(() => reject(err));
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private handleStreamEvent(event: any) {
|
|
131
|
+
// Any JSON event from CLI means the agent is alive — reset inactivity timer
|
|
132
|
+
this.resetActivityTimeout();
|
|
133
|
+
|
|
134
|
+
switch (event.type) {
|
|
135
|
+
case 'stream_event': {
|
|
136
|
+
// Unwrap the inner event from stream_event wrapper
|
|
137
|
+
const inner = event.event;
|
|
138
|
+
if (!inner) break;
|
|
139
|
+
|
|
140
|
+
switch (inner.type) {
|
|
141
|
+
case 'content_block_start': {
|
|
142
|
+
const block = inner.content_block;
|
|
143
|
+
if (block?.type === 'thinking') {
|
|
144
|
+
this.emit('activity', { kind: 'thinking' });
|
|
145
|
+
} else if (block?.type === 'tool_use') {
|
|
146
|
+
this.emit('activity', { kind: 'tool_use', toolName: block.name || 'unknown' });
|
|
147
|
+
} else if (block?.type === 'text') {
|
|
148
|
+
this.emit('activity', { kind: 'text' });
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case 'content_block_delta': {
|
|
153
|
+
if (inner.delta?.type === 'text_delta' && inner.delta.text) {
|
|
154
|
+
this.output += inner.delta.text;
|
|
155
|
+
this.emit('stream', inner.delta.text);
|
|
156
|
+
}
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
case 'content_block_stop':
|
|
160
|
+
this.emit('activity', { kind: 'idle' });
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
case 'assistant':
|
|
167
|
+
// With --include-partial-messages, text is already captured via stream_event.
|
|
168
|
+
// This handler serves as fallback for edge cases where streaming missed content.
|
|
169
|
+
if (event.message?.content) {
|
|
170
|
+
const fullText = event.message.content
|
|
171
|
+
.filter((b: any) => b.type === 'text')
|
|
172
|
+
.map((b: any) => b.text)
|
|
173
|
+
.join('');
|
|
174
|
+
|
|
175
|
+
if (fullText && !this.output.includes(fullText.slice(0, 100))) {
|
|
176
|
+
// Streaming missed this content — add separator and append
|
|
177
|
+
if (this.output.length > 0) {
|
|
178
|
+
this.output += '\n\n';
|
|
179
|
+
this.emit('stream', '\n\n');
|
|
180
|
+
}
|
|
181
|
+
this.output += fullText;
|
|
182
|
+
this.emit('stream', fullText);
|
|
183
|
+
} else if (this.output.length > 0) {
|
|
184
|
+
// Content already captured via streaming — just add turn separator
|
|
185
|
+
this.output += '\n\n';
|
|
186
|
+
this.emit('stream', '\n\n');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
190
|
+
|
|
191
|
+
case 'result':
|
|
192
|
+
if (event.result && !this.resultEmitted) {
|
|
193
|
+
this.resultEmitted = true;
|
|
194
|
+
const tokensIn = event.usage?.input_tokens;
|
|
195
|
+
const tokensOut = event.usage?.output_tokens;
|
|
196
|
+
if (!this.output) {
|
|
197
|
+
this.output = typeof event.result === 'string'
|
|
198
|
+
? event.result
|
|
199
|
+
: JSON.stringify(event.result);
|
|
200
|
+
}
|
|
201
|
+
this.emit('result', this.output, tokensIn, tokensOut);
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
kill() {
|
|
208
|
+
this.killed = true;
|
|
209
|
+
this.clearAllTimers();
|
|
210
|
+
if (this.process && !this.process.killed) {
|
|
211
|
+
this.process.kill('SIGTERM');
|
|
212
|
+
// Force kill after 5s if process hasn't exited
|
|
213
|
+
setTimeout(() => {
|
|
214
|
+
if (this.process && this.process.exitCode === null) {
|
|
215
|
+
this.process.kill('SIGKILL');
|
|
216
|
+
}
|
|
217
|
+
}, 5000);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private settleRef: { settle: (fn: () => void) => void; reject: (err: Error) => void } | null = null;
|
|
222
|
+
|
|
223
|
+
private resetActivityTimeout(settle?: (fn: () => void) => void, reject?: (err: Error) => void) {
|
|
224
|
+
// Store settle/reject on first call (from run()), reuse on subsequent calls (from handleStreamEvent)
|
|
225
|
+
if (settle && reject) {
|
|
226
|
+
this.settleRef = { settle, reject };
|
|
227
|
+
}
|
|
228
|
+
if (this.activityTimer) clearTimeout(this.activityTimer);
|
|
229
|
+
const config = AGENT_CONFIG[this.role];
|
|
230
|
+
this.activityTimer = setTimeout(() => {
|
|
231
|
+
this.kill();
|
|
232
|
+
const err = `Agent ${config.label} timed out after ${config.timeoutMs / 1000}s of inactivity`;
|
|
233
|
+
this.emit('error', err);
|
|
234
|
+
if (this.settleRef) {
|
|
235
|
+
this.settleRef.settle(() => this.settleRef!.reject(new Error(err)));
|
|
236
|
+
}
|
|
237
|
+
}, config.timeoutMs);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private clearAllTimers() {
|
|
241
|
+
if (this.activityTimer) {
|
|
242
|
+
clearTimeout(this.activityTimer);
|
|
243
|
+
this.activityTimer = null;
|
|
244
|
+
}
|
|
245
|
+
if (this.hardTimeoutTimer) {
|
|
246
|
+
clearTimeout(this.hardTimeoutTimer);
|
|
247
|
+
this.hardTimeoutTimer = null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
getOutput(): string {
|
|
252
|
+
return this.output;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type AgentRole, AGENT_CONFIG, PIPELINE_STAGES, getStageForRole } from './types.ts';
|
|
2
|
+
|
|
3
|
+
export interface AgentContext {
|
|
4
|
+
role: AgentRole;
|
|
5
|
+
output: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build the prompt for an agent, including context from previous agents.
|
|
10
|
+
*/
|
|
11
|
+
export function buildAgentPrompt(
|
|
12
|
+
role: AgentRole,
|
|
13
|
+
userPrompt: string,
|
|
14
|
+
previousOutputs: AgentContext[],
|
|
15
|
+
projectPath: string
|
|
16
|
+
): string {
|
|
17
|
+
const parts: string[] = [];
|
|
18
|
+
|
|
19
|
+
parts.push(`# Project Path\n${projectPath}\n`);
|
|
20
|
+
parts.push(`# User Request\n${userPrompt}\n`);
|
|
21
|
+
|
|
22
|
+
if (previousOutputs.length > 0) {
|
|
23
|
+
parts.push('# Previous Agent Outputs\n');
|
|
24
|
+
for (const ctx of previousOutputs) {
|
|
25
|
+
const config = AGENT_CONFIG[ctx.role];
|
|
26
|
+
parts.push(`## ${config.label} Agent Output\n${ctx.output}\n`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const config = AGENT_CONFIG[role];
|
|
31
|
+
const currentStage = getStageForRole(role);
|
|
32
|
+
|
|
33
|
+
// Peers running in parallel within the same stage
|
|
34
|
+
const peers = currentStage.roles
|
|
35
|
+
.filter((r) => r !== role)
|
|
36
|
+
.map((r) => AGENT_CONFIG[r].label);
|
|
37
|
+
|
|
38
|
+
// Agents in later stages
|
|
39
|
+
const downstreamStages = PIPELINE_STAGES.filter(
|
|
40
|
+
(s) => s.index > currentStage.index
|
|
41
|
+
);
|
|
42
|
+
const downstreamLabels = downstreamStages.flatMap((s) =>
|
|
43
|
+
s.roles.map((r) => AGENT_CONFIG[r].label)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
parts.push(`# Your Role: ${config.label} Agent`);
|
|
47
|
+
if (peers.length > 0) {
|
|
48
|
+
parts.push(`You are running IN PARALLEL with: ${peers.join(', ')}`);
|
|
49
|
+
}
|
|
50
|
+
if (downstreamLabels.length > 0) {
|
|
51
|
+
parts.push(
|
|
52
|
+
`After this stage completes, the following agents will run: ${downstreamLabels.join(' → ')}`
|
|
53
|
+
);
|
|
54
|
+
} else {
|
|
55
|
+
parts.push('You are in the final stage of the pipeline.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
parts.push('# Important: Output Language');
|
|
59
|
+
parts.push('Respond in the SAME language as the "# User Request" section above. Do NOT switch to English just because the template is in English.');
|
|
60
|
+
|
|
61
|
+
return parts.join('\n\n');
|
|
62
|
+
}
|