@yeaft/webchat-agent 0.1.224 → 0.1.225

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.
@@ -0,0 +1,478 @@
1
+ /**
2
+ * Conductor — Actor Claude 实例管理
3
+ *
4
+ * Actor 是最底层的执行单元:
5
+ * - 创建 actor session(写 CLAUDE.md → 启动 Claude)
6
+ * - 发送任务 → 收集结果 → 回调 Orchestrator
7
+ * - 释放 actor(清理 session)
8
+ *
9
+ * 复用 V1 的 createRoleQuery 模式,适配 Conductor 三层架构
10
+ */
11
+ import { query, Stream } from '../sdk/index.js';
12
+ import { join } from 'path';
13
+ import { mkdirSync } from 'fs';
14
+ import ctx from '../context.js';
15
+ import { generateActorCLAUDEmd, generateActorDirName, generateActorInstanceId } from './claudemd-generator.js';
16
+ import { getPersonaById } from './personas/index.js';
17
+ import { getSpecialty, getThinkingMode } from './specialties/index.js';
18
+
19
+ // =====================================================================
20
+ // Actor Instance Registry
21
+ // =====================================================================
22
+
23
+ /**
24
+ * Per-task actor registry: taskId → Map<instanceId, ActorInstance>
25
+ * @type {Map<string, Map<string, ActorInstance>>}
26
+ */
27
+ const actorRegistry = new Map();
28
+
29
+ /**
30
+ * Per-actor mutex to prevent concurrent createActorQuery calls
31
+ * @type {Map<string, Promise>}
32
+ */
33
+ const _actorLocks = new Map();
34
+
35
+ // =====================================================================
36
+ // Actor Lifecycle
37
+ // =====================================================================
38
+
39
+ /**
40
+ * 创建 Actor 实例
41
+ *
42
+ * 流程:生成 CLAUDE.md → 创建目录 → 启动 Claude query(appendSystemPrompt 注入)→ 注册
43
+ *
44
+ * @param {object} params
45
+ * @param {string} params.personaId - persona ID
46
+ * @param {string} params.specialtyId - specialty ID
47
+ * @param {string} params.taskId - 所属 task ID
48
+ * @param {string} params.taskDir - task 目录路径 (.conductor/tasks/task-N)
49
+ * @param {object} params.taskContext - 传递给 CLAUDE.md 生成器的上下文
50
+ * @param {string} [params.threadId] - 并行线程 ID(coding actor 用)
51
+ * @param {string} [params.worktreePath] - worktree 路径
52
+ * @param {string} [params.model] - 指定模型
53
+ * @param {function} [params.onOutput] - 输出回调 (instanceId, message) => void
54
+ * @param {function} [params.onComplete] - 完成回调 (instanceId, result) => void
55
+ * @param {function} [params.onError] - 错误回调 (instanceId, error) => void
56
+ * @returns {Promise<ActorInstance>}
57
+ */
58
+ export async function createActor(params) {
59
+ const {
60
+ personaId,
61
+ specialtyId,
62
+ taskId,
63
+ taskDir,
64
+ taskContext = {},
65
+ threadId,
66
+ worktreePath,
67
+ model,
68
+ onOutput,
69
+ onComplete,
70
+ onError
71
+ } = params;
72
+
73
+ const instanceId = generateActorInstanceId(specialtyId, personaId, taskId, threadId);
74
+ const lockKey = instanceId;
75
+
76
+ // Mutex: 防止并发创建同一个 actor
77
+ if (_actorLocks.has(lockKey)) {
78
+ console.log(`[Actor] Waiting for existing lock on ${instanceId}`);
79
+ try { await _actorLocks.get(lockKey); } catch { /* 前一次失败了,继续 */ }
80
+ const existing = getActor(taskId, instanceId);
81
+ if (existing && existing.status === 'working') {
82
+ console.log(`[Actor] Reusing existing actor ${instanceId}`);
83
+ return existing;
84
+ }
85
+ }
86
+
87
+ const promise = _createActorInner(params, instanceId);
88
+ _actorLocks.set(lockKey, promise);
89
+
90
+ try {
91
+ return await promise;
92
+ } finally {
93
+ if (_actorLocks.get(lockKey) === promise) {
94
+ _actorLocks.delete(lockKey);
95
+ }
96
+ }
97
+ }
98
+
99
+ /**
100
+ * createActor 内部实现
101
+ */
102
+ async function _createActorInner(params, instanceId) {
103
+ const {
104
+ personaId,
105
+ specialtyId,
106
+ taskId,
107
+ taskDir,
108
+ taskContext = {},
109
+ threadId,
110
+ worktreePath,
111
+ model,
112
+ onOutput,
113
+ onComplete,
114
+ onError
115
+ } = params;
116
+
117
+ const persona = getPersonaById(personaId);
118
+ if (!persona) throw new Error(`Persona not found: ${personaId}`);
119
+
120
+ const specialty = getSpecialty(specialtyId);
121
+ if (!specialty) throw new Error(`Specialty not found: ${specialtyId}`);
122
+
123
+ // 1. 生成 CLAUDE.md
124
+ const claudemdContent = generateActorCLAUDEmd(persona, specialtyId, {
125
+ ...taskContext,
126
+ worktreePath
127
+ });
128
+
129
+ // 2. 创建 actor 目录
130
+ const actorDirName = generateActorDirName(specialtyId, personaId, threadId);
131
+ const actorDir = join(taskDir, 'actors', actorDirName);
132
+ mkdirSync(actorDir, { recursive: true });
133
+
134
+ // 3. 确定 cwd(coding actor 用 worktree,其他用 actor 目录)
135
+ // CLAUDE.md 通过 appendSystemPrompt 注入 SDK,不写入文件(避免双重注入)
136
+ const actorCwd = worktreePath || actorDir;
137
+ mkdirSync(actorCwd, { recursive: true });
138
+
139
+ // 5. 创建 Claude query
140
+ const inputStream = new Stream();
141
+ const abortController = new AbortController();
142
+
143
+ // 继承全局 disallowedTools
144
+ const globalDisallowed = ctx.CONFIG?.disallowedTools || [];
145
+ const actorDisallowed = ['Agent']; // Actor 不允许使用 Agent 工具
146
+ const effectiveDisallowed = [...globalDisallowed, ...actorDisallowed];
147
+
148
+ const queryOptions = {
149
+ cwd: actorCwd,
150
+ permissionMode: 'bypassPermissions',
151
+ abort: abortController.signal,
152
+ model: model || undefined,
153
+ appendSystemPrompt: claudemdContent,
154
+ ...(effectiveDisallowed.length > 0 && { disallowedTools: effectiveDisallowed })
155
+ };
156
+
157
+ const roleQuery = query({
158
+ prompt: inputStream,
159
+ options: queryOptions
160
+ });
161
+
162
+ // 6. 创建 ActorInstance
163
+ const thinkingMode = getThinkingMode(specialtyId);
164
+
165
+ /** @type {ActorInstance} */
166
+ const instance = {
167
+ instanceId,
168
+ personaId,
169
+ personaName: persona.name,
170
+ specialtyId,
171
+ specialtyDisplayName: specialty.displayName,
172
+ thinkingMode,
173
+ taskId,
174
+ threadId: threadId || null,
175
+ worktreePath: worktreePath || null,
176
+ actorDir,
177
+ claudeSessionId: null,
178
+ status: 'working',
179
+ activity: `${persona.name} 正在执行 ${specialty.displayName}`,
180
+ startedAt: Date.now(),
181
+ // 内部管理
182
+ _query: roleQuery,
183
+ _inputStream: inputStream,
184
+ _abortController: abortController,
185
+ _accumulatedText: '',
186
+ _onOutput: onOutput || null,
187
+ _onComplete: onComplete || null,
188
+ _onError: onError || null
189
+ };
190
+
191
+ // 7. 注册到 registry
192
+ if (!actorRegistry.has(taskId)) {
193
+ actorRegistry.set(taskId, new Map());
194
+ }
195
+ actorRegistry.get(taskId).set(instanceId, instance);
196
+
197
+ // 8. 异步处理 Actor 输出
198
+ processActorOutput(instance);
199
+
200
+ console.log(`[Actor] Created ${instanceId}: ${persona.name} × ${specialtyId} (${thinkingMode})`);
201
+ return instance;
202
+ }
203
+
204
+ // =====================================================================
205
+ // Actor Communication
206
+ // =====================================================================
207
+
208
+ /**
209
+ * 向 Actor 发送任务消息
210
+ *
211
+ * @param {string} taskId - task ID
212
+ * @param {string} instanceId - actor instance ID
213
+ * @param {string} message - 要发送的消息
214
+ */
215
+ export function sendToActor(taskId, instanceId, message) {
216
+ const instance = getActor(taskId, instanceId);
217
+ if (!instance) {
218
+ console.error(`[Actor] Cannot send to unknown actor: ${instanceId}`);
219
+ return;
220
+ }
221
+
222
+ if (instance._inputStream.isDone) {
223
+ console.error(`[Actor] Cannot send to completed actor: ${instanceId}`);
224
+ return;
225
+ }
226
+
227
+ console.log(`[Actor] Sending message to ${instanceId} (${message.length} chars)`);
228
+ instance._inputStream.enqueue(message);
229
+ }
230
+
231
+ /**
232
+ * 异步处理 Actor 输出流
233
+ */
234
+ async function processActorOutput(instance) {
235
+ const { instanceId, _query: roleQuery } = instance;
236
+
237
+ try {
238
+ for await (const message of roleQuery) {
239
+ // 提取 sessionId
240
+ if (message.sessionId && !instance.claudeSessionId) {
241
+ instance.claudeSessionId = message.sessionId;
242
+ }
243
+
244
+ // 处理助手消息
245
+ if (message.type === 'assistant') {
246
+ const text = extractText(message);
247
+ if (text) {
248
+ instance._accumulatedText += text;
249
+
250
+ // 实时输出回调
251
+ if (instance._onOutput) {
252
+ try {
253
+ instance._onOutput(instanceId, message);
254
+ } catch (e) {
255
+ console.error(`[Actor] onOutput callback error for ${instanceId}:`, e.message);
256
+ }
257
+ }
258
+ }
259
+ }
260
+
261
+ // turn 结束的标志
262
+ if (message.type === 'result') {
263
+ instance.status = 'done';
264
+ instance.activity = '已完成';
265
+
266
+ const result = {
267
+ instanceId,
268
+ personaId: instance.personaId,
269
+ personaName: instance.personaName,
270
+ specialtyId: instance.specialtyId,
271
+ thinkingMode: instance.thinkingMode,
272
+ text: instance._accumulatedText,
273
+ costUsd: message.costUSD || 0,
274
+ inputTokens: message.inputTokens || 0,
275
+ outputTokens: message.outputTokens || 0,
276
+ duration: Date.now() - instance.startedAt
277
+ };
278
+
279
+ console.log(`[Actor] ${instanceId} completed (${result.duration}ms, $${result.costUsd.toFixed(4)})`);
280
+
281
+ if (instance._onComplete) {
282
+ try {
283
+ instance._onComplete(instanceId, result);
284
+ } catch (e) {
285
+ console.error(`[Actor] onComplete callback error for ${instanceId}:`, e.message);
286
+ }
287
+ }
288
+ }
289
+ }
290
+ } catch (error) {
291
+ instance.status = 'failed';
292
+ instance.activity = `失败: ${error.message}`;
293
+ console.error(`[Actor] ${instanceId} error:`, error.message);
294
+
295
+ if (instance._onError) {
296
+ try {
297
+ instance._onError(instanceId, error);
298
+ } catch (e) {
299
+ console.error(`[Actor] onError callback error for ${instanceId}:`, e.message);
300
+ }
301
+ }
302
+ }
303
+ }
304
+
305
+ /**
306
+ * 从消息中提取文本内容
307
+ */
308
+ function extractText(message) {
309
+ if (!message.message?.content) return '';
310
+ const content = message.message.content;
311
+ if (typeof content === 'string') return content;
312
+ if (Array.isArray(content)) {
313
+ return content
314
+ .filter(c => c.type === 'text')
315
+ .map(c => c.text)
316
+ .join('');
317
+ }
318
+ return '';
319
+ }
320
+
321
+ // =====================================================================
322
+ // Actor Release & Cleanup
323
+ // =====================================================================
324
+
325
+ /**
326
+ * 释放 Actor 实例(清理 session)
327
+ *
328
+ * @param {string} taskId
329
+ * @param {string} instanceId
330
+ * @param {object} [options]
331
+ * @param {boolean} [options.force=false] - 强制终止(abort)
332
+ */
333
+ export async function releaseActor(taskId, instanceId, options = {}) {
334
+ const instance = getActor(taskId, instanceId);
335
+ if (!instance) {
336
+ console.warn(`[Actor] Cannot release unknown actor: ${instanceId}`);
337
+ return;
338
+ }
339
+
340
+ const { force = false } = options;
341
+
342
+ // 关闭 input stream
343
+ if (!instance._inputStream.isDone) {
344
+ instance._inputStream.done();
345
+ }
346
+
347
+ // 强制终止
348
+ if (force && instance._abortController) {
349
+ instance._abortController.abort();
350
+ }
351
+
352
+ instance.status = 'done';
353
+ instance.activity = '已释放';
354
+
355
+ // 从 registry 移除,防止内存泄漏
356
+ const taskActors = actorRegistry.get(taskId);
357
+ if (taskActors) {
358
+ taskActors.delete(instanceId);
359
+ if (taskActors.size === 0) actorRegistry.delete(taskId);
360
+ }
361
+
362
+ console.log(`[Actor] Released ${instanceId}${force ? ' (forced)' : ''}`);
363
+ }
364
+
365
+ /**
366
+ * 释放指定 task 的全部 actor
367
+ *
368
+ * @param {string} taskId
369
+ * @param {object} [options]
370
+ * @param {boolean} [options.force=false]
371
+ */
372
+ export async function releaseAllActors(taskId, options = {}) {
373
+ const taskActors = actorRegistry.get(taskId);
374
+ if (!taskActors) return;
375
+
376
+ const promises = [];
377
+ for (const [instanceId] of taskActors) {
378
+ promises.push(releaseActor(taskId, instanceId, options));
379
+ }
380
+ await Promise.allSettled(promises);
381
+
382
+ actorRegistry.delete(taskId);
383
+ console.log(`[Actor] Released all actors for task ${taskId}`);
384
+ }
385
+
386
+ // =====================================================================
387
+ // Actor Query
388
+ // =====================================================================
389
+
390
+ /**
391
+ * 获取单个 actor 实例
392
+ * @param {string} taskId
393
+ * @param {string} instanceId
394
+ * @returns {ActorInstance|null}
395
+ */
396
+ export function getActor(taskId, instanceId) {
397
+ return actorRegistry.get(taskId)?.get(instanceId) || null;
398
+ }
399
+
400
+ /**
401
+ * 获取指定 task 的全部活跃 actor
402
+ * @param {string} taskId
403
+ * @returns {ActorInstance[]}
404
+ */
405
+ export function getActiveActors(taskId) {
406
+ const taskActors = actorRegistry.get(taskId);
407
+ if (!taskActors) return [];
408
+ return Array.from(taskActors.values()).filter(a => a.status === 'working');
409
+ }
410
+
411
+ /**
412
+ * 获取指定 task 的全部 actor(含已完成)
413
+ * @param {string} taskId
414
+ * @returns {ActorInstance[]}
415
+ */
416
+ export function getAllActors(taskId) {
417
+ const taskActors = actorRegistry.get(taskId);
418
+ if (!taskActors) return [];
419
+ return Array.from(taskActors.values());
420
+ }
421
+
422
+ /**
423
+ * 获取 actor 实例的公开状态(用于前端展示)
424
+ * @param {ActorInstance} instance
425
+ * @returns {object}
426
+ */
427
+ export function getActorPublicState(instance) {
428
+ return {
429
+ instanceId: instance.instanceId,
430
+ personaId: instance.personaId,
431
+ personaName: instance.personaName,
432
+ specialtyId: instance.specialtyId,
433
+ specialtyDisplayName: instance.specialtyDisplayName,
434
+ thinkingMode: instance.thinkingMode,
435
+ taskId: instance.taskId,
436
+ threadId: instance.threadId,
437
+ status: instance.status,
438
+ activity: instance.activity,
439
+ startedAt: instance.startedAt
440
+ };
441
+ }
442
+
443
+ /**
444
+ * 获取指定 task 全部 actor 的公开状态列表
445
+ * @param {string} taskId
446
+ * @returns {object[]}
447
+ */
448
+ export function getActorStates(taskId) {
449
+ return getAllActors(taskId).map(getActorPublicState);
450
+ }
451
+
452
+ // =====================================================================
453
+ // Error Classification (复用 V1 模式)
454
+ // =====================================================================
455
+
456
+ /**
457
+ * 判断 actor 错误是否可恢复
458
+ * @param {Error} error
459
+ * @returns {{ recoverable: boolean, reason: string }}
460
+ */
461
+ export function classifyActorError(error) {
462
+ const msg = error.message || '';
463
+ if (/context.*(window|limit|exceeded)|token.*limit|too.*(long|large)|max.*token/i.test(msg)) {
464
+ return { recoverable: true, reason: 'context_exceeded' };
465
+ }
466
+ if (/rate.?limit|429|overloaded|503|502|timeout|ECONNRESET|ETIMEDOUT/i.test(msg)) {
467
+ return { recoverable: true, reason: 'transient_api_error' };
468
+ }
469
+ if (/exited with code [1-9]/i.test(msg) && msg.length < 100) {
470
+ return { recoverable: true, reason: 'process_crashed' };
471
+ }
472
+ if (/spawn|ENOENT|not found/i.test(msg)) {
473
+ return { recoverable: false, reason: 'spawn_failed' };
474
+ }
475
+ return { recoverable: true, reason: 'unknown' };
476
+ }
477
+
478
+ export { actorRegistry };
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Conductor — 三段式 CLAUDE.md 生成器
3
+ *
4
+ * CLAUDE.md = 人物基底(persona base) + specialty 行为(specialty behavior) + 任务上下文(task context)
5
+ *
6
+ * 核心原则:一个实例一个文件。每个 persona × specialty 组合生成独立的 CLAUDE.md。
7
+ * Torvalds 做 planning 和 Torvalds 做 coding 是两个完全不同的文件。
8
+ */
9
+ import { getPersonaById } from './personas/index.js';
10
+ import {
11
+ getSpecialty,
12
+ getThinkingMode,
13
+ getPersonalizedBehavior,
14
+ getOutputFormat,
15
+ formatToolRules
16
+ } from './specialties/index.js';
17
+
18
+ /**
19
+ * 生成 Actor 的 CLAUDE.md 内容
20
+ *
21
+ * @param {object|string} personaOrId - persona 对象 或 persona ID
22
+ * @param {string} specialtyId - specialty ID (e.g. 'planning', 'coding', 'review')
23
+ * @param {object} taskContext - 任务上下文
24
+ * @param {string} taskContext.taskId - 任务 ID
25
+ * @param {string} taskContext.description - 任务描述
26
+ * @param {string} [taskContext.memoryPath] - memory.md 路径
27
+ * @param {string} [taskContext.planPath] - plan.json 路径
28
+ * @param {string} [taskContext.worktreePath] - worktree 路径 (coding actor 用)
29
+ * @param {string} [taskContext.reviewTarget] - 审查目标描述 (review actor 用)
30
+ * @param {string} [taskContext.assignedStep] - 分配的执行步骤 (coding actor 用)
31
+ * @param {string} [taskContext.scenario] - 场景名称
32
+ * @param {object} [taskContext.extra] - 额外上下文(自由格式)
33
+ * @returns {string} 完整的 CLAUDE.md 内容
34
+ */
35
+ export function generateActorCLAUDEmd(personaOrId, specialtyId, taskContext = {}) {
36
+ // 解析 persona
37
+ const persona = typeof personaOrId === 'string'
38
+ ? getPersonaById(personaOrId)
39
+ : personaOrId;
40
+
41
+ if (!persona) {
42
+ throw new Error(`Persona not found: ${personaOrId}`);
43
+ }
44
+
45
+ const specialty = getSpecialty(specialtyId);
46
+ if (!specialty) {
47
+ throw new Error(`Specialty not found: ${specialtyId}`);
48
+ }
49
+
50
+ // 三段生成
51
+ const section1 = generatePersonaBase(persona, specialty);
52
+ const section2 = generateSpecialtyBehavior(persona, specialty);
53
+ const section3 = generateTaskContext(persona, specialty, taskContext);
54
+
55
+ return `${section1}
56
+
57
+ ${section2}
58
+
59
+ ${section3}`.trim();
60
+ }
61
+
62
+ // =====================================================================
63
+ // 第一段:人物基底
64
+ // =====================================================================
65
+
66
+ /**
67
+ * 生成人物基底部分
68
+ */
69
+ function generatePersonaBase(persona, specialty) {
70
+ return `# 你是 ${persona.name} — 此刻你在做 ${specialty.displayName}
71
+
72
+ ${persona.base}`;
73
+ }
74
+
75
+ // =====================================================================
76
+ // 第二段:Specialty 行为(结合人物个性定制)
77
+ // =====================================================================
78
+
79
+ /**
80
+ * 生成 specialty 行为部分
81
+ */
82
+ function generateSpecialtyBehavior(persona, specialty) {
83
+ const thinkingMode = getThinkingMode(specialty.id);
84
+ const thinkingLabel = thinkingMode === 'divergent' ? '发散思维' : '收敛执行';
85
+
86
+ const personalizedBehavior = getPersonalizedBehavior(specialty.id, persona);
87
+ const outputFormat = getOutputFormat(specialty.id);
88
+ const toolRules = formatToolRules(specialty.id);
89
+
90
+ return `## 作为 ${specialty.displayName} 的你(${thinkingLabel})
91
+ ${personalizedBehavior}
92
+
93
+ ## 输出要求
94
+ ${outputFormat}
95
+
96
+ ## 工具使用
97
+ ${toolRules}`;
98
+ }
99
+
100
+ // =====================================================================
101
+ // 第三段:任务上下文(动态注入)
102
+ // =====================================================================
103
+
104
+ /**
105
+ * 生成任务上下文部分
106
+ */
107
+ function generateTaskContext(persona, specialty, ctx) {
108
+ const lines = ['## Task 上下文'];
109
+
110
+ if (ctx.taskId) {
111
+ lines.push(`- 任务 ID:${ctx.taskId}`);
112
+ }
113
+
114
+ if (ctx.description) {
115
+ lines.push(`- 任务描述:${ctx.description}`);
116
+ }
117
+
118
+ if (ctx.scenario) {
119
+ lines.push(`- 场景:${ctx.scenario}`);
120
+ }
121
+
122
+ if (ctx.assignedStep) {
123
+ lines.push(`- 执行步骤:${ctx.assignedStep}`);
124
+ }
125
+
126
+ if (ctx.reviewTarget) {
127
+ lines.push(`- 审查目标:${ctx.reviewTarget}`);
128
+ }
129
+
130
+ if (ctx.memoryPath) {
131
+ lines.push(`- 共享记忆:${ctx.memoryPath}`);
132
+ }
133
+
134
+ if (ctx.planPath) {
135
+ lines.push(`- 计划文件:${ctx.planPath}`);
136
+ }
137
+
138
+ // worktree 路径和纪律规则(coding actor 专用)
139
+ if (ctx.worktreePath) {
140
+ lines.push('');
141
+ lines.push('## Worktree 纪律');
142
+ lines.push(`- 代码工作目录:${ctx.worktreePath}`);
143
+ lines.push('- 所有代码操作必须在此目录内');
144
+ lines.push('- 禁止在主目录或 main 分支修改代码');
145
+ lines.push('- 完成后通过回调通知 Orchestrator');
146
+ }
147
+
148
+ // 额外上下文
149
+ if (ctx.extra) {
150
+ lines.push('');
151
+ lines.push('## 补充信息');
152
+ if (typeof ctx.extra === 'string') {
153
+ lines.push(ctx.extra);
154
+ } else {
155
+ for (const [key, value] of Object.entries(ctx.extra)) {
156
+ lines.push(`- ${key}:${value}`);
157
+ }
158
+ }
159
+ }
160
+
161
+ // 通用结尾指令
162
+ lines.push('');
163
+ lines.push('## 工作完成后');
164
+ lines.push('完成工作后,请输出你的产出结果。Orchestrator 会接收并处理。');
165
+
166
+ return lines.join('\n');
167
+ }
168
+
169
+ /**
170
+ * 生成 Actor 实例 ID
171
+ *
172
+ * @param {string} specialtyId - specialty ID
173
+ * @param {string} personaId - persona ID
174
+ * @param {string} taskId - task ID
175
+ * @param {string} [threadId] - 并行线程 ID(可选)
176
+ * @returns {string} 实例 ID,如 "planning-jobs-task-1" 或 "coding-torvalds-task-1-t1"
177
+ */
178
+ export function generateActorInstanceId(specialtyId, personaId, taskId, threadId) {
179
+ const base = `${specialtyId}-${personaId}-${taskId}`;
180
+ return threadId ? `${base}-${threadId}` : base;
181
+ }
182
+
183
+ /**
184
+ * 生成 Actor 的目录名(用于 .conductor/tasks/task-N/actors/ 下)
185
+ *
186
+ * @param {string} specialtyId
187
+ * @param {string} personaId
188
+ * @param {string} [threadId]
189
+ * @returns {string} 目录名,如 "planning-jobs" 或 "coding-torvalds-t1"
190
+ */
191
+ export function generateActorDirName(specialtyId, personaId, threadId) {
192
+ const base = `${specialtyId}-${personaId}`;
193
+ return threadId ? `${base}-${threadId}` : base;
194
+ }