@zhushanwen/pi-subagents 0.0.1

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.
Files changed (37) hide show
  1. package/agents/context-builder.md +19 -0
  2. package/agents/oracle.md +19 -0
  3. package/agents/planner.md +19 -0
  4. package/agents/researcher.md +19 -0
  5. package/agents/reviewer.md +19 -0
  6. package/agents/scout.md +19 -0
  7. package/agents/worker.md +18 -0
  8. package/index.ts +1 -0
  9. package/package.json +59 -0
  10. package/src/commands/subagents.ts +78 -0
  11. package/src/core/agent-registry.ts +222 -0
  12. package/src/core/concurrency-pool.ts +78 -0
  13. package/src/core/event-bridge.ts +199 -0
  14. package/src/core/execution-record.ts +500 -0
  15. package/src/core/model-resolver.ts +206 -0
  16. package/src/core/output-collector.ts +118 -0
  17. package/src/core/path-encoding.ts +16 -0
  18. package/src/core/session-factory.ts +365 -0
  19. package/src/core/session-runner.ts +303 -0
  20. package/src/core/turn-limiter.ts +71 -0
  21. package/src/index.ts +104 -0
  22. package/src/runtime/config/config.ts +170 -0
  23. package/src/runtime/discovery-config.ts +135 -0
  24. package/src/runtime/execution/history-store.ts +196 -0
  25. package/src/runtime/execution/notifier.ts +209 -0
  26. package/src/runtime/execution/record-store.ts +280 -0
  27. package/src/runtime/model-config-service.ts +265 -0
  28. package/src/runtime/session-file-gc.ts +70 -0
  29. package/src/runtime/subagent-service.ts +549 -0
  30. package/src/tools/subagent-tool.ts +286 -0
  31. package/src/tui/bg-notify-render.ts +139 -0
  32. package/src/tui/config-wizard.ts +253 -0
  33. package/src/tui/format-helpers.ts +37 -0
  34. package/src/tui/format.ts +332 -0
  35. package/src/tui/list-view.ts +883 -0
  36. package/src/tui/tool-render.ts +467 -0
  37. package/src/types.ts +334 -0
@@ -0,0 +1,549 @@
1
+ // src/runtime/subagent-service.ts
2
+ //
3
+ // 执行编排 + 记录 + 通知领域 Service。"跑一次子代理 + 管理执行状态"。
4
+ //
5
+ // 与 ModelConfigService(配置/模型解析域)正交——本 Service 持有其引用但不暴露给外部。
6
+ // executor 逻辑已合并进本文件——它是 SubagentService.execute 的编排逻辑,
7
+ // 没有独立状态/生命周期,不需要独立文件。合并后行为方法自然 private。
8
+ //
9
+ // 上游:subagent-tool(execute/query/cancel)、TUI(onChange/listRunning/collectRecords)。
10
+ // session_start 时经 initSession 注入 pi;modelRegistry/entries 归 ModelConfigService.initModel。
11
+
12
+ import { type ConcurrencyPool,DefaultConcurrencyPool } from "../core/concurrency-pool.ts";
13
+ import {
14
+ completeRecord,
15
+ createRecord,
16
+ project,
17
+ snapshot,
18
+ toPersisted,
19
+ tryTransition,
20
+ } from "../core/execution-record.ts";
21
+ import type { AgentConfig } from "../core/model-resolver.ts";
22
+ import type { SdkLike } from "../core/session-factory.ts";
23
+ import { run, type SessionRunnerContext } from "../core/session-runner.ts";
24
+ import type {
25
+ AgentEvent,
26
+ AgentResult,
27
+ ExecuteOptions,
28
+ ExecutionHandle,
29
+ ExecutionMode,
30
+ ExecutionRecord,
31
+ QueryResult,
32
+ RecordSnapshot,
33
+ ResolvedModel,
34
+ SubagentRecord,
35
+ SubagentToolDetails,
36
+ } from "../types.ts";
37
+ import { HistoryStore } from "./execution/history-store.ts";
38
+ import type { BgNotifyRecord, NotifierHost } from "./execution/notifier.ts";
39
+ import { BgNotifier } from "./execution/notifier.ts";
40
+ import { RecordStore } from "./execution/record-store.ts";
41
+ import type { ModelConfigService } from "./model-config-service.ts";
42
+
43
+ /** Pi ExtensionAPI 的最小接口(duck-typed)。 */
44
+ interface PiLike {
45
+ appendEntry(customType: string, data?: unknown): void;
46
+ events: { emit(channel: string, data: unknown): void };
47
+ sendMessage(
48
+ message: { customType: string; content: string; display: boolean; details?: unknown },
49
+ options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
50
+ ): void;
51
+ }
52
+
53
+ /** Service 构造参数(进程级)。 */
54
+ export interface SubagentServiceInit {
55
+ cwd: string;
56
+ /** 配置/模型域 Service(execute 内部调其 resolveModel)。 */
57
+ modelService: ModelConfigService;
58
+ }
59
+
60
+ /** session_start 注入参数(session 级)。 */
61
+ export interface SubagentServiceSessionInit {
62
+ pi: PiLike;
63
+ sessionId: string;
64
+ }
65
+
66
+ /** background 优先级(低,让步);sync 优先级(高,抢占)。 */
67
+ const PRIORITY_BACKGROUND = 1000;
68
+ const PRIORITY_SYNC = 0;
69
+
70
+ /** 触发 onUpdate 的事件类型(streaming delta 不触发,避免每 token 刷新)。 */
71
+ const TRIGGERING_EVENT_TYPES = new Set<AgentEvent["type"]>([
72
+ "tool_start",
73
+ "tool_end",
74
+ "turn_end",
75
+ "message_end",
76
+ "error",
77
+ "compaction",
78
+ ]);
79
+
80
+ /** resolveIdentity 的产物——一次确定、写入 record 后不再变。 */
81
+ interface ResolvedIdentity {
82
+ agent: string;
83
+ agentConfig: AgentConfig | undefined;
84
+ resolved: ResolvedModel;
85
+ }
86
+
87
+ /**
88
+ * 执行编排 Service。进程级单例。
89
+ *
90
+ * session_start:
91
+ * 1. modelService = getModelConfigService() ?? new ModelConfigService({homeDir, agentDir})
92
+ * 2. service = getSubagentService() ?? new SubagentService({cwd, modelService})
93
+ * 3. modelService.initModel({modelRegistry, sessionId, entries})
94
+ * 4. service.initSession({pi, sessionId})
95
+ *
96
+ * session_shutdown:
97
+ * service.dispose()
98
+ */
99
+ export class SubagentService {
100
+ private readonly pool: ConcurrencyPool;
101
+ private readonly store: RecordStore;
102
+ private readonly history: HistoryStore;
103
+ private readonly notifier: BgNotifier;
104
+ private readonly modelService: ModelConfigService;
105
+ private readonly cwd: string;
106
+
107
+ private pi: PiLike | null = null;
108
+ private sdk: SdkLike | null = null;
109
+ private _disposed = false;
110
+ private _seq = 0;
111
+
112
+ constructor(init: SubagentServiceInit) {
113
+ this.cwd = init.cwd;
114
+ this.modelService = init.modelService;
115
+ this.pool = new DefaultConcurrencyPool(this.modelService.getGlobalConfig().maxConcurrent);
116
+ this.history = new HistoryStore(this.modelService.getAgentDir(), init.cwd);
117
+ this.store = new RecordStore(this.history);
118
+ this.notifier = new BgNotifier(this.piAdapter());
119
+ }
120
+
121
+ // ── 生命周期(index.ts 调)──────────────────────────────
122
+
123
+ /** session_start 注入 pi + revive(modelRegistry/entries 归 ModelConfigService.initModel)。 */
124
+ initSession(init: SubagentServiceSessionInit): void {
125
+ this.pi = init.pi;
126
+ // revive(dispose 的逆操作:/resume /fork /new 后复活)
127
+ this._disposed = false;
128
+ this.store.revive();
129
+ this.notifier.revive();
130
+ }
131
+
132
+ /** session 结束清理(清定时器,丢弃 pending 通知)。幂等。 */
133
+ dispose(): void {
134
+ if (this._disposed) return;
135
+ this._disposed = true;
136
+ this.notifier.flushPendingNotifications();
137
+ this.store.dispose();
138
+ this.notifier.dispose();
139
+ }
140
+
141
+ // ── 执行(subagent-tool 调)────────────────────────────
142
+
143
+ /**
144
+ * 预解析 model(renderCall 标题行用,同步)。
145
+ * 代理 modelService.resolveModel——renderCall 在 execute 前调用,但 model 解析是同步的,
146
+ * 让标题行能提前显示 model/thinking,不必等 execute。
147
+ * hub 未就绪时抛(调用方 catch 降级)。
148
+ */
149
+ resolveModel(
150
+ agent: string,
151
+ override?: { model?: string; thinkingLevel?: string },
152
+ ): ResolvedModel {
153
+ return this.modelService.resolveModel(agent, override);
154
+ }
155
+
156
+ /**
157
+ * 统一执行入口。sync/background 共用,mode 由 opts.wait + agentConfig.defaultBackground 判定。
158
+ * 内部完成:mode 判定 → 确认(经回调)→ 模型解析 → 执行 → 收尾。
159
+ *
160
+ * mode 判定规则(内化在 Service,不暴露给 tool 层):
161
+ * wait === false → background(用户显式要求异步)
162
+ * wait === true → sync(用户显式要求同步)
163
+ * wait === undefined + agentConfig.defaultBackground === true → background
164
+ * 否则 → sync
165
+ */
166
+ async execute(opts: ExecuteOptions): Promise<ExecutionHandle> {
167
+ this.assertReady();
168
+
169
+ // mode 判定(业务规则归 Service,tool 层只传 wait 意图)
170
+ const mode = this.resolveMode(opts);
171
+ const ctx = await this.buildSessionRunnerContext();
172
+
173
+ // ── 1. IDENTITY 解析(确认 → agentConfig → resolveModel)──
174
+ const identity = await this.resolveIdentity(opts);
175
+
176
+ // ── 2. RECORD 创建 + 注册 ──
177
+ const record = this.createRecordForMode(identity, opts, mode);
178
+
179
+ // ── 3. MODE 分叉:signal/priority(仅此 2 处即时差异)──
180
+ const signal = mode === "background"
181
+ ? record.controller!.signal
182
+ : opts.signal;
183
+ const priority = mode === "background" ? PRIORITY_BACKGROUND : PRIORITY_SYNC;
184
+
185
+ // ── 4-7. sync 直接 await;background 包 detached 立即返回 id ──
186
+ if (mode === "sync") {
187
+ await this.runAndFinalize(record, opts, ctx, identity, signal, priority);
188
+ return { mode: "sync", record: snapshot(record), details: project(record) };
189
+ }
190
+
191
+ // background:立即返回 backgroundId + 启动时的 details(status=running),
192
+ // 步骤 4-6 在 detached promise 里跑。
193
+ const bgDetails = project(record);
194
+ bgDetails.backgroundId = record.id;
195
+ this.kickOffBackground(record, opts, ctx, identity, signal, priority);
196
+ return { mode: "background", backgroundId: record.id, details: bgDetails };
197
+ }
198
+
199
+ /** poll(backgroundId):查 record 并投影为 QueryResult。不存在 throw。 */
200
+ query(id: string): QueryResult {
201
+ this.assertReady();
202
+ const record = this.store.getMutable(id);
203
+ if (!record) throw new Error(`No subagent record with id "${id}"`);
204
+ return this.recordToQueryResult(record);
205
+ }
206
+
207
+ /** 取消 background record(tryTransition CAS 抢锁防重复副作用)。 */
208
+ cancel(id: string): boolean {
209
+ this.assertReady();
210
+ const record = this.store.getMutable(id);
211
+ if (!record) return false;
212
+ return this.cancelBackground(record);
213
+ }
214
+
215
+ // ── 状态查询(TUI 调)──────────────────────────────────
216
+
217
+ /** 订阅 store 变更(widget/list requestRender)。返回取消订阅。 */
218
+ onChange(listener: () => void): () => void {
219
+ return this.store.onChange(listener);
220
+ }
221
+
222
+ /** 列出 running record 快照(widget 计数用)。 */
223
+ listRunning(): RecordSnapshot[] {
224
+ return this.store.listRunning();
225
+ }
226
+
227
+ /** 合并四源 record(/subagents list 消费)。 */
228
+ collectRecords(limit: number): SubagentRecord[] {
229
+ return this.store.collectRecords(limit, this.modelService.sessionId);
230
+ }
231
+
232
+ // ── 执行内部:mode 判定 + 身份解析 + record 创建 ──────────
233
+
234
+ /** mode 业务规则:wait 显式 > agentConfig.defaultBackground > sync 兜底。 */
235
+ private resolveMode(opts: ExecuteOptions): ExecutionMode {
236
+ if (opts.wait === false) return "background";
237
+ if (opts.wait === true) return "sync";
238
+ // wait === undefined:看 agent 的 defaultBackground
239
+ const agentConfig = this.modelService.getAgentConfig(opts.agent);
240
+ if (agentConfig?.defaultBackground === true) return "background";
241
+ return "sync";
242
+ }
243
+
244
+ /** 步骤 1:身份解析。agentConfig → resolveModel。 */
245
+ private async resolveIdentity(opts: ExecuteOptions): Promise<ResolvedIdentity> {
246
+ // D-1:取消首次确认拦截——categoryConfirmed 默认 true,直接解析。
247
+ // agent 名 + 配置
248
+ const agent = opts.agent ?? "default";
249
+ const agentConfig = this.modelService.getAgentConfig(agent);
250
+
251
+ // 模型解析(5 级 fallback)
252
+ const resolved = this.modelService.resolveModel(agent, {
253
+ model: opts.model,
254
+ thinkingLevel: opts.thinkingLevel,
255
+ });
256
+
257
+ return { agent, agentConfig, resolved };
258
+ }
259
+
260
+ /** 步骤 2:按 mode 生成 id + controller,创建 record 并注册。 */
261
+ private createRecordForMode(
262
+ identity: ResolvedIdentity,
263
+ opts: ExecuteOptions,
264
+ mode: ExecutionMode,
265
+ ): ExecutionRecord {
266
+ const seq = ++this._seq;
267
+ const id = mode === "background"
268
+ ? `bg-${seq}-${Date.now()}`
269
+ : `run-${seq}`;
270
+ const controller = mode === "background" ? new AbortController() : undefined;
271
+
272
+ const record = createRecord(id, {
273
+ agent: identity.agent,
274
+ model: `${identity.resolved.model.provider}/${identity.resolved.model.id}`,
275
+ thinkingLevel: identity.resolved.thinkingLevel,
276
+ mode,
277
+ task: opts.task,
278
+ startedAt: Date.now(),
279
+ controller,
280
+ });
281
+
282
+ this.store.register(record);
283
+ return record;
284
+ }
285
+
286
+ // ── 执行内部:run + finalize(sync/bg 共用)──────────────
287
+
288
+ /** 共享的"干活 + 收尾"——sync 直接 await,background 在 detached 里调。 */
289
+ private async runAndFinalize(
290
+ record: ExecutionRecord,
291
+ opts: ExecuteOptions,
292
+ ctx: SessionRunnerContext,
293
+ identity: ResolvedIdentity,
294
+ signal: AbortSignal | undefined,
295
+ priority: number,
296
+ ): Promise<AgentResult> {
297
+ await this.pool.acquire(priority);
298
+ // onEvent 包装:AgentEvent → onUpdate(project(record)) 回流调用方
299
+ const onEvent = opts.onUpdate
300
+ ? (event: AgentEvent): void => this.onEventThrottled(record, event, opts.onUpdate!)
301
+ : undefined;
302
+
303
+ let result: AgentResult;
304
+ try {
305
+ result = await run(record, opts.task, {
306
+ resolved: identity.resolved,
307
+ agentConfig: identity.agentConfig,
308
+ appendSystemPrompt: opts.appendSystemPrompt,
309
+ skillPath: opts.skillPath,
310
+ schema: opts.schema,
311
+ maxTurns: opts.maxTurns,
312
+ graceTurns: opts.graceTurns,
313
+ signal,
314
+ onEvent,
315
+ }, ctx);
316
+ } catch (err) {
317
+ // run() 正常路径不抛错,但创建期异常(createAndConfigureSession /
318
+ // attachRunHooks 失败)会逃逸出 run() —— 合成 failed result + 收尾。
319
+ // swallow(不 re-throw):sync 调用方拿到合成 failed result,background 的
320
+ // .then 正常跑 notify。避免异常逃逸到 tool 层 + record 卡 running。
321
+ result = await this.finalizeFailed(record, err);
322
+ return result;
323
+ } finally {
324
+ this.pool.release();
325
+ }
326
+
327
+ // status 唯一判定点:success ? done : (aborted ? cancelled : failed)
328
+ const status: "done" | "failed" | "cancelled" = result.success
329
+ ? "done"
330
+ : signal?.aborted ? "cancelled" : "failed";
331
+
332
+ // CAS 抢锁:抢到则完整收尾;没抢到(cancel 已先设 cancelled)则跳过
333
+ if (tryTransition(record, status)) {
334
+ await this.finalizeRecord(record, result, status);
335
+ }
336
+ return result;
337
+ }
338
+
339
+ /** background 的步骤 4-6:包进 detached promise(不 await),execute 立即返回。 */
340
+ private kickOffBackground(
341
+ record: ExecutionRecord,
342
+ opts: ExecuteOptions,
343
+ ctx: SessionRunnerContext,
344
+ identity: ResolvedIdentity,
345
+ signal: AbortSignal | undefined,
346
+ priority: number,
347
+ ): void {
348
+ void this.runAndFinalize(record, opts, ctx, identity, signal, priority)
349
+ .then(() => {
350
+ // background 回注:仅当本路径抢到 CAS(status 已转 done/failed)才 notify。
351
+ // cancel 抢先时 status=cancelled,cancelBackground 自己 notify,此处跳过。
352
+ if (record.status !== "cancelled") {
353
+ this.notifyComplete(record);
354
+ }
355
+ })
356
+ .catch(() => {
357
+ // detached 吞错:runAndFinalize 内部已 finalize record,不外抛
358
+ });
359
+ }
360
+
361
+ /** 取消 background record。CAS 抢锁——抢到则 notify,不写 history。 */
362
+ private cancelBackground(record: ExecutionRecord): boolean {
363
+ record.controller?.abort();
364
+ if (!tryTransition(record, "cancelled")) {
365
+ return false; // detached 已 finalize,cancel 来晚了
366
+ }
367
+ // 抢到锁:completeRecord(用空 result 填 cancelled)+ archive(移出 live map,否则
368
+ // hasRunningBackground 永真)+ notify。不走 finalizeRecord(cancel 不写 history)。
369
+ // durationMs 用真实耗时(startedAt → now),避免耗时统计恒为 0 失真。
370
+ const cancelledResult: AgentResult = {
371
+ text: "",
372
+ turns: record.turns,
373
+ durationMs: Date.now() - record.startedAt,
374
+ success: false,
375
+ error: "cancelled by user",
376
+ sessionId: record.id,
377
+ toolCalls: [],
378
+ };
379
+ completeRecord(record, cancelledResult, "cancelled");
380
+ this.store.archive(record);
381
+ this.notifyComplete(record);
382
+ return true;
383
+ }
384
+
385
+ /** 收尾三件套:completeRecord + store.archive + history.append。 */
386
+ private async finalizeRecord(
387
+ record: ExecutionRecord,
388
+ result: AgentResult,
389
+ status: "done" | "failed" | "cancelled",
390
+ ): Promise<void> {
391
+ completeRecord(record, result, status);
392
+ this.store.archive(record);
393
+ await this.history.append(toPersisted(record, this.cwd));
394
+ }
395
+
396
+ /**
397
+ * run() 创建期异常的收尾(H1 修复)。
398
+ * run() 正常路径不抛错,但 createAndConfigureSession / attachRunHooks 失败
399
+ * 会抛——本方法合成 failed AgentResult → CAS 抢锁 → finalizeRecord
400
+ * (与正常路径同形,写 history + archive)。
401
+ * 返回合成 result 供 runAndFinalize 继续返回(不 re-throw,swallow 策略)。
402
+ */
403
+ private async finalizeFailed(record: ExecutionRecord, err: unknown): Promise<AgentResult> {
404
+ const errMsg = err instanceof Error ? err.message : String(err);
405
+ // durationMs 用真实耗时(startedAt → now),避免失败统计恒为 0 失真。
406
+ const failedResult: AgentResult = {
407
+ text: "",
408
+ turns: record.turns,
409
+ durationMs: Date.now() - record.startedAt,
410
+ success: false,
411
+ error: errMsg,
412
+ sessionId: record.id,
413
+ toolCalls: [],
414
+ };
415
+ // CAS 抢锁:抢到(status 仍 running)则完整收尾;没抢到(cancel 已先设 cancelled)跳过。
416
+ if (tryTransition(record, "failed")) {
417
+ await this.finalizeRecord(record, failedResult, "failed");
418
+ }
419
+ return failedResult;
420
+ }
421
+
422
+ /** background 完成回注(record → BgNotifyRecord 映射 + notifier.notify)。 */
423
+ private notifyComplete(record: ExecutionRecord): void {
424
+ this.notifier.notify(this.toNotifyRecord(record));
425
+ }
426
+
427
+ /** AgentEvent 节流回流到 onUpdate(streaming delta 不触发)。 */
428
+ private onEventThrottled(
429
+ record: ExecutionRecord,
430
+ event: AgentEvent,
431
+ onUpdate: (details: SubagentToolDetails) => void,
432
+ ): void {
433
+ if (TRIGGERING_EVENT_TYPES.has(event.type)) {
434
+ onUpdate(project(record));
435
+ }
436
+ }
437
+
438
+ // ── 内部 ────────────────────────────────────────────────
439
+
440
+ /** 校验 Service 就绪(pi 已注入 + 未 dispose)。 */
441
+ private assertReady(): void {
442
+ if (this.pi === null) {
443
+ throw new Error("pi not injected (initSession not called?)");
444
+ }
445
+ if (this._disposed) {
446
+ throw new Error("hub disposed");
447
+ }
448
+ }
449
+
450
+ /** 构造 SessionRunnerContext。sdk lazy 获取 + 缓存。 */
451
+ private async buildSessionRunnerContext(): Promise<SessionRunnerContext> {
452
+ if (this.sdk === null) {
453
+ const { getSdk } = await import("../core/session-factory.ts");
454
+ this.sdk = await getSdk();
455
+ }
456
+ return {
457
+ cwd: this.cwd,
458
+ agentDir: this.modelService.getAgentDir(),
459
+ factoryCtx: {
460
+ modelRegistry: this.modelService.getModelRegistry(),
461
+ resolveAgent: (name: string) => this.modelService.getAgentConfig(name),
462
+ cwd: this.cwd,
463
+ agentDir: this.modelService.getAgentDir(),
464
+ skillDirs: this.modelService.getDiscoverySkillDirs(),
465
+ },
466
+ sdk: this.sdk,
467
+ };
468
+ }
469
+
470
+ /** notifier 的 NotifierHost 适配器(绑定到 pi.sendMessage + store 查询)。 */
471
+ private piAdapter(): NotifierHost {
472
+ return {
473
+ sendMessage: (message, options) => {
474
+ // deliverAs:"followUp" 让完成通知在当前 streaming turn 结束后唤醒父 agent
475
+ // (不打断正在的工具调用);triggerTurn:true 空闲时直接 prompt 新 turn。
476
+ this.pi?.sendMessage(message, options);
477
+ },
478
+ hasRunningBackground: () => {
479
+ // 有 running 的 background record → 滑动窗口继续等;否则立即 flush
480
+ return this.store.listRunning().some((r) => r.mode === "background");
481
+ },
482
+ };
483
+ }
484
+
485
+ /** ExecutionRecord → QueryResult(poll 返回的只读视图)。 */
486
+ private recordToQueryResult(record: ExecutionRecord): QueryResult {
487
+ const snap = snapshot(record);
488
+ const details = project(record);
489
+ return {
490
+ id: snap.id,
491
+ status: snap.status,
492
+ agent: snap.agent,
493
+ model: snap.model,
494
+ thinkingLevel: snap.thinkingLevel,
495
+ turns: snap.turns,
496
+ totalTokens: snap.totalTokens,
497
+ startedAt: snap.startedAt,
498
+ endedAt: snap.endedAt,
499
+ elapsedSeconds: details.elapsedSeconds,
500
+ result: snap.result,
501
+ error: snap.error,
502
+ eventLog: [...snap.eventLog],
503
+ mode: snap.mode,
504
+ };
505
+ }
506
+
507
+ /** record → BgNotifyRecord(notifier.notify 入参映射,内部不外露)。 */
508
+ private toNotifyRecord(record: ExecutionRecord): BgNotifyRecord {
509
+ const snap = snapshot(record);
510
+ return {
511
+ id: snap.id,
512
+ status: snap.status as "done" | "failed" | "cancelled",
513
+ agent: snap.agent,
514
+ result: snap.result,
515
+ error: snap.error,
516
+ startedAt: snap.startedAt,
517
+ endedAt: snap.endedAt,
518
+ };
519
+ }
520
+ }
521
+
522
+ // ============================================================
523
+ // 进程单例访问器(session_start 重建)
524
+ // ============================================================
525
+
526
+ // 用 globalThis[Symbol.for] 持有进程单例,避免 jiti 因路径字符串不同加载多份模块
527
+ // 导致单例分裂。场景:其它扩展 import "@zhushanwen/pi-subagents" 与本扩展被 Pi host
528
+ // 直接加载,若 jiti 缓存 key 用路径字符串(非 realpath),两份 subagent-service.ts 各持
529
+ // 一个 _service,setSubagentService 写 A、getSubagentService 读 B(null)。globalThis 跨所有模块实例共享,彻底消除。
530
+ // 详见 docs/pi-extension-standards.md §7.5。
531
+ const SERVICE_SLOT_KEY = Symbol.for("@zhushanwen/pi-subagents.service");
532
+
533
+ type ServiceSlot = { current: SubagentService | null };
534
+
535
+ function getServiceSlot(): ServiceSlot {
536
+ const record = globalThis as unknown as Record<symbol, unknown>;
537
+ if (!record[SERVICE_SLOT_KEY]) record[SERVICE_SLOT_KEY] = { current: null };
538
+ return record[SERVICE_SLOT_KEY] as ServiceSlot;
539
+ }
540
+
541
+ /** 获取进程单例。session_start 前为 null。 */
542
+ export function getSubagentService(): SubagentService | null {
543
+ return getServiceSlot().current;
544
+ }
545
+
546
+ /** 设置进程单例(session_start 首次创建时)。 */
547
+ export function setSubagentService(service: SubagentService): void {
548
+ getServiceSlot().current = service;
549
+ }