@yeaft/webchat-agent 0.1.233 → 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.
@@ -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
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Conductor — Module Entry
3
3
  *
4
- * Exports three core subsystems:
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';
@@ -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
+ }
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.233",
3
+ "version": "0.1.235",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",