@jetcrab/pi-desk-subagent 0.0.0-stage → 1.0.1-dev.9001

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,1290 @@
1
+ import { existsSync, realpathSync } from 'node:fs';
2
+ import { isAbsolute, join, relative, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { buildSessionContext, calculateContextTokens, createAgentSessionFromServices, createAgentSessionServices, createCodemodeExtension, createMcpExtension, createToolSearchExtension, getAgentDir, getLastAssistantUsage, ModelRuntime, SessionManager, SettingsManager } from '@earendil-works/pi-coding-agent';
5
+ import { isRetryableAssistantError, StringEnum } from '@earendil-works/pi-ai';
6
+ import { Type } from 'typebox';
7
+ import { bindSessionPlugin } from '@jetcrab/pi-desk-sdk';
8
+ import { discoverSubagentConfigs, isSubagentName } from './agent-config.js';
9
+ import { createExternalResearchExtension } from './external-research.js';
10
+ import { SubagentCapabilities } from './subagent-capabilities.js';
11
+ import { isCapabilityAllowed } from '@jetcrab/pi-desk-sdk/capabilities';
12
+ import { createResearchCoordinatorExtension, createResearchWorkerConfig, RESEARCH_WORKER_AGENT_TYPE } from './research-runtime.js';
13
+ import { SUBAGENT_COMPLETION_CUSTOM_TYPE, SUBAGENT_LAUNCH_STATE_CUSTOM_TYPE, SUBAGENT_RECORD_VERSION, SUBAGENT_SHUTDOWN_STATE_CUSTOM_TYPE } from './subagent-message.js';
14
+ const SUBAGENT_COMPLETION_NOTICE = '子代理结束后会异步主动通知结果并唤醒主代理。不要为等待结果调用 agent_list 查询状态,也不要通过 bash/sleep 等方式等待。可以继续独立工作;没有独立工作时结束当前轮,等待通知。';
15
+ export const CHILD_BOUNDARY = `# 子代理边界
16
+
17
+ - 你是由顶层主会话派发任务的 child subagent,只执行当前任务。
18
+ - 不启动、建议或编排其他子代理。
19
+ - 只在任务授权范围内操作;遇到未授权的产品、架构或范围决策时停止并在结果中说明。
20
+ - 最终回复遵循当前角色的输出要求;开发任务说明完成内容、修改文件和验证情况,只读任务直接返回分析或审查结果。`;
21
+ const SUBAGENT_PACKAGE_ROOT = resolve(fileURLToPath(new URL('../', import.meta.url)));
22
+ const CONTEXT_CHECKPOINT_TOOL = 'ctx';
23
+ export const RESEARCH_COORDINATOR_BOUNDARY = `# 子代理边界
24
+
25
+ - 你是由顶层主会话派发任务的 child subagent,只执行当前任务。
26
+ - 仅可通过 \`research_delegate\` 启动叶子 Research Worker;不得使用或模拟通用 Agent 委派。
27
+ - 只有至少两个范围互不依赖的研究轨道才可委派,Worker 数量由实际范围决定。
28
+ - 调用 \`research_delegate\` 后等待全部 Worker 终态,不在等待期间重复研究相同范围。
29
+ - 只在任务授权范围内操作;遇到未授权的产品、架构或范围决策时停止并在结果中说明。
30
+ - 最终回复遵循当前角色的输出要求;只读任务直接返回研究结果。`;
31
+ function isRecord(value) {
32
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
33
+ }
34
+ function canonicalPath(path) {
35
+ try {
36
+ return realpathSync(path);
37
+ }
38
+ catch {
39
+ return resolve(path);
40
+ }
41
+ }
42
+ function isInsidePath(parent, candidate) {
43
+ const path = relative(canonicalPath(parent), canonicalPath(candidate));
44
+ return path === '' || (!path.startsWith('..') && !isAbsolute(path));
45
+ }
46
+ export function excludeSubagentPackageExtensions(result, packageRoot = SUBAGENT_PACKAGE_ROOT) {
47
+ return {
48
+ ...result,
49
+ extensions: result.extensions.filter((extension) => !isAbsolute(extension.resolvedPath) || !isInsidePath(packageRoot, extension.resolvedPath))
50
+ };
51
+ }
52
+ export function childAllowedTools(tools) {
53
+ return [...new Set([...tools, CONTEXT_CHECKPOINT_TOOL])];
54
+ }
55
+ function isMcpToolName(name) {
56
+ return /^mcp__[A-Za-z0-9_]+__[A-Za-z0-9_]+$/.test(name);
57
+ }
58
+ function isTerminalStatus(value) {
59
+ return (value === 'completed' || value === 'failed' || value === 'stopped' || value === 'interrupted');
60
+ }
61
+ function launchDetails(value) {
62
+ if (!isRecord(value))
63
+ return undefined;
64
+ if (value.version !== SUBAGENT_RECORD_VERSION ||
65
+ value.kind !== 'launch' ||
66
+ typeof value.taskId !== 'string' ||
67
+ !isSubagentName(value.agentType) ||
68
+ typeof value.title !== 'string' ||
69
+ typeof value.sessionFile !== 'string' ||
70
+ typeof value.startedAt !== 'number') {
71
+ return undefined;
72
+ }
73
+ return value;
74
+ }
75
+ function terminalDetails(value) {
76
+ if (!isRecord(value))
77
+ return undefined;
78
+ if (value.version !== SUBAGENT_RECORD_VERSION ||
79
+ value.kind !== 'terminal' ||
80
+ typeof value.taskId !== 'string' ||
81
+ !isTerminalStatus(value.status) ||
82
+ typeof value.endedAt !== 'number' ||
83
+ typeof value.sessionFile !== 'string') {
84
+ return undefined;
85
+ }
86
+ return value;
87
+ }
88
+ function entryTimestamp(entry) {
89
+ const value = Date.parse(entry.timestamp);
90
+ return Number.isFinite(value) ? value : 0;
91
+ }
92
+ function historicalRecord(details, persistLifecycle) {
93
+ return {
94
+ taskId: details.taskId,
95
+ agentType: details.agentType,
96
+ title: details.title,
97
+ status: 'running',
98
+ sessionFile: details.sessionFile,
99
+ startedAt: details.startedAt,
100
+ endedAt: null,
101
+ session: null,
102
+ taskHandle: null,
103
+ taskInfoUnsubscribe: null,
104
+ pendingSteers: [],
105
+ steerReady: false,
106
+ stopRequested: false,
107
+ ownerTaskId: null,
108
+ notifyParent: !persistLifecycle,
109
+ persistLifecycle,
110
+ resolveRun: null
111
+ };
112
+ }
113
+ export function restoreSubagentRecords(branch) {
114
+ const records = new Map();
115
+ const lastEventAt = new Map();
116
+ for (const entry of branch) {
117
+ const launch = entry.type === 'message' &&
118
+ entry.message.role === 'toolResult' &&
119
+ (entry.message.toolName === 'agent' || entry.message.toolName === 'agent_resume')
120
+ ? { details: launchDetails(entry.message.details), persistLifecycle: false }
121
+ : entry.type === 'custom' &&
122
+ (entry.customType === SUBAGENT_LAUNCH_STATE_CUSTOM_TYPE ||
123
+ entry.customType === 'pi-super-subagent:launch:v1')
124
+ ? { details: launchDetails(entry.data), persistLifecycle: true }
125
+ : undefined;
126
+ if (launch?.details) {
127
+ records.set(launch.details.taskId, historicalRecord(launch.details, launch.persistLifecycle));
128
+ lastEventAt.set(launch.details.taskId, entryTimestamp(entry));
129
+ continue;
130
+ }
131
+ const details = entry.type === 'custom_message' &&
132
+ (entry.customType === SUBAGENT_COMPLETION_CUSTOM_TYPE ||
133
+ entry.customType === 'pi-super-subagent:completion:v1')
134
+ ? terminalDetails(entry.details)
135
+ : entry.type === 'custom' &&
136
+ (entry.customType === SUBAGENT_SHUTDOWN_STATE_CUSTOM_TYPE ||
137
+ entry.customType === 'pi-super-subagent:state:v1')
138
+ ? terminalDetails(entry.data)
139
+ : undefined;
140
+ if (!details)
141
+ continue;
142
+ const record = records.get(details.taskId);
143
+ if (!record)
144
+ continue;
145
+ record.status = details.status;
146
+ record.endedAt = details.endedAt;
147
+ record.sessionFile = details.sessionFile;
148
+ lastEventAt.set(details.taskId, entryTimestamp(entry));
149
+ }
150
+ for (const record of records.values()) {
151
+ if (record.status !== 'running')
152
+ continue;
153
+ record.status = 'interrupted';
154
+ record.endedAt = lastEventAt.get(record.taskId) ?? record.startedAt;
155
+ }
156
+ return records;
157
+ }
158
+ function activityForStatus(status) {
159
+ switch (status) {
160
+ case 'running':
161
+ return '执行中';
162
+ case 'completed':
163
+ return '执行完成';
164
+ case 'failed':
165
+ return '执行失败';
166
+ case 'stopped':
167
+ return '已停止';
168
+ case 'interrupted':
169
+ return '执行中断';
170
+ }
171
+ }
172
+ const taskNumberFormatter = new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 });
173
+ export function normalizeRetryableAbortedAssistantMessage(message, stopRequested) {
174
+ if (stopRequested || message.stopReason !== 'aborted' || !message.errorMessage)
175
+ return undefined;
176
+ const retryCandidate = { ...message, stopReason: 'error' };
177
+ return isRetryableAssistantError(retryCandidate) ? retryCandidate : undefined;
178
+ }
179
+ export function createRetryCompatibilityExtension(isStopRequested) {
180
+ return (pi) => {
181
+ pi.on('message_end', (event, ctx) => {
182
+ if (event.message.role !== 'assistant')
183
+ return;
184
+ const message = normalizeRetryableAbortedAssistantMessage(event.message, isStopRequested(ctx.sessionManager.getSessionId()));
185
+ if (!message)
186
+ return;
187
+ console.warn('[pi-desk-subagent] 瞬时流中断交由 AgentSession 自动重试', {
188
+ sessionId: ctx.sessionManager.getSessionId(),
189
+ error: message.errorMessage
190
+ });
191
+ return { message };
192
+ });
193
+ };
194
+ }
195
+ function taskMessageCounts(entries) {
196
+ let user = 0;
197
+ let total = 0;
198
+ for (const entry of entries) {
199
+ if (entry.type === 'message') {
200
+ if (entry.message.role === 'custom' && !entry.message.display)
201
+ continue;
202
+ if (entry.message.role !== 'user' &&
203
+ entry.message.role !== 'assistant' &&
204
+ entry.message.role !== 'toolResult' &&
205
+ entry.message.role !== 'bashExecution' &&
206
+ entry.message.role !== 'custom') {
207
+ continue;
208
+ }
209
+ total += 1;
210
+ if (entry.message.role === 'user')
211
+ user += 1;
212
+ continue;
213
+ }
214
+ if (entry.type === 'custom_message') {
215
+ if (entry.display)
216
+ total += 1;
217
+ continue;
218
+ }
219
+ if (entry.type === 'compaction')
220
+ total += 1;
221
+ }
222
+ return { user, total };
223
+ }
224
+ function taskInfo(model, usage, sessionFile, entries) {
225
+ const info = [];
226
+ if (model) {
227
+ const context = usage
228
+ ? `${usage.tokens === null ? '未计算' : taskNumberFormatter.format(usage.tokens)}/${taskNumberFormatter.format(usage.contextWindow)} tokens`
229
+ : null;
230
+ info.push({
231
+ label: '运行信息',
232
+ value: [model.name || model.id, context].filter(Boolean).join(' · ')
233
+ });
234
+ }
235
+ const counts = taskMessageCounts(entries);
236
+ info.push({ label: '消息数', value: `${counts.user}/${counts.total}(用户/总数)` });
237
+ info.push({ label: '会话文件', value: sessionFile });
238
+ return info;
239
+ }
240
+ function currentTaskInfo(session) {
241
+ if (!session?.sessionFile)
242
+ return [];
243
+ let usage = undefined;
244
+ try {
245
+ usage = session.getContextUsage();
246
+ }
247
+ catch {
248
+ // Session 初始化期间用量可能暂不可得,模型信息仍可展示。
249
+ }
250
+ return taskInfo(session.model, usage, session.sessionFile, session.sessionManager.getBranch());
251
+ }
252
+ function createHistoricalTaskInfoLoader(sessionFile, modelRuntime) {
253
+ return () => {
254
+ const sessionManager = SessionManager.open(sessionFile);
255
+ const entries = sessionManager.getEntries();
256
+ const sessionContext = buildSessionContext(entries, sessionManager.getLeafId());
257
+ const model = sessionContext.model;
258
+ if (!model)
259
+ return taskInfo(undefined, undefined, sessionFile, sessionManager.getBranch());
260
+ const configuredModel = modelRuntime.getModel(model.provider, model.modelId);
261
+ const usage = getLastAssistantUsage(sessionManager.buildContextEntries());
262
+ const contextUsage = usage && configuredModel
263
+ ? { tokens: calculateContextTokens(usage), contextWindow: configuredModel.contextWindow }
264
+ : undefined;
265
+ return taskInfo(configuredModel ?? { id: model.modelId }, contextUsage, sessionFile, sessionManager.getBranch());
266
+ };
267
+ }
268
+ function taskSnapshot(record, modelRuntime) {
269
+ return {
270
+ taskId: record.taskId,
271
+ taskKind: '子代理',
272
+ taskType: record.agentType,
273
+ title: record.title,
274
+ info: currentTaskInfo(record.session),
275
+ ...(record.session
276
+ ? {}
277
+ : { infoLoader: createHistoricalTaskInfoLoader(record.sessionFile, modelRuntime) }),
278
+ status: record.status,
279
+ activity: activityForStatus(record.status),
280
+ startedAt: record.startedAt,
281
+ endedAt: record.endedAt,
282
+ detailSource: record.sessionFile
283
+ ? { kind: 'pi-conversation', sessionFile: record.sessionFile }
284
+ : null,
285
+ interrupt: null
286
+ };
287
+ }
288
+ function safePathPart(value) {
289
+ return value.replace(/[^a-zA-Z0-9._-]/g, '_');
290
+ }
291
+ function finalAssistant(session) {
292
+ const entries = session.sessionManager.getEntries();
293
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
294
+ const entry = entries[index];
295
+ if (entry.type !== 'message' || entry.message.role !== 'assistant')
296
+ continue;
297
+ return {
298
+ text: entry.message.content
299
+ .filter((part) => part.type === 'text')
300
+ .map((part) => part.text)
301
+ .join(''),
302
+ stopReason: entry.message.stopReason,
303
+ errorMessage: entry.message.errorMessage
304
+ };
305
+ }
306
+ return null;
307
+ }
308
+ function lastAssistantText(session) {
309
+ const entries = session.sessionManager.getEntries();
310
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
311
+ const entry = entries[index];
312
+ if (entry.type !== 'message' || entry.message.role !== 'assistant')
313
+ continue;
314
+ const text = entry.message.content
315
+ .filter((part) => part.type === 'text')
316
+ .map((part) => part.text)
317
+ .join('')
318
+ .trim();
319
+ if (text)
320
+ return text;
321
+ }
322
+ return null;
323
+ }
324
+ async function closeChildSession(session) {
325
+ try {
326
+ // AgentSession.dispose() 不发送关闭事件,扩展须先在有效上下文中释放资源。
327
+ await session.extensionRunner.emit({ type: 'session_shutdown', reason: 'quit' });
328
+ }
329
+ catch (error) {
330
+ console.warn(`[pi-desk-subagent] 子会话 ${session.sessionId} 扩展关闭失败`, error);
331
+ }
332
+ finally {
333
+ session.dispose();
334
+ }
335
+ }
336
+ export class SubagentRuntime {
337
+ pi;
338
+ modelRuntime;
339
+ agentDir;
340
+ plugin;
341
+ records = new Map();
342
+ activeRuns = new Set();
343
+ pendingCreates = new Set();
344
+ stopping = false;
345
+ configs = new Map();
346
+ capabilityRules = { tools: {}, skills: {}, capabilities: {} };
347
+ childCapabilities = new WeakMap();
348
+ activeParent = null;
349
+ sessionActive = false;
350
+ constructor(pi, modelRuntime, agentDir = getAgentDir()) {
351
+ this.pi = pi;
352
+ this.modelRuntime = modelRuntime;
353
+ this.agentDir = agentDir;
354
+ this.plugin = bindSessionPlugin(pi, 'subagent');
355
+ this.plugin.onCapabilitiesChange((rules) => {
356
+ this.capabilityRules = rules;
357
+ if (!this.sessionActive)
358
+ return;
359
+ this.registerTools();
360
+ for (const record of this.records.values()) {
361
+ if (record.session)
362
+ this.childCapabilities.get(record.session)?.refresh();
363
+ }
364
+ });
365
+ }
366
+ async startSession(ctx) {
367
+ this.sessionActive = true;
368
+ this.activeParent = {
369
+ cwd: ctx.cwd,
370
+ sessionId: ctx.sessionManager.getSessionId(),
371
+ sessionDir: ctx.sessionManager.getSessionDir(),
372
+ sessionFile: ctx.sessionManager.getSessionFile(),
373
+ model: ctx.model,
374
+ thinkingLevel: ctx.thinkingLevel,
375
+ projectTrusted: ctx.isProjectTrusted()
376
+ };
377
+ this.records.clear();
378
+ for (const [taskId, record] of restoreSubagentRecords(ctx.sessionManager.getBranch())) {
379
+ this.records.set(taskId, record);
380
+ }
381
+ if (this.records.size > 0) {
382
+ this.plugin.reportTasks([...this.records.values()].map((record) => taskSnapshot(record, this.modelRuntime)));
383
+ }
384
+ this.refreshConfigs();
385
+ if (this.configs.size === 0)
386
+ this.registerTools();
387
+ }
388
+ refreshConfigs() {
389
+ const parent = this.activeParent;
390
+ if (!this.sessionActive || !parent)
391
+ return;
392
+ const discovery = discoverSubagentConfigs({
393
+ cwd: parent.cwd,
394
+ projectTrusted: parent.projectTrusted,
395
+ agentDir: this.agentDir
396
+ });
397
+ const previous = [...this.configs.values()].map(({ name, description }) => [name, description]);
398
+ this.configs = discovery.configs;
399
+ for (const diagnostic of discovery.diagnostics) {
400
+ console.warn(`[pi-desk-subagent] ${diagnostic}`);
401
+ }
402
+ const current = [...this.configs.values()].map(({ name, description }) => [name, description]);
403
+ if (JSON.stringify(previous) !== JSON.stringify(current))
404
+ this.registerTools();
405
+ }
406
+ async beforeTree(signal) {
407
+ if (signal.aborted)
408
+ return { cancel: true };
409
+ this.stopping = true;
410
+ // 失效创建期 owner;即使导航被其他扩展取消,迟到创建也不能重新启动。
411
+ if (this.activeParent)
412
+ this.activeParent = { ...this.activeParent };
413
+ try {
414
+ await this.stopAndWait(false);
415
+ return signal.aborted ? { cancel: true } : undefined;
416
+ }
417
+ finally {
418
+ this.stopping = false;
419
+ }
420
+ }
421
+ async shutdown() {
422
+ this.sessionActive = false;
423
+ await this.stopAndWait(true);
424
+ this.activeParent = null;
425
+ this.configs.clear();
426
+ }
427
+ async stopAndWait(shutdown) {
428
+ const running = [...this.records.values()].filter((record) => record.status === 'running' && record.session);
429
+ const endedAt = Date.now();
430
+ for (const record of running) {
431
+ if (shutdown) {
432
+ this.pi.appendEntry(SUBAGENT_SHUTDOWN_STATE_CUSTOM_TYPE, this.makeTerminalDetails(record, 'interrupted', endedAt));
433
+ record.stopRequested = true;
434
+ }
435
+ }
436
+ const results = await Promise.allSettled(running.map((record) => shutdown
437
+ ? record.session.abort()
438
+ : record.stopRequested
439
+ ? undefined
440
+ : this.requestStop(record)));
441
+ const failure = results.find((result) => result.status === 'rejected');
442
+ if (failure?.status === 'rejected' && !shutdown)
443
+ throw failure.reason;
444
+ await Promise.allSettled([...this.pendingCreates]);
445
+ await Promise.allSettled([...this.activeRuns]);
446
+ for (const record of running) {
447
+ if (record.status === 'running') {
448
+ if (!shutdown)
449
+ throw new Error(`Agent ${record.taskId} 尚未完成关闭`);
450
+ this.finishRecord(record, 'interrupted', null, '父会话已关闭', false, endedAt);
451
+ }
452
+ }
453
+ }
454
+ registerTools() {
455
+ const configs = [...this.configs.values()].filter((config) => this.agentAllowed(config.name));
456
+ const names = configs.map((config) => config.name);
457
+ if (names.length > 0) {
458
+ const agentRoles = configs
459
+ .map((config) => `- ${config.name}: ${config.description}`)
460
+ .join('\n');
461
+ const roleGuide = `可用子代理及职责:\n${agentRoles}`;
462
+ const nameSchema = StringEnum(names, {
463
+ description: '执行任务的子代理类型'
464
+ });
465
+ this.pi.registerTool({
466
+ name: 'agent',
467
+ exposure: 'model-only',
468
+ label: 'Agent',
469
+ description: `启动后台子代理执行派发任务。当前可用类型:${names.join(', ')}。`,
470
+ promptSnippet: '启动后台子代理执行派发任务',
471
+ promptGuidelines: [roleGuide],
472
+ parameters: Type.Object({
473
+ subagent_type: nameSchema,
474
+ prompt: Type.String({ description: '完整、自包含的任务说明' }),
475
+ description: Type.String({ description: '用于任务中心显示的简短任务标题' })
476
+ }),
477
+ execute: async (_toolCallId, params, _signal, _onUpdate, ctx) => this.startAgent(params.subagent_type, params.prompt, params.description, ctx)
478
+ });
479
+ }
480
+ this.pi.registerTool({
481
+ name: 'agent_resume',
482
+ exposure: 'model-only',
483
+ label: 'Resume Agent',
484
+ description: '继续当前父会话 branch 中已经进入终态的子代理,并沿用原 Conversation。',
485
+ parameters: Type.Object({
486
+ agent_id: Type.String({ description: '完整 Agent ID 或唯一短前缀' }),
487
+ prompt: Type.String({ description: '追加到原 Conversation 的新任务说明' })
488
+ }),
489
+ execute: async (_toolCallId, params, _signal, _onUpdate, ctx) => this.resumeAgent(params.agent_id, params.prompt, ctx)
490
+ });
491
+ this.pi.registerTool({
492
+ name: 'agent_steer',
493
+ label: 'Steer Agent',
494
+ description: '向当前 running 子代理补充条件,不停止、重建或恢复子代理。',
495
+ parameters: Type.Object({
496
+ agent_id: Type.String({ description: '完整 Agent ID 或唯一短前缀' }),
497
+ prompt: Type.String({ description: '要补充给子代理的条件或要求' })
498
+ }),
499
+ execute: async (_toolCallId, params) => this.steerAgent(params.agent_id, params.prompt)
500
+ });
501
+ this.pi.registerTool({
502
+ name: 'agent_list',
503
+ label: 'List Agents',
504
+ description: '查询当前父会话分支的子代理状态;可传 agent_id,默认按最近启动时间返回最多20条记录。',
505
+ parameters: Type.Object({
506
+ agent_id: Type.Optional(Type.String({ description: '完整 Agent ID 或唯一短前缀;省略时列出最近记录' }))
507
+ }),
508
+ execute: async (_toolCallId, params) => this.listAgents(params.agent_id)
509
+ });
510
+ this.pi.registerTool({
511
+ name: 'agent_stop',
512
+ label: 'Stop Agent',
513
+ description: '立即请求中断当前 running 子代理。',
514
+ promptGuidelines: ['agent_stop 成功返回只表示已请求中断,最终状态以后续终态通知为准。'],
515
+ parameters: Type.Object({
516
+ agent_id: Type.String({ description: '完整 Agent ID 或唯一短前缀' })
517
+ }),
518
+ execute: async (_toolCallId, params) => this.stopAgent(params.agent_id)
519
+ });
520
+ const managed = ['agent', 'agent_resume', 'agent_steer'];
521
+ const active = this.pi.getActiveTools().filter((name) => !managed.includes(name));
522
+ this.pi.setActiveTools(names.length > 0 ? [...active, ...managed] : active);
523
+ }
524
+ requireParent(ctx) {
525
+ const parent = this.activeParent;
526
+ if (!this.sessionActive ||
527
+ this.stopping ||
528
+ !parent ||
529
+ parent.sessionId !== ctx.sessionManager.getSessionId() ||
530
+ parent.cwd !== ctx.cwd) {
531
+ throw new Error('当前父会话已变化');
532
+ }
533
+ parent.model = ctx.model;
534
+ parent.thinkingLevel = ctx.thinkingLevel;
535
+ return parent;
536
+ }
537
+ requireText(value, field) {
538
+ const text = value.trim();
539
+ if (!text)
540
+ throw new Error(`${field} 不能为空`);
541
+ return text;
542
+ }
543
+ resolveRecord(agentIdOrPrefix, records = [...this.records.values()]) {
544
+ const query = this.requireText(agentIdOrPrefix, 'agent_id').toLowerCase();
545
+ const exact = records.find((record) => record.taskId.toLowerCase() === query);
546
+ if (exact)
547
+ return exact;
548
+ const matches = records.filter((record) => record.taskId.toLowerCase().startsWith(query));
549
+ if (matches.length === 0)
550
+ throw new Error(`未知 Agent ID:${agentIdOrPrefix}`);
551
+ if (matches.length > 1)
552
+ throw new Error(`Agent ID 前缀不唯一:${agentIdOrPrefix}`);
553
+ return matches[0];
554
+ }
555
+ agentAllowed(agentType) {
556
+ const type = agentType === RESEARCH_WORKER_AGENT_TYPE ? 'research' : agentType;
557
+ return isCapabilityAllowed(this.capabilityRules.capabilities.agents, type);
558
+ }
559
+ resolveAgentConfig(agentType) {
560
+ if (!this.agentAllowed(agentType))
561
+ return undefined;
562
+ if (agentType !== RESEARCH_WORKER_AGENT_TYPE)
563
+ return this.configs.get(agentType);
564
+ const research = this.configs.get('research');
565
+ return research ? createResearchWorkerConfig(research) : undefined;
566
+ }
567
+ createSessionManager(parent) {
568
+ const childSessionDir = parent.sessionDir
569
+ ? join(parent.sessionDir, 'subagents', safePathPart(parent.sessionId))
570
+ : join(parent.cwd, 'temp', 'pi', 'pi-desk-subagent', safePathPart(parent.sessionId));
571
+ return SessionManager.create(parent.cwd, childSessionDir, parent.sessionFile ? { parentSession: parent.sessionFile } : undefined);
572
+ }
573
+ async startRecord(config, title, prompt, parent, options) {
574
+ const session = await this.createOwnedChildSession(config, parent, this.createSessionManager(parent));
575
+ if (!this.sessionActive || this.stopping || this.activeParent !== parent) {
576
+ await closeChildSession(session);
577
+ throw new Error('当前父会话已变化,子代理未启动');
578
+ }
579
+ const sessionFile = session.sessionFile;
580
+ if (!this.agentAllowed(config.name)) {
581
+ await closeChildSession(session);
582
+ throw new Error(`当前能力模式不允许 ${config.name} Agent`);
583
+ }
584
+ if (!sessionFile) {
585
+ await closeChildSession(session);
586
+ throw new Error('子代理没有持久 Session 文件');
587
+ }
588
+ const record = {
589
+ taskId: session.sessionId,
590
+ agentType: config.name,
591
+ title,
592
+ status: 'running',
593
+ sessionFile,
594
+ startedAt: Date.now(),
595
+ endedAt: null,
596
+ session,
597
+ taskHandle: null,
598
+ taskInfoUnsubscribe: null,
599
+ pendingSteers: [],
600
+ steerReady: false,
601
+ stopRequested: false,
602
+ ownerTaskId: options.ownerTaskId,
603
+ notifyParent: options.notifyParent,
604
+ persistLifecycle: options.persistLifecycle,
605
+ resolveRun: options.resolveRun
606
+ };
607
+ this.records.set(record.taskId, record);
608
+ let launchPersisted = false;
609
+ try {
610
+ if (record.persistLifecycle) {
611
+ this.pi.appendEntry(SUBAGENT_LAUNCH_STATE_CUSTOM_TYPE, this.makeLaunchDetails(record));
612
+ launchPersisted = true;
613
+ }
614
+ this.launchRecord(record, prompt);
615
+ return record;
616
+ }
617
+ catch (error) {
618
+ record.taskInfoUnsubscribe?.();
619
+ record.taskInfoUnsubscribe = null;
620
+ this.records.delete(record.taskId);
621
+ if (launchPersisted) {
622
+ try {
623
+ this.pi.appendEntry(SUBAGENT_SHUTDOWN_STATE_CUSTOM_TYPE, this.makeTerminalDetails(record, 'failed'));
624
+ }
625
+ catch (persistError) {
626
+ console.warn(`[pi-desk-subagent] Agent ${record.taskId} 启动失败终态写入失败`, persistError);
627
+ }
628
+ }
629
+ await closeChildSession(session);
630
+ throw error;
631
+ }
632
+ }
633
+ async startAgent(agentType, prompt, description, ctx) {
634
+ const parent = this.requireParent(ctx);
635
+ this.refreshConfigs();
636
+ const config = this.resolveAgentConfig(agentType);
637
+ if (!config)
638
+ throw new Error(`当前会话没有可用的 ${agentType} Agent`);
639
+ const record = await this.startRecord(config, this.requireText(description, 'description'), this.requireText(prompt, 'prompt'), parent, {
640
+ ownerTaskId: null,
641
+ notifyParent: true,
642
+ persistLifecycle: false,
643
+ resolveRun: null
644
+ });
645
+ return {
646
+ content: [
647
+ {
648
+ type: 'text',
649
+ text: `子代理已在后台启动。\n\nAgent ID: ${record.taskId}\n${SUBAGENT_COMPLETION_NOTICE}`
650
+ }
651
+ ],
652
+ details: this.makeLaunchDetails(record)
653
+ };
654
+ }
655
+ async resumeAgent(agentId, prompt, ctx) {
656
+ const parent = this.requireParent(ctx);
657
+ this.refreshConfigs();
658
+ const record = this.resolveRecord(agentId);
659
+ if (record.status === 'running')
660
+ throw new Error(`Agent ${record.taskId} 仍在运行`);
661
+ if (!existsSync(record.sessionFile)) {
662
+ throw new Error(`子代理 Session 文件不存在:${record.sessionFile}`);
663
+ }
664
+ const config = this.resolveAgentConfig(record.agentType);
665
+ if (!config)
666
+ throw new Error(`当前会话没有可用的 ${record.agentType} Agent`);
667
+ const taskPrompt = this.requireText(prompt, 'prompt');
668
+ const sessionManager = SessionManager.open(record.sessionFile);
669
+ if (sessionManager.getSessionId() !== record.taskId) {
670
+ throw new Error(`子代理 Session ID 与 Agent ID 不一致:${record.taskId}`);
671
+ }
672
+ const session = await this.createOwnedChildSession(config, parent, sessionManager);
673
+ if (!this.sessionActive || this.stopping || this.activeParent !== parent) {
674
+ await closeChildSession(session);
675
+ throw new Error('当前父会话已变化,子代理未启动');
676
+ }
677
+ if (!this.agentAllowed(record.agentType)) {
678
+ await closeChildSession(session);
679
+ throw new Error(`当前能力模式不允许 ${record.agentType} Agent`);
680
+ }
681
+ const previous = {
682
+ status: record.status,
683
+ startedAt: record.startedAt,
684
+ endedAt: record.endedAt
685
+ };
686
+ record.status = 'running';
687
+ record.startedAt = Date.now();
688
+ record.endedAt = null;
689
+ record.session = session;
690
+ record.pendingSteers = [];
691
+ record.steerReady = false;
692
+ record.stopRequested = false;
693
+ record.ownerTaskId = null;
694
+ record.notifyParent = true;
695
+ record.persistLifecycle = false;
696
+ record.resolveRun = null;
697
+ try {
698
+ this.launchRecord(record, taskPrompt);
699
+ }
700
+ catch (error) {
701
+ record.taskInfoUnsubscribe?.();
702
+ record.taskInfoUnsubscribe = null;
703
+ record.status = previous.status;
704
+ record.startedAt = previous.startedAt;
705
+ record.endedAt = previous.endedAt;
706
+ record.session = null;
707
+ await closeChildSession(session);
708
+ throw error;
709
+ }
710
+ return {
711
+ content: [
712
+ {
713
+ type: 'text',
714
+ text: `子代理已恢复运行。\n\nAgent ID: ${record.taskId}\n${SUBAGENT_COMPLETION_NOTICE}`
715
+ }
716
+ ],
717
+ details: this.makeLaunchDetails(record)
718
+ };
719
+ }
720
+ async steerAgent(agentId, prompt) {
721
+ const record = this.resolveRecord(agentId);
722
+ if (!this.agentAllowed(record.agentType))
723
+ throw new Error(`当前能力模式不允许 ${record.agentType} Agent`);
724
+ const text = this.requireText(prompt, 'prompt');
725
+ if (record.status !== 'running' || !record.session) {
726
+ throw new Error(`Agent ${record.taskId} 当前不是 running`);
727
+ }
728
+ if (!record.steerReady) {
729
+ record.pendingSteers.push(text);
730
+ return {
731
+ content: [
732
+ {
733
+ type: 'text',
734
+ text: `补充要求已排队,子代理开始处理后会收到。\n\nAgent ID: ${record.taskId}`
735
+ }
736
+ ],
737
+ details: {
738
+ taskId: record.taskId,
739
+ agentType: record.agentType,
740
+ title: record.title,
741
+ delivery: 'queued'
742
+ }
743
+ };
744
+ }
745
+ await record.session.steer(text);
746
+ return {
747
+ content: [
748
+ {
749
+ type: 'text',
750
+ text: `补充要求已发送。\n\nAgent ID: ${record.taskId}`
751
+ }
752
+ ],
753
+ details: {
754
+ taskId: record.taskId,
755
+ agentType: record.agentType,
756
+ title: record.title,
757
+ delivery: 'delivered'
758
+ }
759
+ };
760
+ }
761
+ async stopAgent(agentId) {
762
+ const record = this.resolveRecord(agentId);
763
+ if (record.status !== 'running' || !record.session) {
764
+ throw new Error(`Agent ${record.taskId} 当前不是 running`);
765
+ }
766
+ await this.requestStop(record);
767
+ return {
768
+ content: [
769
+ {
770
+ type: 'text',
771
+ text: `已请求停止子代理。最终状态以后续结束通知为准。\n\nAgent ID: ${record.taskId}`
772
+ }
773
+ ],
774
+ details: {
775
+ taskId: record.taskId,
776
+ agentType: record.agentType,
777
+ title: record.title
778
+ }
779
+ };
780
+ }
781
+ async requestStop(record) {
782
+ if (record.stopRequested)
783
+ throw new Error(`Agent ${record.taskId} 已在中断中`);
784
+ if (record.status !== 'running' || !record.session) {
785
+ throw new Error(`Agent ${record.taskId} 当前不是 running`);
786
+ }
787
+ record.stopRequested = true;
788
+ try {
789
+ await record.session.abort();
790
+ }
791
+ catch (error) {
792
+ record.stopRequested = false;
793
+ throw error;
794
+ }
795
+ }
796
+ async stopOwnedRecords(ownerTaskId) {
797
+ const running = [...this.records.values()].filter((record) => record.ownerTaskId === ownerTaskId &&
798
+ record.status === 'running' &&
799
+ record.session &&
800
+ !record.stopRequested);
801
+ await Promise.allSettled(running.map((record) => this.requestStop(record)));
802
+ }
803
+ updateRootActivity(root, activity) {
804
+ if (this.records.get(root.taskId) !== root || root.status !== 'running' || !root.taskHandle)
805
+ return;
806
+ root.taskHandle.update({ activity });
807
+ }
808
+ async runDelegatedResearchWorker(ownerTaskId, task, signal) {
809
+ const parent = this.activeParent;
810
+ this.refreshConfigs();
811
+ const research = this.resolveAgentConfig('research');
812
+ if (!this.sessionActive || this.stopping || !parent)
813
+ throw new Error('当前父会话已变化');
814
+ if (!research)
815
+ throw new Error('当前会话没有可用的 research Agent');
816
+ let resolveRun;
817
+ const settled = new Promise((resolve) => {
818
+ resolveRun = resolve;
819
+ });
820
+ const record = await this.startRecord(createResearchWorkerConfig(research), task.title, task.prompt, parent, {
821
+ ownerTaskId,
822
+ notifyParent: false,
823
+ persistLifecycle: true,
824
+ resolveRun: (outcome) => resolveRun?.(outcome)
825
+ });
826
+ if (signal?.aborted && record.status === 'running' && record.session) {
827
+ await this.requestStop(record);
828
+ }
829
+ return settled;
830
+ }
831
+ async delegateResearchWorkers(ownerTaskId, tasks, signal, onProgress) {
832
+ if (tasks.length < 2)
833
+ throw new Error('research_delegate 至少需要两个研究任务');
834
+ const root = this.records.get(ownerTaskId);
835
+ if (!root || root.status !== 'running' || root.agentType !== 'research') {
836
+ throw new Error(`Research Root ${ownerTaskId} 当前不可用`);
837
+ }
838
+ if (signal?.aborted)
839
+ throw new Error('Research 委派已取消');
840
+ const total = tasks.length;
841
+ const startedAt = Date.now();
842
+ let completed = 0;
843
+ console.info('[pi-desk-subagent] Research Worker 批次启动', {
844
+ rootTaskId: ownerTaskId,
845
+ total
846
+ });
847
+ this.updateRootActivity(root, `正在启动 ${total} 个 Research Worker`);
848
+ const abort = () => {
849
+ void this.stopOwnedRecords(ownerTaskId).catch((error) => {
850
+ console.warn(`[pi-desk-subagent] Root ${ownerTaskId} 中断 Worker 失败`, error);
851
+ });
852
+ };
853
+ signal?.addEventListener('abort', abort, { once: true });
854
+ try {
855
+ const outcomes = await Promise.all(tasks.map(async (task) => {
856
+ let outcome;
857
+ try {
858
+ outcome = await this.runDelegatedResearchWorker(ownerTaskId, task, signal);
859
+ }
860
+ catch (error) {
861
+ outcome = {
862
+ taskId: null,
863
+ title: task.title,
864
+ status: 'failed',
865
+ result: null,
866
+ error: error instanceof Error ? error.message : String(error),
867
+ sessionFile: null
868
+ };
869
+ }
870
+ completed += 1;
871
+ onProgress(completed, total);
872
+ this.updateRootActivity(root, `等待 Research Worker:${completed}/${total} 已进入终态`);
873
+ return outcome;
874
+ }));
875
+ this.updateRootActivity(root, `正在汇总 ${total} 个 Research Worker 结果`);
876
+ console.info('[pi-desk-subagent] Research Worker 批次结束', {
877
+ rootTaskId: ownerTaskId,
878
+ total,
879
+ completed: outcomes.filter((outcome) => outcome.status === 'completed').length,
880
+ elapsedMs: Date.now() - startedAt
881
+ });
882
+ return outcomes;
883
+ }
884
+ finally {
885
+ signal?.removeEventListener('abort', abort);
886
+ }
887
+ }
888
+ listAgents(agentId) {
889
+ const visible = [...this.records.values()].filter((record) => record.agentType !== RESEARCH_WORKER_AGENT_TYPE);
890
+ const records = agentId?.trim()
891
+ ? [this.resolveRecord(agentId, visible)]
892
+ : visible.sort((left, right) => right.startedAt - left.startedAt).slice(0, 20);
893
+ const text = [
894
+ `下面是最近执行的${records.length}条记录。`,
895
+ records.length === 0
896
+ ? '当前会话分支没有子代理。'
897
+ : records
898
+ .map((record) => `- ${record.taskId} · ${record.agentType} · ${record.status} · ${record.title}`)
899
+ .join('\n')
900
+ ].join('\n\n');
901
+ return {
902
+ content: [{ type: 'text', text }],
903
+ details: {
904
+ agents: records.map(({ taskId, agentType, title, status, startedAt }) => ({
905
+ taskId,
906
+ agentType,
907
+ title,
908
+ status,
909
+ startedAt
910
+ }))
911
+ }
912
+ };
913
+ }
914
+ async createOwnedChildSession(config, parent, sessionManager) {
915
+ const creation = (async () => {
916
+ const session = await this.createChildSession(config, parent, sessionManager);
917
+ if (!this.sessionActive || this.stopping || this.activeParent !== parent) {
918
+ await closeChildSession(session);
919
+ throw new Error('当前父会话已变化,子代理未启动');
920
+ }
921
+ return session;
922
+ })();
923
+ this.pendingCreates.add(creation);
924
+ try {
925
+ return await creation;
926
+ }
927
+ finally {
928
+ this.pendingCreates.delete(creation);
929
+ }
930
+ }
931
+ async createChildSession(config, parent, sessionManager) {
932
+ const agentDir = this.agentDir;
933
+ const settingsManager = SettingsManager.create(parent.cwd, agentDir);
934
+ settingsManager.setProjectTrusted(parent.projectTrusted);
935
+ const capabilityRuntime = new SubagentCapabilities(() => this.capabilityRules);
936
+ const extensionFactories = [
937
+ ...(config.tools.includes('codemode')
938
+ ? [
939
+ {
940
+ name: 'codemode',
941
+ builtin: true,
942
+ replaceable: true,
943
+ factory: createCodemodeExtension()
944
+ }
945
+ ]
946
+ : []),
947
+ ...(config.tools.includes('tool_search')
948
+ ? [
949
+ {
950
+ name: 'tool-search',
951
+ builtin: true,
952
+ replaceable: true,
953
+ factory: createToolSearchExtension()
954
+ }
955
+ ]
956
+ : []),
957
+ ...(config.tools.some(isMcpToolName)
958
+ ? [{ name: 'mcp', builtin: true, replaceable: true, factory: createMcpExtension() }]
959
+ : []),
960
+ {
961
+ name: 'subagent-capabilities',
962
+ hidden: true,
963
+ factory: (pi) => capabilityRuntime.extension(pi)
964
+ },
965
+ {
966
+ name: 'subagent-retry-compatibility',
967
+ hidden: true,
968
+ factory: createRetryCompatibilityExtension((sessionId) => this.records.get(sessionId)?.stopRequested ?? true)
969
+ },
970
+ ...(config.tools.includes('external_research')
971
+ ? [
972
+ {
973
+ name: 'external-research-subagent',
974
+ hidden: true,
975
+ factory: createExternalResearchExtension()
976
+ }
977
+ ]
978
+ : []),
979
+ ...(config.name === 'research' && config.tools.includes('research_delegate')
980
+ ? [
981
+ {
982
+ name: 'research-coordinator-subagent',
983
+ hidden: true,
984
+ factory: createResearchCoordinatorExtension((tasks, signal, onProgress) => this.delegateResearchWorkers(sessionManager.getSessionId(), tasks, signal, (completed, total) => onProgress({ completed, total })))
985
+ }
986
+ ]
987
+ : [])
988
+ ];
989
+ const services = await createAgentSessionServices({
990
+ cwd: parent.cwd,
991
+ agentDir,
992
+ settingsManager,
993
+ resourceLoaderOptions: {
994
+ noExtensions: false,
995
+ noPromptTemplates: true,
996
+ noThemes: true,
997
+ extensionFactories,
998
+ extensionsOverride: excludeSubagentPackageExtensions
999
+ }
1000
+ });
1001
+ for (const diagnostic of services.diagnostics) {
1002
+ console.warn(`[pi-desk-subagent] Child 资源诊断:${diagnostic.message}`);
1003
+ }
1004
+ services.resourceLoader = capabilityRuntime.resources(services.resourceLoader, () => [
1005
+ config.name === 'research' &&
1006
+ config.tools.includes('research_delegate') &&
1007
+ isCapabilityAllowed(this.capabilityRules.tools, 'research_delegate')
1008
+ ? RESEARCH_COORDINATOR_BOUNDARY
1009
+ : CHILD_BOUNDARY,
1010
+ `# 当前子代理角色\n\n${config.systemPrompt}`
1011
+ ]);
1012
+ const model = this.resolveModel(config, parent, services.modelRuntime);
1013
+ const { session } = await createAgentSessionFromServices({
1014
+ services,
1015
+ model,
1016
+ thinkingLevel: config.thinking ?? parent.thinkingLevel ?? 'off',
1017
+ tools: childAllowedTools(config.tools),
1018
+ sessionManager
1019
+ });
1020
+ capabilityRuntime.attach(session);
1021
+ this.childCapabilities.set(session, capabilityRuntime);
1022
+ try {
1023
+ await session.bindExtensions({
1024
+ mode: 'rpc',
1025
+ onError: (error) => {
1026
+ console.warn(`[pi-desk-subagent] Child Extension 错误:${error.error}`);
1027
+ }
1028
+ });
1029
+ capabilityRuntime.refresh();
1030
+ const availableTools = new Set(session.getAllTools().map((tool) => tool.name));
1031
+ // MCP 在官方启动/执行边界等待连接,bind 后尚未登记不代表角色配置无效。
1032
+ const missingTools = config.tools.filter((tool) => !isMcpToolName(tool) && !availableTools.has(tool));
1033
+ if (missingTools.length > 0) {
1034
+ throw new Error(`Agent "${config.name}" 配置了不可用工具:${missingTools.join(', ')}`);
1035
+ }
1036
+ return session;
1037
+ }
1038
+ catch (error) {
1039
+ await closeChildSession(session);
1040
+ throw error;
1041
+ }
1042
+ }
1043
+ resolveModel(config, parent, modelRuntime) {
1044
+ if (config.model) {
1045
+ const separator = config.model.indexOf('/');
1046
+ if (separator <= 0 || separator === config.model.length - 1) {
1047
+ throw new Error(`Agent "${config.name}" model 必须使用 provider/model 格式`);
1048
+ }
1049
+ const model = modelRuntime.getModel(config.model.slice(0, separator), config.model.slice(separator + 1));
1050
+ if (!model)
1051
+ throw new Error(`找不到 Agent 模型:${config.model}`);
1052
+ return model;
1053
+ }
1054
+ if (!parent.model)
1055
+ throw new Error(`Agent "${config.name}" 没有可用模型`);
1056
+ const model = modelRuntime.getModel(parent.model.provider, parent.model.id);
1057
+ if (!model)
1058
+ throw new Error(`找不到 Agent 模型:${parent.model.provider}/${parent.model.id}`);
1059
+ return model;
1060
+ }
1061
+ launchRecord(record, prompt) {
1062
+ const session = record.session;
1063
+ if (!session)
1064
+ throw new Error(`Agent ${record.taskId} 缺少运行 Session`);
1065
+ record.taskHandle = this.plugin.startTask({
1066
+ taskId: record.taskId,
1067
+ taskKind: '子代理',
1068
+ taskType: record.agentType,
1069
+ title: record.title,
1070
+ info: currentTaskInfo(session),
1071
+ activity: '执行中',
1072
+ startedAt: record.startedAt,
1073
+ detailSource: { kind: 'pi-conversation', session },
1074
+ interrupt: async () => this.requestStop(record)
1075
+ });
1076
+ record.taskInfoUnsubscribe?.();
1077
+ record.taskInfoUnsubscribe = session.agent?.subscribe?.((event) => {
1078
+ if (event.type !== 'message_end')
1079
+ return;
1080
+ if (this.records.get(record.taskId) !== record ||
1081
+ record.status !== 'running' ||
1082
+ record.session !== session ||
1083
+ !record.taskHandle)
1084
+ return;
1085
+ record.taskHandle.update({ info: currentTaskInfo(session) });
1086
+ });
1087
+ const promptPromise = session.prompt(prompt, {
1088
+ preflightResult: (success) => {
1089
+ if (!success || record.status !== 'running' || record.session !== session)
1090
+ return;
1091
+ record.steerReady = true;
1092
+ void this.flushPendingSteers(record, session);
1093
+ }
1094
+ });
1095
+ const run = this.finishRun(record, session, promptPromise)
1096
+ .catch((error) => {
1097
+ console.warn(`[pi-desk-subagent] 子会话 ${session.sessionId} 收尾失败`, error);
1098
+ })
1099
+ .finally(() => {
1100
+ this.activeRuns.delete(run);
1101
+ });
1102
+ this.activeRuns.add(run);
1103
+ }
1104
+ async flushPendingSteers(record, session) {
1105
+ while (record.status === 'running' &&
1106
+ record.session === session &&
1107
+ record.pendingSteers.length > 0) {
1108
+ const prompt = record.pendingSteers.shift();
1109
+ if (!prompt)
1110
+ continue;
1111
+ try {
1112
+ await session.steer(prompt);
1113
+ }
1114
+ catch (error) {
1115
+ console.warn(`[pi-desk-subagent] Agent ${record.taskId} 补充条件投递失败`, error);
1116
+ }
1117
+ }
1118
+ }
1119
+ async finishRun(record, session, promptPromise) {
1120
+ let status = 'failed';
1121
+ let result = null;
1122
+ let errorMessage = null;
1123
+ try {
1124
+ await promptPromise;
1125
+ const assistant = finalAssistant(session);
1126
+ result = assistant?.text.trim() || null;
1127
+ if (record.stopRequested || assistant?.stopReason === 'aborted') {
1128
+ status = 'interrupted';
1129
+ }
1130
+ else if (assistant?.stopReason === 'stop' && result) {
1131
+ status = 'completed';
1132
+ }
1133
+ else {
1134
+ status = 'failed';
1135
+ errorMessage =
1136
+ assistant?.errorMessage ??
1137
+ (assistant
1138
+ ? `子代理结束原因:${assistant.stopReason}`
1139
+ : '子代理结束但没有最终 Assistant 消息');
1140
+ }
1141
+ }
1142
+ catch (error) {
1143
+ if (record.stopRequested) {
1144
+ status = 'interrupted';
1145
+ }
1146
+ else {
1147
+ status = 'failed';
1148
+ errorMessage = error instanceof Error ? error.message : String(error);
1149
+ }
1150
+ }
1151
+ finally {
1152
+ record.steerReady = false;
1153
+ await closeChildSession(session);
1154
+ if (record.status === 'running' && record.session === session) {
1155
+ this.finishRecord(record, record.stopRequested ? 'interrupted' : status, result, errorMessage, record.notifyParent);
1156
+ }
1157
+ else if (record.session === session) {
1158
+ record.taskInfoUnsubscribe?.();
1159
+ record.taskInfoUnsubscribe = null;
1160
+ record.session = null;
1161
+ }
1162
+ }
1163
+ }
1164
+ finishRecord(record, status, result, errorMessage, notifyParent, endedAt = Date.now()) {
1165
+ if (record.status !== 'running')
1166
+ return;
1167
+ record.status = status;
1168
+ record.endedAt = endedAt;
1169
+ record.pendingSteers = [];
1170
+ record.steerReady = false;
1171
+ const handle = record.taskHandle;
1172
+ const session = record.session;
1173
+ const current = this.records.get(record.taskId) === record;
1174
+ if (current && handle && session)
1175
+ handle.update({ info: currentTaskInfo(session) });
1176
+ record.taskHandle = null;
1177
+ record.taskInfoUnsubscribe?.();
1178
+ record.taskInfoUnsubscribe = null;
1179
+ if (current && handle) {
1180
+ if (status === 'completed')
1181
+ handle.complete('执行完成');
1182
+ else if (status === 'failed')
1183
+ handle.fail(errorMessage ?? '执行失败');
1184
+ else if (status === 'stopped')
1185
+ handle.stopped('已停止');
1186
+ else
1187
+ handle.interrupted('执行中断');
1188
+ }
1189
+ const resolvedResult = result ?? (session ? lastAssistantText(session) : null);
1190
+ record.session = null;
1191
+ if (current && (record.persistLifecycle || this.stopping) && this.sessionActive) {
1192
+ try {
1193
+ this.pi.appendEntry(SUBAGENT_SHUTDOWN_STATE_CUSTOM_TYPE, this.makeTerminalDetails(record, status, endedAt));
1194
+ }
1195
+ catch (error) {
1196
+ console.warn(`[pi-desk-subagent] Agent ${record.taskId} 终态写入失败`, error);
1197
+ }
1198
+ }
1199
+ const resolveRun = record.resolveRun;
1200
+ record.resolveRun = null;
1201
+ record.ownerTaskId = null;
1202
+ if (resolveRun) {
1203
+ resolveRun({
1204
+ taskId: record.taskId,
1205
+ title: record.title,
1206
+ status,
1207
+ result: resolvedResult,
1208
+ error: errorMessage,
1209
+ sessionFile: record.sessionFile
1210
+ });
1211
+ }
1212
+ if (current && notifyParent && this.sessionActive && !this.stopping) {
1213
+ this.notifyCompletion(record, resolvedResult, errorMessage);
1214
+ }
1215
+ }
1216
+ notifyCompletion(record, result, errorMessage) {
1217
+ const details = this.makeTerminalDetails(record, record.status);
1218
+ const outcome = record.status === 'completed'
1219
+ ? '已完成'
1220
+ : record.status === 'interrupted'
1221
+ ? '已中断'
1222
+ : record.status === 'stopped'
1223
+ ? '已停止'
1224
+ : '执行失败';
1225
+ const body = result ?? errorMessage ?? '(没有结果)';
1226
+ try {
1227
+ this.pi.sendMessage({
1228
+ customType: SUBAGENT_COMPLETION_CUSTOM_TYPE,
1229
+ content: `子代理${outcome}。\n\nAgent ID: ${record.taskId}\n类型: ${record.agentType}\n任务: ${record.title}\n\n结果:\n${body}`,
1230
+ display: true,
1231
+ details
1232
+ }, { deliverAs: 'steer', triggerTurn: true });
1233
+ }
1234
+ catch (error) {
1235
+ console.warn(`[pi-desk-subagent] Agent ${record.taskId} 完成通知失败`, error);
1236
+ }
1237
+ }
1238
+ makeLaunchDetails(record) {
1239
+ return {
1240
+ version: SUBAGENT_RECORD_VERSION,
1241
+ kind: 'launch',
1242
+ taskId: record.taskId,
1243
+ agentType: record.agentType,
1244
+ title: record.title,
1245
+ sessionFile: record.sessionFile,
1246
+ startedAt: record.startedAt
1247
+ };
1248
+ }
1249
+ makeTerminalDetails(record, status, endedAt = record.endedAt ?? Date.now()) {
1250
+ return {
1251
+ version: SUBAGENT_RECORD_VERSION,
1252
+ kind: 'terminal',
1253
+ taskId: record.taskId,
1254
+ status,
1255
+ endedAt,
1256
+ sessionFile: record.sessionFile
1257
+ };
1258
+ }
1259
+ }
1260
+ export function registerSubagentExtension(pi) {
1261
+ let runtimePromise;
1262
+ const runtime = () => {
1263
+ runtimePromise ??= ModelRuntime.create().then((modelRuntime) => new SubagentRuntime(pi, modelRuntime));
1264
+ return runtimePromise;
1265
+ };
1266
+ pi.on('session_start', async (_event, ctx) => (await runtime()).startSession(ctx));
1267
+ pi.on('session_before_tree', async (event, ctx) => {
1268
+ if (event.signal.aborted)
1269
+ return { cancel: true };
1270
+ if (!runtimePromise)
1271
+ return;
1272
+ try {
1273
+ return await (await runtimePromise).beforeTree(event.signal);
1274
+ }
1275
+ catch (error) {
1276
+ console.error('[pi-desk-subagent] 树导航前停止子代理失败', error);
1277
+ ctx.ui.notify(`无法切换分支:${error instanceof Error ? error.message : String(error)}`, 'error');
1278
+ return { cancel: true };
1279
+ }
1280
+ });
1281
+ pi.on('session_tree', async (_event, ctx) => (await runtime()).startSession(ctx));
1282
+ pi.on('before_agent_start', async () => {
1283
+ if (runtimePromise)
1284
+ (await runtimePromise).refreshConfigs();
1285
+ });
1286
+ pi.on('session_shutdown', async () => {
1287
+ if (runtimePromise)
1288
+ await (await runtimePromise).shutdown();
1289
+ });
1290
+ }