@yeaft/webchat-agent 0.1.225 → 0.1.226
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/conductor/conductor-claude.js +148 -129
- package/conductor/index.js +12 -10
- package/conductor/persistence.js +172 -136
- package/conductor/session.js +118 -265
- package/conductor/ui-messages.js +53 -52
- package/conductor/worktree.js +89 -82
- package/conductor.js +21 -32
- package/connection/message-router.js +10 -32
- package/package.json +1 -1
package/conductor/session.js
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Conductor — Session
|
|
2
|
+
* Conductor — Session Core (V5 Singleton)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* - session 级元数据存在 ~/.claude/conductor/<sessionId>/
|
|
4
|
+
* Each Agent has exactly one Conductor (1:1 binding).
|
|
5
|
+
* No multi-session Map — just a single conductor instance.
|
|
6
|
+
*
|
|
7
|
+
* Conductor has no workDir; tasks bind to workDir.
|
|
9
8
|
*/
|
|
10
|
-
import { randomUUID } from 'crypto';
|
|
11
9
|
import ctx from '../context.js';
|
|
12
10
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
cleanupMessageShards
|
|
11
|
+
ensureConductorHome, loadConductorMeta, saveConductorMeta,
|
|
12
|
+
loadConductorMessages, loadState, getMaxShardIndex,
|
|
13
|
+
getConductorHome, cleanupMessageShards
|
|
17
14
|
} from './persistence.js';
|
|
18
15
|
import {
|
|
19
16
|
sendConductorMessage, sendConductorOutput, sendStatusUpdate, recordUserMessage
|
|
@@ -21,335 +18,191 @@ import {
|
|
|
21
18
|
import { createConductorClaude, sendToConductor, stopConductorClaude } from './conductor-claude.js';
|
|
22
19
|
|
|
23
20
|
// =====================================================================
|
|
24
|
-
//
|
|
21
|
+
// Singleton Conductor Instance
|
|
25
22
|
// =====================================================================
|
|
26
23
|
|
|
27
|
-
/** @type {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// =====================================================================
|
|
31
|
-
// Session Lifecycle
|
|
32
|
-
// =====================================================================
|
|
24
|
+
/** @type {Conductor|null} */
|
|
25
|
+
let conductor = null;
|
|
33
26
|
|
|
34
27
|
/**
|
|
35
|
-
*
|
|
28
|
+
* Get the current Conductor instance (or null if not initialized)
|
|
36
29
|
*/
|
|
37
|
-
export
|
|
38
|
-
|
|
39
|
-
sessionId = randomUUID(),
|
|
40
|
-
name = '',
|
|
41
|
-
workDir = null,
|
|
42
|
-
scenarioId = null,
|
|
43
|
-
userId,
|
|
44
|
-
username
|
|
45
|
-
} = msg;
|
|
46
|
-
|
|
47
|
-
const session = {
|
|
48
|
-
id: sessionId,
|
|
49
|
-
name: name || 'Conductor',
|
|
50
|
-
workDir,
|
|
51
|
-
scenarioId,
|
|
52
|
-
status: 'running',
|
|
53
|
-
tasks: new Map(),
|
|
54
|
-
conductorState: null,
|
|
55
|
-
costUsd: 0,
|
|
56
|
-
totalInputTokens: 0,
|
|
57
|
-
totalOutputTokens: 0,
|
|
58
|
-
activeClaudes: 0,
|
|
59
|
-
uiMessages: [],
|
|
60
|
-
userId,
|
|
61
|
-
username,
|
|
62
|
-
agentId: ctx.CONFIG?.agentName || null,
|
|
63
|
-
createdAt: Date.now(),
|
|
64
|
-
_rotating: false,
|
|
65
|
-
_conductorSemRelease: null
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
conductorSessions.set(sessionId, session);
|
|
69
|
-
|
|
70
|
-
// 初始化数据目录
|
|
71
|
-
await initSessionDataDir(sessionId);
|
|
72
|
-
|
|
73
|
-
// 通知前端
|
|
74
|
-
sendConductorMessage({
|
|
75
|
-
type: 'conductor_session_created',
|
|
76
|
-
sessionId,
|
|
77
|
-
name: session.name,
|
|
78
|
-
workDir: session.workDir,
|
|
79
|
-
scenarioId: session.scenarioId,
|
|
80
|
-
userId,
|
|
81
|
-
username
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
sendStatusUpdate(session);
|
|
85
|
-
|
|
86
|
-
// 注册到全局 index
|
|
87
|
-
await upsertConductorIndex(session);
|
|
88
|
-
await saveSessionMeta(session);
|
|
89
|
-
|
|
90
|
-
// 启动 Conductor Claude
|
|
91
|
-
try {
|
|
92
|
-
await createConductorClaude(session);
|
|
93
|
-
console.log(`[Conductor] Session ${sessionId} created, Claude ready`);
|
|
94
|
-
} catch (e) {
|
|
95
|
-
console.error('[Conductor] Failed to start Claude:', e.message);
|
|
96
|
-
sendConductorOutput(session, 'system', {
|
|
97
|
-
message: { role: 'assistant', content: `Conductor 启动失败: ${e.message}` }
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return session;
|
|
30
|
+
export function getConductor() {
|
|
31
|
+
return conductor;
|
|
102
32
|
}
|
|
103
33
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
export async function handleListConductorSessions(msg) {
|
|
108
|
-
const { requestId, _requestClientId } = msg;
|
|
109
|
-
const index = await loadConductorIndex();
|
|
110
|
-
|
|
111
|
-
const agentId = ctx.CONFIG?.agentName || null;
|
|
112
|
-
const filtered = agentId
|
|
113
|
-
? index.filter(e => !e.agentId || e.agentId === agentId)
|
|
114
|
-
: index;
|
|
115
|
-
|
|
116
|
-
// 更新活跃 session 的状态
|
|
117
|
-
for (const entry of filtered) {
|
|
118
|
-
const active = conductorSessions.get(entry.sessionId);
|
|
119
|
-
if (active) {
|
|
120
|
-
entry.status = active.status;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
ctx.sendToServer({
|
|
125
|
-
type: 'conductor_sessions_list',
|
|
126
|
-
requestId,
|
|
127
|
-
_requestClientId,
|
|
128
|
-
sessions: filtered.filter(e => !e.hidden)
|
|
129
|
-
});
|
|
130
|
-
}
|
|
34
|
+
// =====================================================================
|
|
35
|
+
// Conductor Lifecycle
|
|
36
|
+
// =====================================================================
|
|
131
37
|
|
|
132
38
|
/**
|
|
133
|
-
*
|
|
39
|
+
* Initialize or restore the single Conductor instance.
|
|
40
|
+
* Called when user opens the Conductor UI.
|
|
134
41
|
*/
|
|
135
|
-
export async function
|
|
136
|
-
const {
|
|
137
|
-
|
|
138
|
-
//
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const loaded = await loadSessionMessages(sessionId);
|
|
144
|
-
session.uiMessages = loaded.messages;
|
|
42
|
+
export async function initConductor(msg) {
|
|
43
|
+
const { userId, username } = msg;
|
|
44
|
+
|
|
45
|
+
// Already initialized in memory — just send current state
|
|
46
|
+
if (conductor) {
|
|
47
|
+
if (!conductor.uiMessages || conductor.uiMessages.length === 0) {
|
|
48
|
+
const loaded = await loadConductorMessages();
|
|
49
|
+
conductor.uiMessages = loaded.messages;
|
|
145
50
|
}
|
|
146
|
-
const cleaned = (
|
|
51
|
+
const cleaned = (conductor.uiMessages || []).map(m => {
|
|
147
52
|
const { _streaming, ...rest } = m;
|
|
148
53
|
return rest;
|
|
149
54
|
});
|
|
150
|
-
const dir =
|
|
55
|
+
const dir = getConductorHome();
|
|
151
56
|
const hasOlderMessages = await getMaxShardIndex(dir) > 0;
|
|
152
57
|
|
|
58
|
+
// Load state.json for task info
|
|
59
|
+
const state = await loadState();
|
|
60
|
+
|
|
153
61
|
sendConductorMessage({
|
|
154
|
-
type: '
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
scenarioId: session.scenarioId,
|
|
159
|
-
tasks: Array.from(session.tasks.values()),
|
|
160
|
-
userId: session.userId,
|
|
161
|
-
username: session.username,
|
|
62
|
+
type: 'conductor_opened',
|
|
63
|
+
tasks: state.tasks,
|
|
64
|
+
userId: conductor.userId,
|
|
65
|
+
username: conductor.username,
|
|
162
66
|
uiMessages: cleaned,
|
|
163
|
-
hasOlderMessages
|
|
67
|
+
hasOlderMessages,
|
|
68
|
+
status: conductor.status
|
|
164
69
|
});
|
|
165
|
-
sendStatusUpdate(
|
|
166
|
-
return;
|
|
70
|
+
sendStatusUpdate(conductor);
|
|
71
|
+
return conductor;
|
|
167
72
|
}
|
|
168
73
|
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
if (!meta) {
|
|
172
|
-
sendConductorMessage({
|
|
173
|
-
type: 'error', sessionId,
|
|
174
|
-
message: 'Conductor session not found'
|
|
175
|
-
});
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
74
|
+
// Ensure conductor home directory exists
|
|
75
|
+
await ensureConductorHome();
|
|
178
76
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
scenarioId: meta.scenarioId || null,
|
|
77
|
+
// Try restore from disk
|
|
78
|
+
const meta = await loadConductorMeta();
|
|
79
|
+
|
|
80
|
+
conductor = {
|
|
184
81
|
status: 'running',
|
|
185
|
-
tasks: new Map(
|
|
82
|
+
tasks: new Map(),
|
|
186
83
|
conductorState: null,
|
|
187
|
-
costUsd: meta
|
|
188
|
-
totalInputTokens: meta
|
|
189
|
-
totalOutputTokens: meta
|
|
84
|
+
costUsd: meta?.costUsd || 0,
|
|
85
|
+
totalInputTokens: meta?.totalInputTokens || 0,
|
|
86
|
+
totalOutputTokens: meta?.totalOutputTokens || 0,
|
|
190
87
|
activeClaudes: 0,
|
|
191
88
|
uiMessages: [],
|
|
192
|
-
userId: userId || meta
|
|
193
|
-
username: username || meta
|
|
194
|
-
|
|
195
|
-
createdAt: meta.createdAt || Date.now(),
|
|
89
|
+
userId: userId || meta?.userId,
|
|
90
|
+
username: username || meta?.username,
|
|
91
|
+
createdAt: meta?.createdAt || Date.now(),
|
|
196
92
|
_rotating: false,
|
|
197
93
|
_conductorSemRelease: null
|
|
198
94
|
};
|
|
199
95
|
|
|
200
|
-
|
|
96
|
+
// Load state.json to populate tasks map
|
|
97
|
+
const state = await loadState();
|
|
98
|
+
for (const [taskId, entry] of Object.entries(state.tasks || {})) {
|
|
99
|
+
conductor.tasks.set(taskId, entry);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Load UI messages from disk
|
|
103
|
+
const loaded = await loadConductorMessages();
|
|
104
|
+
conductor.uiMessages = loaded.messages;
|
|
201
105
|
|
|
202
|
-
const
|
|
203
|
-
session.uiMessages = loaded.messages;
|
|
106
|
+
const isResume = !!meta;
|
|
204
107
|
|
|
205
108
|
sendConductorMessage({
|
|
206
|
-
type: '
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
uiMessages: session.uiMessages,
|
|
215
|
-
hasOlderMessages: loaded.hasOlderMessages
|
|
109
|
+
type: 'conductor_opened',
|
|
110
|
+
tasks: state.tasks,
|
|
111
|
+
userId: conductor.userId,
|
|
112
|
+
username: conductor.username,
|
|
113
|
+
uiMessages: conductor.uiMessages,
|
|
114
|
+
hasOlderMessages: loaded.hasOlderMessages,
|
|
115
|
+
status: conductor.status,
|
|
116
|
+
resumed: isResume
|
|
216
117
|
});
|
|
217
|
-
sendStatusUpdate(
|
|
118
|
+
sendStatusUpdate(conductor);
|
|
218
119
|
|
|
219
|
-
//
|
|
120
|
+
// Start Conductor Claude
|
|
220
121
|
try {
|
|
221
|
-
await createConductorClaude(
|
|
222
|
-
console.log(`[Conductor]
|
|
122
|
+
await createConductorClaude(conductor);
|
|
123
|
+
console.log(`[Conductor] ${isResume ? 'Resumed' : 'Created'}, Claude ready`);
|
|
223
124
|
} catch (e) {
|
|
224
|
-
console.error('[Conductor] Failed to
|
|
125
|
+
console.error('[Conductor] Failed to start Claude:', e.message);
|
|
126
|
+
sendConductorOutput(conductor, 'system', {
|
|
127
|
+
message: { role: 'assistant', content: `Conductor 启动失败: ${e.message}` }
|
|
128
|
+
});
|
|
225
129
|
}
|
|
226
130
|
|
|
227
|
-
await
|
|
228
|
-
|
|
131
|
+
await saveConductorMeta(conductor);
|
|
132
|
+
return conductor;
|
|
229
133
|
}
|
|
230
134
|
|
|
231
135
|
/**
|
|
232
|
-
*
|
|
136
|
+
* Handle user input to Conductor
|
|
233
137
|
*/
|
|
234
138
|
export async function handleConductorUserInput(msg) {
|
|
235
|
-
const {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
console.warn(`[Conductor] Session not found: ${sessionId}`);
|
|
139
|
+
const { content } = msg;
|
|
140
|
+
if (!conductor) {
|
|
141
|
+
console.warn('[Conductor] Not initialized, ignoring input');
|
|
239
142
|
return;
|
|
240
143
|
}
|
|
241
144
|
|
|
242
|
-
|
|
243
|
-
if (session.status === 'stopped') {
|
|
145
|
+
if (conductor.status === 'stopped') {
|
|
244
146
|
sendConductorMessage({
|
|
245
147
|
type: 'conductor_error',
|
|
246
|
-
|
|
247
|
-
error: 'Session is stopped'
|
|
148
|
+
error: 'Conductor is stopped'
|
|
248
149
|
});
|
|
249
150
|
return;
|
|
250
151
|
}
|
|
251
152
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
session.status = 'running';
|
|
256
|
-
|
|
257
|
-
// 发给 Conductor Claude
|
|
258
|
-
await sendToConductor(session, content);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* 更新工作路径
|
|
263
|
-
*/
|
|
264
|
-
export async function handleUpdateWorkDir(msg) {
|
|
265
|
-
const { sessionId, workDir } = msg;
|
|
266
|
-
const session = conductorSessions.get(sessionId);
|
|
267
|
-
if (!session) return;
|
|
268
|
-
|
|
269
|
-
session.workDir = workDir;
|
|
270
|
-
console.log(`[Conductor] Session ${sessionId} workDir updated: ${workDir}`);
|
|
271
|
-
|
|
272
|
-
sendConductorMessage({
|
|
273
|
-
type: 'conductor_workdir_updated',
|
|
274
|
-
sessionId,
|
|
275
|
-
workDir
|
|
276
|
-
});
|
|
277
|
-
sendStatusUpdate(session);
|
|
278
|
-
await saveSessionMeta(session);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* 更新 session name
|
|
283
|
-
*/
|
|
284
|
-
export async function handleUpdateConductorSession(msg) {
|
|
285
|
-
const { sessionId, name, workDir } = msg;
|
|
286
|
-
const session = conductorSessions.get(sessionId);
|
|
287
|
-
if (!session) return;
|
|
288
|
-
|
|
289
|
-
if (name !== undefined) session.name = name;
|
|
290
|
-
if (workDir !== undefined) session.workDir = workDir;
|
|
291
|
-
await saveSessionMeta(session);
|
|
292
|
-
await upsertConductorIndex(session);
|
|
153
|
+
recordUserMessage(conductor, content);
|
|
154
|
+
conductor.status = 'running';
|
|
155
|
+
await sendToConductor(conductor, content);
|
|
293
156
|
}
|
|
294
157
|
|
|
295
158
|
/**
|
|
296
|
-
*
|
|
159
|
+
* Stop Conductor
|
|
297
160
|
*/
|
|
298
|
-
export async function
|
|
299
|
-
|
|
300
|
-
if (!session) return;
|
|
301
|
-
|
|
302
|
-
session.status = 'stopped';
|
|
161
|
+
export async function stopConductor() {
|
|
162
|
+
if (!conductor) return;
|
|
303
163
|
|
|
304
|
-
|
|
305
|
-
await stopConductorClaude(
|
|
164
|
+
conductor.status = 'stopped';
|
|
165
|
+
await stopConductorClaude(conductor);
|
|
306
166
|
|
|
307
|
-
sendConductorOutput(
|
|
308
|
-
message: { role: 'assistant', content: '
|
|
167
|
+
sendConductorOutput(conductor, 'system', {
|
|
168
|
+
message: { role: 'assistant', content: 'Conductor 已停止' }
|
|
309
169
|
});
|
|
310
|
-
sendStatusUpdate(
|
|
311
|
-
await
|
|
312
|
-
await upsertConductorIndex(session);
|
|
170
|
+
sendStatusUpdate(conductor);
|
|
171
|
+
await saveConductorMeta(conductor);
|
|
313
172
|
|
|
314
|
-
|
|
315
|
-
console.log(
|
|
173
|
+
conductor = null;
|
|
174
|
+
console.log('[Conductor] Stopped');
|
|
316
175
|
}
|
|
317
176
|
|
|
318
177
|
/**
|
|
319
|
-
*
|
|
178
|
+
* Clear Conductor (reset messages, restart Claude)
|
|
320
179
|
*/
|
|
321
|
-
export async function
|
|
322
|
-
|
|
323
|
-
if (!session) return;
|
|
180
|
+
export async function clearConductor() {
|
|
181
|
+
if (!conductor) return;
|
|
324
182
|
|
|
325
|
-
|
|
326
|
-
await stopConductorClaude(session);
|
|
183
|
+
await stopConductorClaude(conductor);
|
|
327
184
|
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
session.totalOutputTokens = 0;
|
|
185
|
+
// Clear data but keep tasks
|
|
186
|
+
conductor.uiMessages = [];
|
|
187
|
+
conductor.costUsd = 0;
|
|
188
|
+
conductor.totalInputTokens = 0;
|
|
189
|
+
conductor.totalOutputTokens = 0;
|
|
334
190
|
|
|
335
|
-
const dir =
|
|
191
|
+
const dir = getConductorHome();
|
|
336
192
|
await cleanupMessageShards(dir);
|
|
337
193
|
|
|
338
|
-
|
|
194
|
+
conductor.status = 'running';
|
|
339
195
|
|
|
340
|
-
sendConductorMessage({
|
|
341
|
-
|
|
342
|
-
sessionId
|
|
343
|
-
});
|
|
344
|
-
sendStatusUpdate(session);
|
|
196
|
+
sendConductorMessage({ type: 'conductor_cleared' });
|
|
197
|
+
sendStatusUpdate(conductor);
|
|
345
198
|
|
|
346
|
-
//
|
|
199
|
+
// Restart Claude
|
|
347
200
|
try {
|
|
348
|
-
await createConductorClaude(
|
|
201
|
+
await createConductorClaude(conductor);
|
|
349
202
|
} catch (e) {
|
|
350
203
|
console.error('[Conductor] Failed to restart Claude after clear:', e.message);
|
|
351
204
|
}
|
|
352
205
|
|
|
353
|
-
await
|
|
354
|
-
console.log(
|
|
206
|
+
await saveConductorMeta(conductor);
|
|
207
|
+
console.log('[Conductor] Cleared');
|
|
355
208
|
}
|
package/conductor/ui-messages.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Conductor — UI
|
|
2
|
+
* Conductor — UI Message Helpers (V5)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Sends WebSocket messages to server/frontend.
|
|
5
|
+
* Maintains uiMessages persistence list on the conductor instance.
|
|
6
|
+
*
|
|
7
|
+
* No sessionId — Conductor is a singleton per Agent.
|
|
6
8
|
*/
|
|
7
9
|
import ctx from '../context.js';
|
|
8
|
-
import {
|
|
10
|
+
import { saveConductorMeta } from './persistence.js';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
|
-
*
|
|
13
|
+
* Send conductor message to server (forwarded to Web frontend)
|
|
12
14
|
*/
|
|
13
15
|
export function sendConductorMessage(msg) {
|
|
14
16
|
if (ctx.sendToServer) {
|
|
@@ -17,19 +19,18 @@ export function sendConductorMessage(msg) {
|
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
|
-
*
|
|
22
|
+
* Send Conductor Claude output to frontend
|
|
21
23
|
* outputType: 'text' | 'tool_use' | 'tool_result' | 'system' | 'task_created' | 'task_forwarded'
|
|
22
24
|
*/
|
|
23
|
-
export function sendConductorOutput(
|
|
25
|
+
export function sendConductorOutput(conductor, outputType, rawMessage, extra = {}) {
|
|
24
26
|
sendConductorMessage({
|
|
25
27
|
type: 'conductor_output',
|
|
26
|
-
sessionId: session.id,
|
|
27
28
|
outputType,
|
|
28
29
|
data: rawMessage,
|
|
29
30
|
...extra
|
|
30
31
|
});
|
|
31
32
|
|
|
32
|
-
//
|
|
33
|
+
// Record trimmed UI messages
|
|
33
34
|
if (outputType === 'text') {
|
|
34
35
|
const content = rawMessage?.message?.content;
|
|
35
36
|
let text = '';
|
|
@@ -39,10 +40,9 @@ export function sendConductorOutput(session, outputType, rawMessage, extra = {})
|
|
|
39
40
|
text = content.filter(b => b.type === 'text').map(b => b.text).join('');
|
|
40
41
|
}
|
|
41
42
|
if (!text) return;
|
|
42
|
-
// 反向查找 streaming 消息
|
|
43
43
|
let found = false;
|
|
44
|
-
for (let i =
|
|
45
|
-
const msg =
|
|
44
|
+
for (let i = conductor.uiMessages.length - 1; i >= 0; i--) {
|
|
45
|
+
const msg = conductor.uiMessages[i];
|
|
46
46
|
if (msg.source === 'conductor' && msg.type === 'text' && msg._streaming) {
|
|
47
47
|
msg.content += text;
|
|
48
48
|
found = true;
|
|
@@ -50,7 +50,7 @@ export function sendConductorOutput(session, outputType, rawMessage, extra = {})
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
if (!found) {
|
|
53
|
-
|
|
53
|
+
conductor.uiMessages.push({
|
|
54
54
|
source: 'conductor', type: 'text', content: text,
|
|
55
55
|
_streaming: true, timestamp: Date.now()
|
|
56
56
|
});
|
|
@@ -61,25 +61,25 @@ export function sendConductorOutput(session, outputType, rawMessage, extra = {})
|
|
|
61
61
|
if (typeof content === 'string') text = content;
|
|
62
62
|
else if (Array.isArray(content)) text = content.filter(b => b.type === 'text').map(b => b.text).join('');
|
|
63
63
|
if (!text) return;
|
|
64
|
-
|
|
64
|
+
conductor.uiMessages.push({
|
|
65
65
|
source: 'conductor', type: 'system', content: text, timestamp: Date.now()
|
|
66
66
|
});
|
|
67
67
|
} else if (outputType === 'task_created') {
|
|
68
|
-
|
|
68
|
+
conductor.uiMessages.push({
|
|
69
69
|
source: 'conductor', type: 'task_created',
|
|
70
70
|
taskId: extra.taskId, taskTitle: extra.taskTitle,
|
|
71
71
|
content: `Created task: ${extra.taskTitle}`,
|
|
72
72
|
timestamp: Date.now()
|
|
73
73
|
});
|
|
74
74
|
} else if (outputType === 'task_forwarded') {
|
|
75
|
-
|
|
75
|
+
conductor.uiMessages.push({
|
|
76
76
|
source: 'conductor', type: 'task_forwarded',
|
|
77
77
|
taskId: extra.taskId,
|
|
78
78
|
content: `Forwarded message to task: ${extra.taskId}`,
|
|
79
79
|
timestamp: Date.now()
|
|
80
80
|
});
|
|
81
81
|
} else if (outputType === 'tool_use') {
|
|
82
|
-
endConductorStreaming(
|
|
82
|
+
endConductorStreaming(conductor);
|
|
83
83
|
const content = rawMessage?.message?.content;
|
|
84
84
|
if (Array.isArray(content)) {
|
|
85
85
|
for (const block of content) {
|
|
@@ -89,7 +89,7 @@ export function sendConductorOutput(session, outputType, rawMessage, extra = {})
|
|
|
89
89
|
if (input.file_path) trimmedInput.file_path = input.file_path;
|
|
90
90
|
if (input.command) trimmedInput.command = input.command.substring(0, 200);
|
|
91
91
|
if (input.pattern) trimmedInput.pattern = input.pattern;
|
|
92
|
-
|
|
92
|
+
conductor.uiMessages.push({
|
|
93
93
|
source: 'conductor', type: 'tool',
|
|
94
94
|
toolName: block.name, toolId: block.id,
|
|
95
95
|
toolInput: Object.keys(trimmedInput).length > 0 ? trimmedInput : null,
|
|
@@ -103,9 +103,9 @@ export function sendConductorOutput(session, outputType, rawMessage, extra = {})
|
|
|
103
103
|
} else if (outputType === 'tool_result') {
|
|
104
104
|
const toolId = rawMessage?.message?.tool_use_id;
|
|
105
105
|
if (toolId) {
|
|
106
|
-
for (let i =
|
|
107
|
-
if (
|
|
108
|
-
|
|
106
|
+
for (let i = conductor.uiMessages.length - 1; i >= 0; i--) {
|
|
107
|
+
if (conductor.uiMessages[i].type === 'tool' && conductor.uiMessages[i].toolId === toolId) {
|
|
108
|
+
conductor.uiMessages[i].hasResult = true;
|
|
109
109
|
break;
|
|
110
110
|
}
|
|
111
111
|
}
|
|
@@ -114,56 +114,57 @@ export function sendConductorOutput(session, outputType, rawMessage, extra = {})
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
|
-
*
|
|
117
|
+
* End Conductor streaming message
|
|
118
118
|
*/
|
|
119
|
-
export function endConductorStreaming(
|
|
120
|
-
for (let i =
|
|
121
|
-
if (
|
|
122
|
-
delete
|
|
119
|
+
export function endConductorStreaming(conductor) {
|
|
120
|
+
for (let i = conductor.uiMessages.length - 1; i >= 0; i--) {
|
|
121
|
+
if (conductor.uiMessages[i].source === 'conductor' && conductor.uiMessages[i]._streaming) {
|
|
122
|
+
delete conductor.uiMessages[i]._streaming;
|
|
123
123
|
break;
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
/**
|
|
129
|
-
*
|
|
129
|
+
* Record user message to uiMessages
|
|
130
130
|
*/
|
|
131
|
-
export function recordUserMessage(
|
|
132
|
-
|
|
131
|
+
export function recordUserMessage(conductor, content) {
|
|
132
|
+
conductor.uiMessages.push({
|
|
133
133
|
source: 'user', type: 'text', content,
|
|
134
134
|
timestamp: Date.now()
|
|
135
135
|
});
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
/**
|
|
139
|
-
*
|
|
139
|
+
* Send conductor status update to frontend
|
|
140
140
|
*/
|
|
141
|
-
export function sendStatusUpdate(
|
|
142
|
-
const tasks =
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
141
|
+
export function sendStatusUpdate(conductor) {
|
|
142
|
+
const tasks = {};
|
|
143
|
+
for (const [taskId, t] of conductor.tasks) {
|
|
144
|
+
tasks[taskId] = {
|
|
145
|
+
taskId: t.taskId,
|
|
146
|
+
title: t.title,
|
|
147
|
+
workDir: t.workDir,
|
|
148
|
+
scenario: t.scenario,
|
|
149
|
+
status: t.status,
|
|
150
|
+
activeActors: t.activeActors || [],
|
|
151
|
+
currentStep: t.currentStep || '',
|
|
152
|
+
lastUpdate: t.lastUpdate || t.updatedAt
|
|
153
|
+
};
|
|
154
|
+
}
|
|
153
155
|
|
|
154
156
|
sendConductorMessage({
|
|
155
157
|
type: 'conductor_status',
|
|
156
|
-
|
|
157
|
-
status: session.status,
|
|
158
|
-
workDir: session.workDir,
|
|
158
|
+
status: conductor.status,
|
|
159
159
|
tasks,
|
|
160
|
-
costUsd:
|
|
161
|
-
totalInputTokens:
|
|
162
|
-
totalOutputTokens:
|
|
163
|
-
activeClaudes:
|
|
160
|
+
costUsd: conductor.costUsd,
|
|
161
|
+
totalInputTokens: conductor.totalInputTokens,
|
|
162
|
+
totalOutputTokens: conductor.totalOutputTokens,
|
|
163
|
+
activeClaudes: conductor.activeClaudes || 0
|
|
164
164
|
});
|
|
165
165
|
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
166
|
+
// Async persist
|
|
167
|
+
saveConductorMeta(conductor).catch(e =>
|
|
168
|
+
console.warn('[Conductor] Failed to save meta:', e.message)
|
|
169
|
+
);
|
|
169
170
|
}
|