@yeaft/webchat-agent 0.1.236 → 0.1.238
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 +38 -1
- package/conductor/index.js +13 -2
- package/conductor/orchestrator.js +46 -4
- package/conductor/session.js +2 -0
- package/conductor/task-runner.js +336 -0
- package/conductor/task.js +3 -2
- package/conductor/ui-messages.js +8 -1
- package/conductor/worktree.js +7 -2
- 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, forwardToTask } from './task-runner.js';
|
|
24
25
|
|
|
25
26
|
// =====================================================================
|
|
26
27
|
// Claude Session ID Persistence
|
|
@@ -142,7 +143,15 @@ export async function createConductorClaude(conductor) {
|
|
|
142
143
|
cwd: conductorHome,
|
|
143
144
|
permissionMode: 'bypassPermissions',
|
|
144
145
|
abort: abortController.signal,
|
|
145
|
-
appendSystemPrompt: systemPrompt
|
|
146
|
+
appendSystemPrompt: systemPrompt,
|
|
147
|
+
// Conductor Claude is a lightweight classifier — restrict heavy tools
|
|
148
|
+
// Only allow: Read, Glob, Grep (for context awareness), WebFetch, WebSearch
|
|
149
|
+
disallowedTools: [
|
|
150
|
+
'Write', 'Edit', 'NotebookEdit',
|
|
151
|
+
'Bash', 'Agent',
|
|
152
|
+
'EnterPlanMode', 'ExitPlanMode',
|
|
153
|
+
'EnterWorktree', 'ExitWorktree'
|
|
154
|
+
]
|
|
146
155
|
};
|
|
147
156
|
|
|
148
157
|
if (savedSessionId) {
|
|
@@ -191,6 +200,7 @@ export async function sendToConductor(conductor, content) {
|
|
|
191
200
|
|
|
192
201
|
state.turnActive = true;
|
|
193
202
|
state.accumulatedText = '';
|
|
203
|
+
state._lastUserContent = content; // Preserve original user input for task description
|
|
194
204
|
state.inputStream.enqueue({
|
|
195
205
|
type: 'user',
|
|
196
206
|
message: { role: 'user', content: fullContent }
|
|
@@ -328,6 +338,18 @@ async function processConductorOutput(conductor, conductorQuery) {
|
|
|
328
338
|
for (const ct of createTasks) {
|
|
329
339
|
const taskId = `task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
330
340
|
|
|
341
|
+
// Notify frontend: task is being created (worktree may take a few seconds)
|
|
342
|
+
sendConductorOutput(conductor, 'task_creating', null, {
|
|
343
|
+
taskId, taskTitle: ct.title
|
|
344
|
+
});
|
|
345
|
+
sendConductorMessage({
|
|
346
|
+
type: 'conductor_task_creating',
|
|
347
|
+
taskId,
|
|
348
|
+
title: ct.title,
|
|
349
|
+
workDir: ct.workDir,
|
|
350
|
+
scenario: ct.scenario
|
|
351
|
+
});
|
|
352
|
+
|
|
331
353
|
// Create task directory at {workDir}/.conductor/{taskId}/ (includes worktree)
|
|
332
354
|
let worktreePath = null;
|
|
333
355
|
if (ct.workDir) {
|
|
@@ -339,10 +361,15 @@ async function processConductorOutput(conductor, conductorQuery) {
|
|
|
339
361
|
}
|
|
340
362
|
}
|
|
341
363
|
|
|
364
|
+
// Extract user's original message as task description
|
|
365
|
+
// (the content that triggered this CREATE_TASK)
|
|
366
|
+
const userDescription = state._lastUserContent || ct.title;
|
|
367
|
+
|
|
342
368
|
// Register in state.json
|
|
343
369
|
const taskEntry = {
|
|
344
370
|
taskId,
|
|
345
371
|
title: ct.title,
|
|
372
|
+
description: userDescription,
|
|
346
373
|
workDir: ct.workDir,
|
|
347
374
|
scenario: ct.scenario,
|
|
348
375
|
status: 'created',
|
|
@@ -367,6 +394,11 @@ async function processConductorOutput(conductor, conductorQuery) {
|
|
|
367
394
|
});
|
|
368
395
|
|
|
369
396
|
console.log(`[Conductor] Task created: ${taskId} — ${ct.title} @ ${ct.workDir} (${ct.scenario})`);
|
|
397
|
+
|
|
398
|
+
// Start Orchestrator-driven execution (fire-and-forget)
|
|
399
|
+
startTaskExecution(conductor, taskEntry).catch(e =>
|
|
400
|
+
console.error(`[Conductor] Task execution failed for ${taskId}:`, e.message)
|
|
401
|
+
);
|
|
370
402
|
}
|
|
371
403
|
|
|
372
404
|
// Handle FORWARD_TASK
|
|
@@ -381,6 +413,11 @@ async function processConductorOutput(conductor, conductorQuery) {
|
|
|
381
413
|
});
|
|
382
414
|
task.lastUpdate = Date.now();
|
|
383
415
|
|
|
416
|
+
// Forward to Orchestrator (which will relay to active actors)
|
|
417
|
+
forwardToTask(ft.taskId, ft.message).catch(e =>
|
|
418
|
+
console.warn(`[Conductor] Failed to forward to orchestrator ${ft.taskId}:`, e.message)
|
|
419
|
+
);
|
|
420
|
+
|
|
384
421
|
sendConductorOutput(conductor, 'task_forwarded', null, {
|
|
385
422
|
taskId: ft.taskId
|
|
386
423
|
});
|
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 ----
|
|
@@ -61,5 +62,15 @@ export {
|
|
|
61
62
|
cleanupTaskWorktree,
|
|
62
63
|
cleanupAllConductorWorktrees,
|
|
63
64
|
getTaskWorktreePath,
|
|
64
|
-
getActorCwd
|
|
65
|
+
getActorCwd,
|
|
66
|
+
READ_WRITE_SPECIALTIES
|
|
65
67
|
} from './worktree.js';
|
|
68
|
+
|
|
69
|
+
// ---- Task Runner (Orchestrator ↔ Actor bridge) ----
|
|
70
|
+
export {
|
|
71
|
+
startTaskExecution,
|
|
72
|
+
stopTaskExecution,
|
|
73
|
+
stopAllTaskExecutions,
|
|
74
|
+
forwardToTask,
|
|
75
|
+
getOrchestrator
|
|
76
|
+
} from './task-runner.js';
|
|
@@ -53,6 +53,18 @@ export class Orchestrator {
|
|
|
53
53
|
|
|
54
54
|
/** @type {Map<string, Task>} */
|
|
55
55
|
this.tasks = new Map();
|
|
56
|
+
|
|
57
|
+
/** Abort flag — checked at each phase boundary in _runStateMachine */
|
|
58
|
+
this._aborted = false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Abort the orchestrator — stops the state machine loop at the next phase boundary.
|
|
63
|
+
* Does NOT release actors (that's handled by task-runner's stopTaskExecution).
|
|
64
|
+
*/
|
|
65
|
+
abort() {
|
|
66
|
+
this._aborted = true;
|
|
67
|
+
this.log('Orchestrator aborted');
|
|
56
68
|
}
|
|
57
69
|
|
|
58
70
|
// ===================================================================
|
|
@@ -63,15 +75,16 @@ export class Orchestrator {
|
|
|
63
75
|
* 创建并启动新 Task
|
|
64
76
|
*
|
|
65
77
|
* @param {object} params
|
|
78
|
+
* @param {string} [params.taskId] — 外部指定的 taskId(来自 Conductor),不提供则自动生成
|
|
66
79
|
* @param {string} params.title
|
|
67
80
|
* @param {string} params.description
|
|
68
81
|
* @param {string} params.scenario — 'dev' | 'writing' | 'trading' | 'video'
|
|
69
82
|
* @param {string} params.workDir
|
|
70
83
|
* @returns {Task}
|
|
71
84
|
*/
|
|
72
|
-
async startTask({ title, description, scenario, workDir }) {
|
|
85
|
+
async startTask({ taskId: externalTaskId, title, description, scenario, workDir }) {
|
|
73
86
|
const flow = getFlow(scenario); // 验证 scenario 合法性
|
|
74
|
-
const task = createTask({ title, description, scenario, workDir });
|
|
87
|
+
const task = createTask({ taskId: externalTaskId, title, description, scenario, workDir });
|
|
75
88
|
this.tasks.set(task.taskId, task);
|
|
76
89
|
this._notify(task);
|
|
77
90
|
|
|
@@ -114,13 +127,33 @@ export class Orchestrator {
|
|
|
114
127
|
|
|
115
128
|
/**
|
|
116
129
|
* 用户消息注入(Task 对话页直接交互时)
|
|
117
|
-
*
|
|
130
|
+
*
|
|
131
|
+
* 将 Conductor 转发的用户消息注入到正在运行的 task:
|
|
132
|
+
* - 如果 task 有活跃的 actor,发送给所有活跃 actor
|
|
133
|
+
* - 如果没有活跃 actor(phase 之间),存入 inbox 等待下一个 actor
|
|
118
134
|
*/
|
|
119
135
|
async handleUserMessage(taskId, message) {
|
|
120
136
|
const task = this.tasks.get(taskId);
|
|
121
137
|
if (!task) return;
|
|
122
138
|
this.log(`User message for ${taskId}: ${message.substring(0, 100)}...`);
|
|
123
|
-
|
|
139
|
+
|
|
140
|
+
// Store in inbox for actors in subsequent phases
|
|
141
|
+
if (!task.inbox) task.inbox = [];
|
|
142
|
+
task.inbox.push({ from: 'user', content: message, timestamp: Date.now() });
|
|
143
|
+
task.updatedAt = Date.now();
|
|
144
|
+
|
|
145
|
+
// If there are active actors, forward to them
|
|
146
|
+
if (task.activeActors.length > 0) {
|
|
147
|
+
for (const actor of task.activeActors) {
|
|
148
|
+
try {
|
|
149
|
+
this.actorManager.sendMessage?.(actor.instanceId, message);
|
|
150
|
+
} catch (e) {
|
|
151
|
+
this.log(`Failed to send user message to actor ${actor.instanceId}: ${e.message}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
this._notify(task);
|
|
124
157
|
}
|
|
125
158
|
|
|
126
159
|
// ===================================================================
|
|
@@ -132,6 +165,15 @@ export class Orchestrator {
|
|
|
132
165
|
*/
|
|
133
166
|
async _runStateMachine(task, flow) {
|
|
134
167
|
while (task.phase !== Phase.DONE && task.phase !== Phase.FAILED) {
|
|
168
|
+
// Check abort flag at each phase boundary
|
|
169
|
+
if (this._aborted) {
|
|
170
|
+
this.log(`Task ${task.taskId} aborted`);
|
|
171
|
+
task.error = 'Task aborted';
|
|
172
|
+
transitionPhase(task, Phase.FAILED);
|
|
173
|
+
this._notify(task);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
|
|
135
177
|
this.log(`Task ${task.taskId} entering phase: ${task.phase}`);
|
|
136
178
|
|
|
137
179
|
switch (task.phase) {
|
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,336 @@
|
|
|
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
|
+
import { READ_WRITE_SPECIALTIES } from './worktree.js';
|
|
23
|
+
|
|
24
|
+
// =====================================================================
|
|
25
|
+
// Per-task Orchestrator registry
|
|
26
|
+
// =====================================================================
|
|
27
|
+
|
|
28
|
+
/** @type {Map<string, Orchestrator>} taskId → Orchestrator */
|
|
29
|
+
const orchestrators = new Map();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get an active orchestrator for a task
|
|
33
|
+
* @param {string} taskId
|
|
34
|
+
* @returns {Orchestrator|null}
|
|
35
|
+
*/
|
|
36
|
+
export function getOrchestrator(taskId) {
|
|
37
|
+
return orchestrators.get(taskId) || null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// =====================================================================
|
|
41
|
+
// ActorManager Adapter
|
|
42
|
+
// =====================================================================
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Create an actorManager adapter that translates Orchestrator's
|
|
46
|
+
* createActor(persona, specialty, context) interface into Actor module's
|
|
47
|
+
* createActor({ personaId, specialtyId, ... }) format.
|
|
48
|
+
*
|
|
49
|
+
* Orchestrator calls:
|
|
50
|
+
* actorManager.createActor(
|
|
51
|
+
* { id: personaId, name: personaName },
|
|
52
|
+
* specialty, // string like 'coding', 'planning', etc.
|
|
53
|
+
* { taskId, actorInstanceId, instruction, workDir, memoryPath, ... }
|
|
54
|
+
* ) → Promise<ActorResult>
|
|
55
|
+
*
|
|
56
|
+
* Actor.js expects:
|
|
57
|
+
* createActor({
|
|
58
|
+
* personaId, specialtyId, taskId, taskDir, taskContext,
|
|
59
|
+
* worktreePath, onOutput, onComplete, onError
|
|
60
|
+
* }) → Promise<ActorInstance>
|
|
61
|
+
*
|
|
62
|
+
* The adapter wraps actor creation + message sending + result collection
|
|
63
|
+
* into a single Promise<ActorResult> that the Orchestrator awaits.
|
|
64
|
+
*/
|
|
65
|
+
function createActorManagerAdapter(conductor, taskEntry) {
|
|
66
|
+
const { taskId, workDir, worktreePath } = taskEntry;
|
|
67
|
+
const taskDir = getTaskDir(workDir, taskId);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
/**
|
|
71
|
+
* Create an actor, send it instructions, and wait for its result.
|
|
72
|
+
* @returns {Promise<ActorResult>}
|
|
73
|
+
*/
|
|
74
|
+
async createActor(persona, specialty, context) {
|
|
75
|
+
const actorWorktreePath = READ_WRITE_SPECIALTIES.has(specialty)
|
|
76
|
+
? worktreePath
|
|
77
|
+
: null;
|
|
78
|
+
|
|
79
|
+
// Create a deferred promise for actor completion
|
|
80
|
+
let resolveResult, rejectResult;
|
|
81
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
82
|
+
resolveResult = resolve;
|
|
83
|
+
rejectResult = reject;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const instance = await createActor({
|
|
87
|
+
personaId: persona.id,
|
|
88
|
+
specialtyId: specialty,
|
|
89
|
+
taskId,
|
|
90
|
+
taskDir,
|
|
91
|
+
taskContext: {
|
|
92
|
+
description: taskEntry.description || '',
|
|
93
|
+
instruction: context.instruction || context.description || '',
|
|
94
|
+
memoryPath: context.memoryPath || null,
|
|
95
|
+
planPath: context.planPath || null,
|
|
96
|
+
scenario: taskEntry.scenario,
|
|
97
|
+
workDir
|
|
98
|
+
},
|
|
99
|
+
threadId: context.threadId || null,
|
|
100
|
+
worktreePath: actorWorktreePath,
|
|
101
|
+
onOutput: (instanceId, message) => {
|
|
102
|
+
// Relay actor output to frontend in real-time
|
|
103
|
+
sendConductorOutput(conductor, 'text', message, {
|
|
104
|
+
taskId,
|
|
105
|
+
persona: persona.name || persona.id,
|
|
106
|
+
specialty
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
onComplete: (instanceId, result) => {
|
|
110
|
+
console.log(`[TaskRunner] Actor ${instanceId} completed for task ${taskId}`);
|
|
111
|
+
|
|
112
|
+
// Accumulate actor cost into conductor totals
|
|
113
|
+
if (result.costUsd) conductor.costUsd += result.costUsd;
|
|
114
|
+
if (result.inputTokens) conductor.totalInputTokens += result.inputTokens;
|
|
115
|
+
if (result.outputTokens) conductor.totalOutputTokens += result.outputTokens;
|
|
116
|
+
|
|
117
|
+
// Update tracking: decrement active count
|
|
118
|
+
if (taskEntry.activeActors) {
|
|
119
|
+
const idx = taskEntry.activeActors.indexOf(instanceId);
|
|
120
|
+
if (idx !== -1) taskEntry.activeActors.splice(idx, 1);
|
|
121
|
+
}
|
|
122
|
+
conductor.activeClaudes = Math.max(0, (conductor.activeClaudes || 1) - 1);
|
|
123
|
+
sendStatusUpdate(conductor);
|
|
124
|
+
|
|
125
|
+
// Release actor from registry to prevent memory leak
|
|
126
|
+
releaseActor(taskId, instanceId).catch(e =>
|
|
127
|
+
console.warn(`[TaskRunner] Failed to auto-release actor ${instanceId}:`, e.message)
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
resolveResult(result);
|
|
131
|
+
},
|
|
132
|
+
onError: (instanceId, error) => {
|
|
133
|
+
console.error(`[TaskRunner] Actor ${instanceId} failed for task ${taskId}:`, error.message);
|
|
134
|
+
|
|
135
|
+
// Update tracking: decrement active count
|
|
136
|
+
if (taskEntry.activeActors) {
|
|
137
|
+
const idx = taskEntry.activeActors.indexOf(instanceId);
|
|
138
|
+
if (idx !== -1) taskEntry.activeActors.splice(idx, 1);
|
|
139
|
+
}
|
|
140
|
+
conductor.activeClaudes = Math.max(0, (conductor.activeClaudes || 1) - 1);
|
|
141
|
+
sendStatusUpdate(conductor);
|
|
142
|
+
|
|
143
|
+
// Release actor from registry on error too
|
|
144
|
+
releaseActor(taskId, instanceId).catch(e =>
|
|
145
|
+
console.warn(`[TaskRunner] Failed to auto-release failed actor ${instanceId}:`, e.message)
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
rejectResult(error);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Update conductor's active claude count
|
|
153
|
+
conductor.activeClaudes = (conductor.activeClaudes || 0) + 1;
|
|
154
|
+
|
|
155
|
+
// Update task entry's activeActors for status tracking
|
|
156
|
+
if (!taskEntry.activeActors) taskEntry.activeActors = [];
|
|
157
|
+
taskEntry.activeActors.push(instance.instanceId);
|
|
158
|
+
sendStatusUpdate(conductor);
|
|
159
|
+
|
|
160
|
+
// Send the instruction to the actor to start work
|
|
161
|
+
const instruction = context.instruction || context.description || taskEntry.description || '';
|
|
162
|
+
if (instruction) {
|
|
163
|
+
sendToActor(taskId, instance.instanceId, instruction);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return resultPromise;
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Release an actor instance.
|
|
171
|
+
*/
|
|
172
|
+
releaseActor(instanceId) {
|
|
173
|
+
releaseActor(taskId, instanceId).catch(e =>
|
|
174
|
+
console.warn(`[TaskRunner] Failed to release actor ${instanceId}:`, e.message)
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Update tracking
|
|
178
|
+
if (taskEntry.activeActors) {
|
|
179
|
+
const idx = taskEntry.activeActors.indexOf(instanceId);
|
|
180
|
+
if (idx !== -1) taskEntry.activeActors.splice(idx, 1);
|
|
181
|
+
}
|
|
182
|
+
conductor.activeClaudes = Math.max(0, (conductor.activeClaudes || 1) - 1);
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Send a message to an active actor (for user message forwarding).
|
|
187
|
+
*/
|
|
188
|
+
sendMessage(instanceId, message) {
|
|
189
|
+
sendToActor(taskId, instanceId, message);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// =====================================================================
|
|
195
|
+
// Task Execution Entry Point
|
|
196
|
+
// =====================================================================
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Start task execution via Orchestrator.
|
|
200
|
+
*
|
|
201
|
+
* Called after initTaskDir completes (folder + worktree ready).
|
|
202
|
+
* This is fire-and-forget — the orchestrator runs asynchronously.
|
|
203
|
+
*
|
|
204
|
+
* @param {object} conductor — Conductor singleton instance
|
|
205
|
+
* @param {object} taskEntry — Task registry entry from conductor.tasks Map
|
|
206
|
+
*/
|
|
207
|
+
export async function startTaskExecution(conductor, taskEntry) {
|
|
208
|
+
const { taskId, title, workDir, scenario, description } = taskEntry;
|
|
209
|
+
|
|
210
|
+
console.log(`[TaskRunner] Starting execution for task ${taskId}: "${title}" [${scenario}]`);
|
|
211
|
+
|
|
212
|
+
// Update task status
|
|
213
|
+
taskEntry.status = 'running';
|
|
214
|
+
taskEntry.lastUpdate = Date.now();
|
|
215
|
+
|
|
216
|
+
// Create actorManager adapter
|
|
217
|
+
const actorManager = createActorManagerAdapter(conductor, taskEntry);
|
|
218
|
+
|
|
219
|
+
// Create Orchestrator instance
|
|
220
|
+
const orchestrator = createOrchestrator({
|
|
221
|
+
actorManager,
|
|
222
|
+
|
|
223
|
+
onStatusChange: (serializedTask) => {
|
|
224
|
+
// Sync orchestrator state back to conductor's task entry
|
|
225
|
+
taskEntry.status = serializedTask.phase;
|
|
226
|
+
taskEntry.currentStep = `${serializedTask.phase}${serializedTask.currentStepIndex >= 0 ? ' step ' + serializedTask.currentStepIndex : ''}`;
|
|
227
|
+
taskEntry.activeActors = serializedTask.activeActors.map(a => a.instanceId);
|
|
228
|
+
taskEntry.lastUpdate = Date.now();
|
|
229
|
+
|
|
230
|
+
// Persist to state.json (async, non-blocking)
|
|
231
|
+
updateTaskInState(taskId, taskEntry).catch(e =>
|
|
232
|
+
console.warn(`[TaskRunner] Failed to persist task ${taskId}:`, e.message)
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// Push status to frontend
|
|
236
|
+
sendConductorMessage({
|
|
237
|
+
type: 'conductor_task_status',
|
|
238
|
+
taskId,
|
|
239
|
+
task: {
|
|
240
|
+
...taskEntry,
|
|
241
|
+
phase: serializedTask.phase,
|
|
242
|
+
complexity: serializedTask.complexity,
|
|
243
|
+
plan: serializedTask.plan,
|
|
244
|
+
activeActors: serializedTask.activeActors,
|
|
245
|
+
completedActors: serializedTask.completedActors,
|
|
246
|
+
iterations: serializedTask.iterations
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
sendStatusUpdate(conductor);
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
onLog: (...args) => {
|
|
253
|
+
console.log('[TaskRunner/Orchestrator]', ...args);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Register orchestrator
|
|
258
|
+
orchestrators.set(taskId, orchestrator);
|
|
259
|
+
|
|
260
|
+
// Start the task (async — orchestrator runs its state machine internally)
|
|
261
|
+
try {
|
|
262
|
+
await orchestrator.startTask({
|
|
263
|
+
taskId,
|
|
264
|
+
title,
|
|
265
|
+
description: description || title,
|
|
266
|
+
scenario,
|
|
267
|
+
workDir
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
console.log(`[TaskRunner] Orchestrator started for task ${taskId}`);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
console.error(`[TaskRunner] Failed to start orchestrator for task ${taskId}:`, err.message);
|
|
273
|
+
|
|
274
|
+
taskEntry.status = 'failed';
|
|
275
|
+
taskEntry.error = err.message;
|
|
276
|
+
taskEntry.lastUpdate = Date.now();
|
|
277
|
+
|
|
278
|
+
await updateTaskInState(taskId, taskEntry).catch(() => {});
|
|
279
|
+
|
|
280
|
+
sendConductorOutput(conductor, 'system', {
|
|
281
|
+
message: { role: 'assistant', content: `Task "${title}" failed to start: ${err.message}` }
|
|
282
|
+
});
|
|
283
|
+
sendStatusUpdate(conductor);
|
|
284
|
+
|
|
285
|
+
// Cleanup
|
|
286
|
+
orchestrators.delete(taskId);
|
|
287
|
+
await releaseAllActors(taskId, { force: true }).catch(() => {});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Forward a user message to a running task's orchestrator.
|
|
293
|
+
*
|
|
294
|
+
* @param {string} taskId
|
|
295
|
+
* @param {string} message
|
|
296
|
+
*/
|
|
297
|
+
export async function forwardToTask(taskId, message) {
|
|
298
|
+
const orchestrator = orchestrators.get(taskId);
|
|
299
|
+
if (!orchestrator) {
|
|
300
|
+
console.warn(`[TaskRunner] Cannot forward to task ${taskId}: no active orchestrator`);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
await orchestrator.handleUserMessage(taskId, message);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Stop a running task's orchestrator and release all its actors.
|
|
309
|
+
*
|
|
310
|
+
* @param {string} taskId
|
|
311
|
+
*/
|
|
312
|
+
export async function stopTaskExecution(taskId) {
|
|
313
|
+
const orchestrator = orchestrators.get(taskId);
|
|
314
|
+
if (!orchestrator) return;
|
|
315
|
+
|
|
316
|
+
console.log(`[TaskRunner] Stopping task ${taskId}`);
|
|
317
|
+
|
|
318
|
+
// Abort the orchestrator state machine (stops at next phase boundary)
|
|
319
|
+
orchestrator.abort();
|
|
320
|
+
orchestrators.delete(taskId);
|
|
321
|
+
|
|
322
|
+
// Release all actors for this task
|
|
323
|
+
await releaseAllActors(taskId, { force: true }).catch(e =>
|
|
324
|
+
console.warn(`[TaskRunner] Error releasing actors for ${taskId}:`, e.message)
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Stop all running task orchestrators (used during conductor shutdown).
|
|
330
|
+
*/
|
|
331
|
+
export async function stopAllTaskExecutions() {
|
|
332
|
+
const taskIds = [...orchestrators.keys()];
|
|
333
|
+
for (const taskId of taskIds) {
|
|
334
|
+
await stopTaskExecution(taskId);
|
|
335
|
+
}
|
|
336
|
+
}
|
package/conductor/task.js
CHANGED
|
@@ -58,14 +58,15 @@ export function generateTaskId() {
|
|
|
58
58
|
* 创建 Task 实例
|
|
59
59
|
*
|
|
60
60
|
* @param {object} params
|
|
61
|
+
* @param {string} [params.taskId] — 外部指定的 taskId(来自 Conductor),不提供则自动生成
|
|
61
62
|
* @param {string} params.title — 任务标题
|
|
62
63
|
* @param {string} params.description — 任务描述(用户原始输入 or Conductor 转译)
|
|
63
64
|
* @param {string} params.scenario — 场景: dev / writing / trading / video
|
|
64
65
|
* @param {string} params.workDir — 工作目录(创建时绑定)
|
|
65
66
|
* @returns {Task}
|
|
66
67
|
*/
|
|
67
|
-
export function createTask({ title, description, scenario, workDir }) {
|
|
68
|
-
const taskId = generateTaskId();
|
|
68
|
+
export function createTask({ taskId: externalTaskId, title, description, scenario, workDir }) {
|
|
69
|
+
const taskId = externalTaskId || generateTaskId();
|
|
69
70
|
const now = Date.now();
|
|
70
71
|
|
|
71
72
|
return {
|
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',
|
package/conductor/worktree.js
CHANGED
|
@@ -15,6 +15,12 @@ import { promisify } from 'util';
|
|
|
15
15
|
|
|
16
16
|
const execFile = promisify(execFileCb);
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Specialties that need read-write access (use worktree as cwd).
|
|
20
|
+
* Shared constant — imported by task-runner.js.
|
|
21
|
+
*/
|
|
22
|
+
export const READ_WRITE_SPECIALTIES = new Set(['coding', 'testing', 'review']);
|
|
23
|
+
|
|
18
24
|
/**
|
|
19
25
|
* Create a worktree for a task.
|
|
20
26
|
* Called at task creation time.
|
|
@@ -147,8 +153,7 @@ export function getTaskWorktreePath(taskDir) {
|
|
|
147
153
|
* - Read-only actors → workDir (main project directory)
|
|
148
154
|
*/
|
|
149
155
|
export function getActorCwd(workDir, taskDir, specialty) {
|
|
150
|
-
|
|
151
|
-
if (readWriteSpecialties.has(specialty)) {
|
|
156
|
+
if (READ_WRITE_SPECIALTIES.has(specialty)) {
|
|
152
157
|
return getTaskWorktreePath(taskDir);
|
|
153
158
|
}
|
|
154
159
|
return workDir;
|