@yeaft/webchat-agent 0.1.234 → 0.1.235
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 +24 -0
- package/conductor/index.js +10 -1
- package/conductor/session.js +2 -0
- package/conductor/task-runner.js +287 -0
- package/conductor/ui-messages.js +8 -1
- package/package.json +1 -1
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
getConductorHome, ensureConductorHome,
|
|
22
22
|
loadState, saveState, updateTaskInState, initTaskDir
|
|
23
23
|
} from './persistence.js';
|
|
24
|
+
import { startTaskExecution } from './task-runner.js';
|
|
24
25
|
|
|
25
26
|
// =====================================================================
|
|
26
27
|
// Claude Session ID Persistence
|
|
@@ -191,6 +192,7 @@ export async function sendToConductor(conductor, content) {
|
|
|
191
192
|
|
|
192
193
|
state.turnActive = true;
|
|
193
194
|
state.accumulatedText = '';
|
|
195
|
+
state._lastUserContent = content; // Preserve original user input for task description
|
|
194
196
|
state.inputStream.enqueue({
|
|
195
197
|
type: 'user',
|
|
196
198
|
message: { role: 'user', content: fullContent }
|
|
@@ -328,6 +330,18 @@ async function processConductorOutput(conductor, conductorQuery) {
|
|
|
328
330
|
for (const ct of createTasks) {
|
|
329
331
|
const taskId = `task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
330
332
|
|
|
333
|
+
// Notify frontend: task is being created (worktree may take a few seconds)
|
|
334
|
+
sendConductorOutput(conductor, 'task_creating', null, {
|
|
335
|
+
taskId, taskTitle: ct.title
|
|
336
|
+
});
|
|
337
|
+
sendConductorMessage({
|
|
338
|
+
type: 'conductor_task_creating',
|
|
339
|
+
taskId,
|
|
340
|
+
title: ct.title,
|
|
341
|
+
workDir: ct.workDir,
|
|
342
|
+
scenario: ct.scenario
|
|
343
|
+
});
|
|
344
|
+
|
|
331
345
|
// Create task directory at {workDir}/.conductor/{taskId}/ (includes worktree)
|
|
332
346
|
let worktreePath = null;
|
|
333
347
|
if (ct.workDir) {
|
|
@@ -339,10 +353,15 @@ async function processConductorOutput(conductor, conductorQuery) {
|
|
|
339
353
|
}
|
|
340
354
|
}
|
|
341
355
|
|
|
356
|
+
// Extract user's original message as task description
|
|
357
|
+
// (the content that triggered this CREATE_TASK)
|
|
358
|
+
const userDescription = state._lastUserContent || ct.title;
|
|
359
|
+
|
|
342
360
|
// Register in state.json
|
|
343
361
|
const taskEntry = {
|
|
344
362
|
taskId,
|
|
345
363
|
title: ct.title,
|
|
364
|
+
description: userDescription,
|
|
346
365
|
workDir: ct.workDir,
|
|
347
366
|
scenario: ct.scenario,
|
|
348
367
|
status: 'created',
|
|
@@ -367,6 +386,11 @@ async function processConductorOutput(conductor, conductorQuery) {
|
|
|
367
386
|
});
|
|
368
387
|
|
|
369
388
|
console.log(`[Conductor] Task created: ${taskId} — ${ct.title} @ ${ct.workDir} (${ct.scenario})`);
|
|
389
|
+
|
|
390
|
+
// Start Orchestrator-driven execution (fire-and-forget)
|
|
391
|
+
startTaskExecution(conductor, taskEntry).catch(e =>
|
|
392
|
+
console.error(`[Conductor] Task execution failed for ${taskId}:`, e.message)
|
|
393
|
+
);
|
|
370
394
|
}
|
|
371
395
|
|
|
372
396
|
// Handle FORWARD_TASK
|
package/conductor/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Conductor — Module Entry
|
|
3
3
|
*
|
|
4
|
-
* Exports
|
|
4
|
+
* Exports core subsystems:
|
|
5
5
|
* 1. Personas — 18 persona definitions (4 scenarios)
|
|
6
6
|
* 2. Specialties — specialty behavior definitions
|
|
7
7
|
* 3. CLAUDE.md Generator — dynamic 3-section generation
|
|
8
8
|
* 4. Actor — Claude instance create/manage/release
|
|
9
9
|
* 5. Worktree — per-task worktree management
|
|
10
|
+
* 6. Task Runner — Orchestrator ↔ Actor bridge layer
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
// ---- Personas ----
|
|
@@ -63,3 +64,11 @@ export {
|
|
|
63
64
|
getTaskWorktreePath,
|
|
64
65
|
getActorCwd
|
|
65
66
|
} from './worktree.js';
|
|
67
|
+
|
|
68
|
+
// ---- Task Runner (Orchestrator ↔ Actor bridge) ----
|
|
69
|
+
export {
|
|
70
|
+
startTaskExecution,
|
|
71
|
+
stopTaskExecution,
|
|
72
|
+
stopAllTaskExecutions,
|
|
73
|
+
getOrchestrator
|
|
74
|
+
} from './task-runner.js';
|
package/conductor/session.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
sendConductorMessage, sendConductorOutput, sendStatusUpdate, recordUserMessage
|
|
17
17
|
} from './ui-messages.js';
|
|
18
18
|
import { createConductorClaude, sendToConductor, stopConductorClaude } from './conductor-claude.js';
|
|
19
|
+
import { stopAllTaskExecutions } from './task-runner.js';
|
|
19
20
|
|
|
20
21
|
// =====================================================================
|
|
21
22
|
// Singleton Conductor Instance
|
|
@@ -164,6 +165,7 @@ export async function stopConductor() {
|
|
|
164
165
|
if (!conductor) return;
|
|
165
166
|
|
|
166
167
|
conductor.status = 'stopped';
|
|
168
|
+
await stopAllTaskExecutions();
|
|
167
169
|
await stopConductorClaude(conductor);
|
|
168
170
|
|
|
169
171
|
sendConductorOutput(conductor, 'system', {
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conductor — Task Runner (Bridge Layer)
|
|
3
|
+
*
|
|
4
|
+
* Bridges the gap between Conductor Claude (task creation) and
|
|
5
|
+
* Orchestrator (state machine) + Actor (Claude instances).
|
|
6
|
+
*
|
|
7
|
+
* Responsibilities:
|
|
8
|
+
* 1. Create actorManager adapter that translates Orchestrator's interface
|
|
9
|
+
* to Actor module's actual parameter format
|
|
10
|
+
* 2. Instantiate Orchestrator and start task execution
|
|
11
|
+
* 3. Relay status changes and actor output to frontend via UI messages
|
|
12
|
+
* 4. Handle task completion / failure cleanup
|
|
13
|
+
*/
|
|
14
|
+
import { createOrchestrator } from './orchestrator.js';
|
|
15
|
+
import {
|
|
16
|
+
createActor, sendToActor, releaseActor, releaseAllActors
|
|
17
|
+
} from './actor.js';
|
|
18
|
+
import { getTaskDir, updateTaskInState } from './persistence.js';
|
|
19
|
+
import {
|
|
20
|
+
sendConductorMessage, sendConductorOutput, sendStatusUpdate
|
|
21
|
+
} from './ui-messages.js';
|
|
22
|
+
|
|
23
|
+
// =====================================================================
|
|
24
|
+
// Per-task Orchestrator registry
|
|
25
|
+
// =====================================================================
|
|
26
|
+
|
|
27
|
+
/** @type {Map<string, Orchestrator>} taskId → Orchestrator */
|
|
28
|
+
const orchestrators = new Map();
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get an active orchestrator for a task
|
|
32
|
+
* @param {string} taskId
|
|
33
|
+
* @returns {Orchestrator|null}
|
|
34
|
+
*/
|
|
35
|
+
export function getOrchestrator(taskId) {
|
|
36
|
+
return orchestrators.get(taskId) || null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// =====================================================================
|
|
40
|
+
// Read-write specialties (need worktree cwd)
|
|
41
|
+
// =====================================================================
|
|
42
|
+
|
|
43
|
+
const READ_WRITE_SPECIALTIES = new Set(['coding', 'testing', 'review']);
|
|
44
|
+
|
|
45
|
+
// =====================================================================
|
|
46
|
+
// ActorManager Adapter
|
|
47
|
+
// =====================================================================
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create an actorManager adapter that translates Orchestrator's
|
|
51
|
+
* createActor(persona, specialty, context) interface into Actor module's
|
|
52
|
+
* createActor({ personaId, specialtyId, ... }) format.
|
|
53
|
+
*
|
|
54
|
+
* Orchestrator calls:
|
|
55
|
+
* actorManager.createActor(
|
|
56
|
+
* { id: personaId, name: personaName },
|
|
57
|
+
* specialty, // string like 'coding', 'planning', etc.
|
|
58
|
+
* { taskId, actorInstanceId, instruction, workDir, memoryPath, ... }
|
|
59
|
+
* ) → Promise<ActorResult>
|
|
60
|
+
*
|
|
61
|
+
* Actor.js expects:
|
|
62
|
+
* createActor({
|
|
63
|
+
* personaId, specialtyId, taskId, taskDir, taskContext,
|
|
64
|
+
* worktreePath, onOutput, onComplete, onError
|
|
65
|
+
* }) → Promise<ActorInstance>
|
|
66
|
+
*
|
|
67
|
+
* The adapter wraps actor creation + message sending + result collection
|
|
68
|
+
* into a single Promise<ActorResult> that the Orchestrator awaits.
|
|
69
|
+
*/
|
|
70
|
+
function createActorManagerAdapter(conductor, taskEntry) {
|
|
71
|
+
const { taskId, workDir, worktreePath } = taskEntry;
|
|
72
|
+
const taskDir = getTaskDir(workDir, taskId);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
/**
|
|
76
|
+
* Create an actor, send it instructions, and wait for its result.
|
|
77
|
+
* @returns {Promise<ActorResult>}
|
|
78
|
+
*/
|
|
79
|
+
async createActor(persona, specialty, context) {
|
|
80
|
+
const actorWorktreePath = READ_WRITE_SPECIALTIES.has(specialty)
|
|
81
|
+
? worktreePath
|
|
82
|
+
: null;
|
|
83
|
+
|
|
84
|
+
// Create a deferred promise for actor completion
|
|
85
|
+
let resolveResult, rejectResult;
|
|
86
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
87
|
+
resolveResult = resolve;
|
|
88
|
+
rejectResult = reject;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const instance = await createActor({
|
|
92
|
+
personaId: persona.id,
|
|
93
|
+
specialtyId: specialty,
|
|
94
|
+
taskId,
|
|
95
|
+
taskDir,
|
|
96
|
+
taskContext: {
|
|
97
|
+
description: taskEntry.description || '',
|
|
98
|
+
instruction: context.instruction || context.description || '',
|
|
99
|
+
memoryPath: context.memoryPath || null,
|
|
100
|
+
planPath: context.planPath || null,
|
|
101
|
+
scenario: taskEntry.scenario,
|
|
102
|
+
workDir
|
|
103
|
+
},
|
|
104
|
+
threadId: context.threadId || null,
|
|
105
|
+
worktreePath: actorWorktreePath,
|
|
106
|
+
onOutput: (instanceId, message) => {
|
|
107
|
+
// Relay actor output to frontend in real-time
|
|
108
|
+
sendConductorOutput(conductor, 'text', message, {
|
|
109
|
+
taskId,
|
|
110
|
+
persona: persona.name || persona.id,
|
|
111
|
+
specialty
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
onComplete: (instanceId, result) => {
|
|
115
|
+
console.log(`[TaskRunner] Actor ${instanceId} completed for task ${taskId}`);
|
|
116
|
+
|
|
117
|
+
// Accumulate actor cost into conductor totals
|
|
118
|
+
if (result.costUsd) conductor.costUsd += result.costUsd;
|
|
119
|
+
if (result.inputTokens) conductor.totalInputTokens += result.inputTokens;
|
|
120
|
+
if (result.outputTokens) conductor.totalOutputTokens += result.outputTokens;
|
|
121
|
+
|
|
122
|
+
resolveResult(result);
|
|
123
|
+
},
|
|
124
|
+
onError: (instanceId, error) => {
|
|
125
|
+
console.error(`[TaskRunner] Actor ${instanceId} failed for task ${taskId}:`, error.message);
|
|
126
|
+
rejectResult(error);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Update conductor's active claude count
|
|
131
|
+
conductor.activeClaudes = (conductor.activeClaudes || 0) + 1;
|
|
132
|
+
|
|
133
|
+
// Update task entry's activeActors for status tracking
|
|
134
|
+
if (!taskEntry.activeActors) taskEntry.activeActors = [];
|
|
135
|
+
taskEntry.activeActors.push(instance.instanceId);
|
|
136
|
+
sendStatusUpdate(conductor);
|
|
137
|
+
|
|
138
|
+
// Send the instruction to the actor to start work
|
|
139
|
+
const instruction = context.instruction || context.description || taskEntry.description || '';
|
|
140
|
+
if (instruction) {
|
|
141
|
+
sendToActor(taskId, instance.instanceId, instruction);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return resultPromise;
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Release an actor instance.
|
|
149
|
+
*/
|
|
150
|
+
releaseActor(instanceId) {
|
|
151
|
+
releaseActor(taskId, instanceId).catch(e =>
|
|
152
|
+
console.warn(`[TaskRunner] Failed to release actor ${instanceId}:`, e.message)
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// Update tracking
|
|
156
|
+
if (taskEntry.activeActors) {
|
|
157
|
+
const idx = taskEntry.activeActors.indexOf(instanceId);
|
|
158
|
+
if (idx !== -1) taskEntry.activeActors.splice(idx, 1);
|
|
159
|
+
}
|
|
160
|
+
conductor.activeClaudes = Math.max(0, (conductor.activeClaudes || 1) - 1);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// =====================================================================
|
|
166
|
+
// Task Execution Entry Point
|
|
167
|
+
// =====================================================================
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Start task execution via Orchestrator.
|
|
171
|
+
*
|
|
172
|
+
* Called after initTaskDir completes (folder + worktree ready).
|
|
173
|
+
* This is fire-and-forget — the orchestrator runs asynchronously.
|
|
174
|
+
*
|
|
175
|
+
* @param {object} conductor — Conductor singleton instance
|
|
176
|
+
* @param {object} taskEntry — Task registry entry from conductor.tasks Map
|
|
177
|
+
*/
|
|
178
|
+
export async function startTaskExecution(conductor, taskEntry) {
|
|
179
|
+
const { taskId, title, workDir, scenario, description } = taskEntry;
|
|
180
|
+
|
|
181
|
+
console.log(`[TaskRunner] Starting execution for task ${taskId}: "${title}" [${scenario}]`);
|
|
182
|
+
|
|
183
|
+
// Update task status
|
|
184
|
+
taskEntry.status = 'running';
|
|
185
|
+
taskEntry.lastUpdate = Date.now();
|
|
186
|
+
|
|
187
|
+
// Create actorManager adapter
|
|
188
|
+
const actorManager = createActorManagerAdapter(conductor, taskEntry);
|
|
189
|
+
|
|
190
|
+
// Create Orchestrator instance
|
|
191
|
+
const orchestrator = createOrchestrator({
|
|
192
|
+
actorManager,
|
|
193
|
+
|
|
194
|
+
onStatusChange: (serializedTask) => {
|
|
195
|
+
// Sync orchestrator state back to conductor's task entry
|
|
196
|
+
taskEntry.status = serializedTask.phase;
|
|
197
|
+
taskEntry.currentStep = `${serializedTask.phase}${serializedTask.currentStepIndex >= 0 ? ' step ' + serializedTask.currentStepIndex : ''}`;
|
|
198
|
+
taskEntry.activeActors = serializedTask.activeActors.map(a => a.instanceId);
|
|
199
|
+
taskEntry.lastUpdate = Date.now();
|
|
200
|
+
|
|
201
|
+
// Persist to state.json (async, non-blocking)
|
|
202
|
+
updateTaskInState(taskId, taskEntry).catch(e =>
|
|
203
|
+
console.warn(`[TaskRunner] Failed to persist task ${taskId}:`, e.message)
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// Push status to frontend
|
|
207
|
+
sendConductorMessage({
|
|
208
|
+
type: 'conductor_task_status',
|
|
209
|
+
taskId,
|
|
210
|
+
task: {
|
|
211
|
+
...taskEntry,
|
|
212
|
+
phase: serializedTask.phase,
|
|
213
|
+
complexity: serializedTask.complexity,
|
|
214
|
+
plan: serializedTask.plan,
|
|
215
|
+
activeActors: serializedTask.activeActors,
|
|
216
|
+
completedActors: serializedTask.completedActors,
|
|
217
|
+
iterations: serializedTask.iterations
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
sendStatusUpdate(conductor);
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
onLog: (...args) => {
|
|
224
|
+
console.log('[TaskRunner/Orchestrator]', ...args);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Register orchestrator
|
|
229
|
+
orchestrators.set(taskId, orchestrator);
|
|
230
|
+
|
|
231
|
+
// Start the task (async — orchestrator runs its state machine internally)
|
|
232
|
+
try {
|
|
233
|
+
await orchestrator.startTask({
|
|
234
|
+
title,
|
|
235
|
+
description: description || title,
|
|
236
|
+
scenario,
|
|
237
|
+
workDir
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
console.log(`[TaskRunner] Orchestrator started for task ${taskId}`);
|
|
241
|
+
} catch (err) {
|
|
242
|
+
console.error(`[TaskRunner] Failed to start orchestrator for task ${taskId}:`, err.message);
|
|
243
|
+
|
|
244
|
+
taskEntry.status = 'failed';
|
|
245
|
+
taskEntry.error = err.message;
|
|
246
|
+
taskEntry.lastUpdate = Date.now();
|
|
247
|
+
|
|
248
|
+
await updateTaskInState(taskId, taskEntry).catch(() => {});
|
|
249
|
+
|
|
250
|
+
sendConductorOutput(conductor, 'system', {
|
|
251
|
+
message: { role: 'assistant', content: `Task "${title}" failed to start: ${err.message}` }
|
|
252
|
+
});
|
|
253
|
+
sendStatusUpdate(conductor);
|
|
254
|
+
|
|
255
|
+
// Cleanup
|
|
256
|
+
orchestrators.delete(taskId);
|
|
257
|
+
await releaseAllActors(taskId, { force: true }).catch(() => {});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Stop a running task's orchestrator and release all its actors.
|
|
263
|
+
*
|
|
264
|
+
* @param {string} taskId
|
|
265
|
+
*/
|
|
266
|
+
export async function stopTaskExecution(taskId) {
|
|
267
|
+
const orchestrator = orchestrators.get(taskId);
|
|
268
|
+
if (!orchestrator) return;
|
|
269
|
+
|
|
270
|
+
console.log(`[TaskRunner] Stopping task ${taskId}`);
|
|
271
|
+
orchestrators.delete(taskId);
|
|
272
|
+
|
|
273
|
+
// Release all actors for this task
|
|
274
|
+
await releaseAllActors(taskId, { force: true }).catch(e =>
|
|
275
|
+
console.warn(`[TaskRunner] Error releasing actors for ${taskId}:`, e.message)
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Stop all running task orchestrators (used during conductor shutdown).
|
|
281
|
+
*/
|
|
282
|
+
export async function stopAllTaskExecutions() {
|
|
283
|
+
const taskIds = [...orchestrators.keys()];
|
|
284
|
+
for (const taskId of taskIds) {
|
|
285
|
+
await stopTaskExecution(taskId);
|
|
286
|
+
}
|
|
287
|
+
}
|
package/conductor/ui-messages.js
CHANGED
|
@@ -20,7 +20,7 @@ export function sendConductorMessage(msg) {
|
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Send Conductor Claude output to frontend
|
|
23
|
-
* outputType: 'text' | 'tool_use' | 'tool_result' | 'system' | 'task_created' | 'task_forwarded'
|
|
23
|
+
* outputType: 'text' | 'tool_use' | 'tool_result' | 'system' | 'task_creating' | 'task_created' | 'task_forwarded'
|
|
24
24
|
*/
|
|
25
25
|
export function sendConductorOutput(conductor, outputType, rawMessage, extra = {}) {
|
|
26
26
|
sendConductorMessage({
|
|
@@ -64,6 +64,13 @@ export function sendConductorOutput(conductor, outputType, rawMessage, extra = {
|
|
|
64
64
|
conductor.uiMessages.push({
|
|
65
65
|
source: 'conductor', type: 'system', content: text, timestamp: Date.now()
|
|
66
66
|
});
|
|
67
|
+
} else if (outputType === 'task_creating') {
|
|
68
|
+
conductor.uiMessages.push({
|
|
69
|
+
source: 'conductor', type: 'task_creating',
|
|
70
|
+
taskId: extra.taskId, taskTitle: extra.taskTitle,
|
|
71
|
+
content: `Creating task: ${extra.taskTitle}...`,
|
|
72
|
+
timestamp: Date.now()
|
|
73
|
+
});
|
|
67
74
|
} else if (outputType === 'task_created') {
|
|
68
75
|
conductor.uiMessages.push({
|
|
69
76
|
source: 'conductor', type: 'task_created',
|