@zhushanwen/pi-subagent-workflow 8.3.0 → 8.5.0

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 (103) hide show
  1. package/package.json +18 -4
  2. package/relay/relay.mjs +390 -0
  3. package/skills/subagent-ext-config/SKILL.md +80 -0
  4. package/src/execution/__tests__/agent-registry.test.ts +110 -0
  5. package/src/execution/__tests__/chat-engine-routing.test.ts +597 -0
  6. package/src/execution/__tests__/execution-record.test.ts +127 -1
  7. package/src/execution/__tests__/pi-invocation.test.ts +62 -1
  8. package/src/execution/__tests__/relay-agent.test.ts +448 -0
  9. package/src/execution/__tests__/relay-env.test.ts +42 -0
  10. package/src/execution/__tests__/startup-config-declaration.test.ts +35 -0
  11. package/src/execution/__tests__/stream-sink-retirement.test.ts +261 -0
  12. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +310 -0
  13. package/src/execution/__tests__/subprocess-agent-runner.test.ts +53 -5
  14. package/src/execution/agent-registry.ts +10 -0
  15. package/src/execution/config.ts +25 -2
  16. package/src/execution/engine/__tests__/common/data-dir.test.ts +53 -0
  17. package/src/execution/engine/__tests__/common/errors.test.ts +132 -0
  18. package/src/execution/engine/__tests__/common/event-journal.test.ts +177 -0
  19. package/src/execution/engine/__tests__/common/kill-chain.test.ts +192 -0
  20. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +81 -0
  21. package/src/execution/engine/__tests__/common/persona-router.test.ts +123 -0
  22. package/src/execution/engine/__tests__/common/pool-manager.test.ts +154 -0
  23. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +128 -0
  24. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +28 -0
  25. package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +141 -0
  26. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +109 -0
  27. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +101 -0
  28. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +77 -0
  29. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +104 -0
  30. package/src/execution/engine/__tests__/conformance/contract.relay.test.ts +342 -0
  31. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +201 -0
  32. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +76 -0
  33. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +79 -0
  34. package/src/execution/engine/__tests__/engine-discovery.test.ts +87 -0
  35. package/src/execution/engine/__tests__/engines-declaration.test.ts +36 -0
  36. package/src/execution/engine/__tests__/model-prompt.test.ts +85 -0
  37. package/src/execution/engine/__tests__/paths.test.ts +39 -0
  38. package/src/execution/engine/__tests__/registry.test.ts +120 -0
  39. package/src/execution/engine/__tests__/routing.test.ts +231 -0
  40. package/src/execution/engine/common/data-dir.ts +62 -0
  41. package/src/execution/engine/common/errors.ts +183 -0
  42. package/src/execution/engine/common/event-journal.ts +254 -0
  43. package/src/execution/engine/common/journal-replay.ts +62 -0
  44. package/src/execution/engine/common/kill-chain.ts +221 -0
  45. package/src/execution/engine/common/nesting-guard.ts +50 -0
  46. package/src/execution/engine/common/persona-router.ts +108 -0
  47. package/src/execution/engine/common/pool-manager.ts +226 -0
  48. package/src/execution/engine/common/schema-emulation.ts +189 -0
  49. package/src/execution/engine/common/session-view-projection.ts +51 -0
  50. package/src/execution/engine/engine-discovery.ts +65 -0
  51. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +469 -0
  52. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +155 -0
  53. package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +164 -0
  54. package/src/execution/engine/engines/pi/pi-engine.ts +415 -0
  55. package/src/execution/engine/engines/pi/reader.ts +48 -0
  56. package/src/execution/engine/engines/pi/registration.ts +35 -0
  57. package/src/execution/engine/engines/pi/task-spec-mapper.ts +100 -0
  58. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +39 -0
  59. package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +150 -0
  60. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +246 -0
  61. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +228 -0
  62. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +210 -0
  63. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +64 -0
  64. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +127 -0
  65. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +567 -0
  66. package/src/execution/engine/engines/zcode/constants.ts +43 -0
  67. package/src/execution/engine/engines/zcode/golden-sample.ts +39 -0
  68. package/src/execution/engine/engines/zcode/launcher.ts +161 -0
  69. package/src/execution/engine/engines/zcode/parser.ts +436 -0
  70. package/src/execution/engine/engines/zcode/preparer.ts +363 -0
  71. package/src/execution/engine/engines/zcode/reader.ts +381 -0
  72. package/src/execution/engine/engines/zcode/registration.ts +37 -0
  73. package/src/execution/engine/engines/zcode/zcode-engine.ts +648 -0
  74. package/src/execution/engine/host-task-spec.ts +47 -0
  75. package/src/execution/engine/model-prompt.ts +59 -0
  76. package/src/execution/engine/paths.ts +42 -0
  77. package/src/execution/engine/port.ts +153 -0
  78. package/src/execution/engine/registry.ts +123 -0
  79. package/src/execution/engine/routing.ts +218 -0
  80. package/src/execution/engine/types.ts +304 -0
  81. package/src/execution/execute-options-mapper.ts +5 -1
  82. package/src/execution/execution-record.ts +6 -0
  83. package/src/execution/model-resolver.ts +6 -0
  84. package/src/execution/pi-invocation.ts +32 -2
  85. package/src/execution/record-entry.ts +14 -0
  86. package/src/execution/record-store.ts +34 -0
  87. package/src/execution/relay-env.ts +37 -0
  88. package/src/execution/session-runner.ts +24 -0
  89. package/src/execution/stream-sink.ts +26 -0
  90. package/src/execution/subagent-service.ts +249 -11
  91. package/src/execution/subprocess-agent-runner.ts +196 -14
  92. package/src/execution/types.ts +56 -0
  93. package/src/index.ts +46 -1
  94. package/src/interface/command-actions.ts +72 -8
  95. package/src/interface/subagent-actions.ts +8 -2
  96. package/src/interface/subagent-tool.ts +6 -0
  97. package/src/interface/subagents.ts +198 -30
  98. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +5 -2
  99. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +3 -3
  100. package/src/orchestration/models/types.ts +7 -0
  101. package/src/orchestration/worker-script-builder.ts +5 -2
  102. package/src/shared/meta-parser.ts +5 -1
  103. package/src/shared/resource-meta.ts +5 -0
@@ -29,6 +29,15 @@ import {
29
29
  tryTransition,
30
30
  } from "./execution-record.ts";
31
31
  import { doFinalizeRecord, doFinalizeRoundToIdle } from "./finalize-record.ts";
32
+ import { getEngineDataDir } from "./engine/common/data-dir.ts";
33
+ import { EngineError } from "./engine/common/errors.ts";
34
+ import { JournalWriter } from "./engine/common/event-journal.ts";
35
+ import { resolveJournalPath } from "./engine/paths.ts";
36
+ import { executeOptionsToEngineTaskSpec } from "./engine/host-task-spec.ts";
37
+ import type { EnginePort, RunContext } from "./engine/port.ts";
38
+ import { DEFAULT_ENGINE_ID, getEngine } from "./engine/registry.ts";
39
+ import { type EngineRouteResult, resolveEngineRouting, routeEngine } from "./engine/routing.ts";
40
+ import type { AgentOutcome } from "./engine/types.ts";
32
41
  import { ManifestStore } from "./manifest-store.ts";
33
42
  import type { ModelConfigService } from "./model-config-service.ts";
34
43
  import type { AgentConfig, ModelInfo, ResolvedModel } from "./model-resolver.ts";
@@ -38,7 +47,7 @@ import { getSubagentRecordsDir, getSubagentSessionDir } from "./path-encoding.ts
38
47
  import type { StatusFilter } from "./record-store.ts";
39
48
  import { RecordStore } from "./record-store.ts";
40
49
  import { MAX_FORK_DEPTH } from "./session-context-resolver.ts";
41
- import { getChildByRecord, killAllSpawnedChildren, runSpawn, spawnedChildren, type SessionRunnerContext, type SpawnResumeOpts } from "./session-runner.ts";
50
+ import { getChildByRecord, killAllSpawnedChildren, registerSpawnedChildForRecord, runSpawn, spawnedChildren, type SessionRunnerContext, type SpawnResumeOpts } from "./session-runner.ts";
42
51
  import { isIdle, isResumable, hasLiveProcessHandle } from "./lifecycle-predicates.ts";
43
52
  import { startIdleGc } from "./idle-gc.ts";
44
53
  import {
@@ -48,8 +57,8 @@ import {
48
57
  resetAllEpipeFailures,
49
58
  sendPromptCommand,
50
59
  } from "./stdin-writer.ts";
51
- import type { StreamSink } from "./stream-sink.ts";
52
- import { SubagentStream } from "./stream-sink.ts";
60
+ import type { StreamSink, SubagentStream } from "./stream-sink.ts";
61
+ import { createBackgroundStream } from "./stream-sink.ts";
53
62
  import { writeCancelledTombstone } from "./tombstone-store.ts";
54
63
  import type { WorktreeHandle } from "./types.ts";
55
64
  import type {
@@ -684,8 +693,47 @@ export class SubagentService {
684
693
  // ── 1. IDENTITY 解析(确认 → agentConfig → resolveModel)──
685
694
  const identity = await this.resolveIdentity(opts);
686
695
 
696
+ // ── 1.5 引擎路由(D4 chat 入口分叉;U2 升级为 routeEngine 编排)──
697
+ // 三层解析(调用参数 > agent frontmatter > config.json defaultEngine)仍是同步纯
698
+ // 函数;解析为非 pi 时升级走 routeEngine(probe 编排 + fallback 三守卫)。时机
699
+ // 选择:路由(含 probe)在 record 创建前完成——兜底时 record 直接按 pi 语义创建 +
700
+ // engineFallback 留痕(D5 字节级守护只约束「无 fallback 的纯缺省路径」,兜底路径
701
+ // 的 entry 允许含 engine/engineFallback 字段);守卫命中/strict 时 routeEngine 在此
702
+ // throw,不产生孤儿 record。
703
+ const routingInput = {
704
+ callEngine: opts.engine,
705
+ agentEngine: identity.agentConfig?.engine,
706
+ globalDefaultEngine: this.modelService.getGlobalConfig().defaultEngine,
707
+ };
708
+ const routing = resolveEngineRouting(routingInput);
709
+ let route: EngineRouteResult | undefined;
710
+ if (routing.engineId !== DEFAULT_ENGINE_ID) {
711
+ route = await routeEngine({
712
+ routing: routingInput,
713
+ // 守卫 c 判据只看调用方显式指定的 model(resolved model 含 ctxModel 兼底,
714
+ // 恒非空会把一切兜底误判为 model 绑定命中)
715
+ taskModel: opts.model,
716
+ strict: this.modelService.getGlobalConfig().engineRouting?.strict === true,
717
+ probe: (engineId) => getEngine(engineId).probe(),
718
+ });
719
+ if (route.engineId !== DEFAULT_ENGINE_ID) {
720
+ return this.executeViaEngine(opts, identity, route);
721
+ }
722
+ // 兜底成功(典型:默认路由 + probe 失败 + 无守卫命中)→ 落回下方 pi 主路径,
723
+ // record 创建时按 pi 语义 + engine/engineFallback 留痕(engine = 实际执行引擎)
724
+ }
725
+ // D5 字节级守护:无 fallback 的 pi 路由剥掉 opts.engine——createRecordForMode
726
+ // 不盖章(pi record entry 序列化产物不得新增 engine 键,undefined 经 JSON 省略)。
727
+ // 兜底路径显式盖 engine='pi' + engineFallback(见上方时机注释)。
728
+ const piOpts =
729
+ route?.engineFallback !== undefined
730
+ ? { ...opts, engine: DEFAULT_ENGINE_ID, engineFallback: route.engineFallback }
731
+ : opts.engine === undefined
732
+ ? opts
733
+ : { ...opts, engine: undefined };
734
+
687
735
  // ── 2. RECORD 创建 + 注册 ──
688
- const record = this.createRecordForMode(identity, opts, mode);
736
+ const record = this.createRecordForMode(identity, piOpts, mode);
689
737
  emitPendingRegister(this.pi, record.id, record.agent);
690
738
 
691
739
  // ── 2.5 worktree 创建(仅 worktree===true 或已传入 handle 时)──
@@ -724,7 +772,7 @@ export class SubagentService {
724
772
  // ── 4-7. background 包 detached 立即返回 id ──
725
773
  // background detached 运行对 tool 层不可见,完成由 notify 驱动新 turn。
726
774
  const bgDetails = project(record);
727
- this.kickOffBackground(record, { ...opts, worktree: worktreeHandle }, ctx, identity, signal, priority);
775
+ this.kickOffBackground(record, { ...piOpts, worktree: worktreeHandle }, ctx, identity, signal, priority);
728
776
  return { mode: "background", subagentId: record.id, sessionFile: record.sessionFile, details: bgDetails };
729
777
  }
730
778
 
@@ -1389,6 +1437,10 @@ export class SubagentService {
1389
1437
  depth,
1390
1438
  chatMode: opts.conversation === true,
1391
1439
  idleTimeoutMs: opts.idleTimeoutMs,
1440
+ // P4 引擎留痕(D9①):opts.engine/engineFallback 由引擎适配层写入(PiEngine.run
1441
+ // 从 RunContext 回填;缺省 = pi 投影,存量调用方零感知)
1442
+ engine: opts.engine,
1443
+ engineFallback: opts.engineFallback,
1392
1444
  controller,
1393
1445
  });
1394
1446
 
@@ -1403,6 +1455,194 @@ export class SubagentService {
1403
1455
  return { mode: "background", subagentId: record.id, sessionFile: record.sessionFile, details };
1404
1456
  }
1405
1457
 
1458
+ // ── 引擎分支(D4/D10:非 pi 引擎的 chat 域执行骨架,U0)──────────
1459
+
1460
+ /**
1461
+ * 路由到非 pi 引擎的执行入口:routeEngine(注册表校验 + probe/守卫)已由 execute
1462
+ * 完成——这里只剩 unsupported 预检 → record 创建+盖章 → detached 引擎 run。
1463
+ * 全部同步拒绝发生在 record 创建前(不产生孤儿 record)。
1464
+ */
1465
+ private executeViaEngine(
1466
+ opts: ExecuteOptions,
1467
+ identity: ResolvedIdentity,
1468
+ route: EngineRouteResult,
1469
+ ): ExecutionHandle {
1470
+ const engine = route.engine;
1471
+ this.assertEngineParamSupport(engine, opts);
1472
+ // record 盖章路由结果(D5 仅 pi 缺省不盖章;非 pi 显式留痕,createRecordForMode
1473
+ // 经 opts.engine/engineFallback 读入 record identity——engine 为实际执行引擎,
1474
+ // fallback 路径 from=请求引擎留痕,probe 通过的常态路径恒缺省)
1475
+ const record = this.createRecordForMode(
1476
+ identity,
1477
+ {
1478
+ ...opts,
1479
+ engine: route.engineId,
1480
+ ...(route.engineFallback !== undefined ? { engineFallback: route.engineFallback } : {}),
1481
+ },
1482
+ "background",
1483
+ );
1484
+ emitPendingRegister(this.pi, record.id, record.agent);
1485
+ this.kickOffEngineRun(record, opts, engine);
1486
+ return { mode: "background", subagentId: record.id, sessionFile: record.sessionFile, details: project(record) };
1487
+ }
1488
+
1489
+ /**
1490
+ * 非 pi 引擎的 unsupported 参数预检(D11 处置「调用前拒绝」的判据 = capabilities)。
1491
+ * conversation / fork / worktree 三参数对首期接入的引擎(zcode)均不可用:
1492
+ * conversation 依赖同进程 idle 复用、fork 依赖父 pi session 上下文继承、worktree 依赖
1493
+ * 文件隔离(capabilities.sandbox='none')。同步 throw,文案含 capabilities 依据与恢复指引。
1494
+ */
1495
+ private assertEngineParamSupport(engine: EnginePort, opts: ExecuteOptions): void {
1496
+ const caps = engine.capabilities();
1497
+ if (opts.conversation === true && caps.conversation === "unsupported") {
1498
+ throw new EngineError(
1499
+ "engine_capability_unsupported",
1500
+ `engine '${engine.id}' 不支持 conversation(capabilities.conversation = 'unsupported',` +
1501
+ `spawn 单轮模式无同进程 idle 复用,message/close 交互控制面不可用)`,
1502
+ `改用 engine: pi(支持 conversation 续聊),或不传该参数(一次性任务默认形态)`,
1503
+ );
1504
+ }
1505
+ if (opts.fork === true) {
1506
+ throw new EngineError(
1507
+ "engine_capability_unsupported",
1508
+ `engine '${engine.id}' 不支持 fork(fork 依赖父 pi session 上下文继承,` +
1509
+ `capabilities.steer = '${caps.steer}'——非 pi 引擎无父 session 分叉通道)`,
1510
+ `把所需父上下文写进 task 正文后不传 fork,或改用 engine: pi`,
1511
+ );
1512
+ }
1513
+ if ((opts.worktree === true || typeof opts.worktree === "object") && caps.sandbox === "none") {
1514
+ throw new EngineError(
1515
+ "engine_capability_unsupported",
1516
+ `engine '${engine.id}' 不支持 worktree 隔离(capabilities.sandbox = 'none',` +
1517
+ `引擎未接文件系统隔离层)`,
1518
+ `改用 engine: pi(worktree 隔离可用),或不传该参数(在 parent cwd 执行)`,
1519
+ );
1520
+ }
1521
+ }
1522
+
1523
+ /**
1524
+ * 非 pi 引擎的 detached 执行编排(与 kickOffBackground 同构的 background 语义):
1525
+ * pool 并发槽(maxConcurrent 对非 pi 引擎同样生效)→ journal 接线(D6 第②级:
1526
+ * taskId=record.id,初始池 key 占位 'shared',onPoolResolved retarget 到引擎实际
1527
+ * 池 key——路径与 paths.ts 同源推导)→ engine.run(signal 接 record controller,
1528
+ * kill-chain 两级生效)→ engineHandle 回填(终态迁移落 entry 前)→ 终态迁移 →
1529
+ * bg notify(chat 域宿主职责,与 pi 完成通知同语义)。
1530
+ */
1531
+ private kickOffEngineRun(record: ExecutionRecord, opts: ExecuteOptions, engine: EnginePort): void {
1532
+ const signal = record.controller?.signal;
1533
+ void (async () => {
1534
+ try {
1535
+ await this.pool.acquire(PRIORITY_BACKGROUND, this.effectiveMaxConcurrentFor(record), signal);
1536
+ } catch {
1537
+ // S1: 排队中被 abort(signal.aborted)走 cancelled,与已运行被 abort 一致(runAndFinalize 同款)
1538
+ if (signal?.aborted) {
1539
+ await this.finalizeAborted(record);
1540
+ } else {
1541
+ await this.finalizeFailed(record, new Error("aborted"));
1542
+ }
1543
+ return;
1544
+ }
1545
+ // [review MF1] acquire 成功后必须 finally release:不 release 则
1546
+ // DefaultConcurrencyPool._active 永不递减——每次引擎任务泄漏一个并发槽,累计
1547
+ // maxConcurrent 次后全部 background subagent(pi 与引擎共用同一池)在 acquire 队列永久挂起
1548
+ try {
1549
+ await this.runEngineTask(record, opts, engine, signal);
1550
+ // cancel 抢先(closedReason='cancelled')时 cancelBackground 自己 notify,跳过
1551
+ if (record.closedReason !== "cancelled") {
1552
+ this.notifyComplete(record);
1553
+ }
1554
+ } finally {
1555
+ this.pool.release();
1556
+ }
1557
+ })();
1558
+ }
1559
+
1560
+ /**
1561
+ * kickOffEngineRun 的 acquire 后主体:journal 接线(D6 第②级:taskId=record.id,
1562
+ * 初始池 key 占位 'shared',onPoolResolved retarget 到引擎实际池 key)→ engine.run
1563
+ * (signal 接 record controller,kill-chain 两级生效)→ engineHandle 回填(终态迁移
1564
+ * 落 entry 前)→ 终态迁移。bg notify 归编排侧(与 kickOffBackground 收尾通知归编排对称)。
1565
+ */
1566
+ private async runEngineTask(
1567
+ record: ExecutionRecord,
1568
+ opts: ExecuteOptions,
1569
+ engine: EnginePort,
1570
+ signal: AbortSignal | undefined,
1571
+ ): Promise<void> {
1572
+ const journal = new JournalWriter({
1573
+ path: resolveJournalPath(getEngineDataDir(), engine.id, "shared", record.id),
1574
+ taskId: record.id,
1575
+ engineId: engine.id,
1576
+ });
1577
+ const retargetJournal = (poolKey: string): void => {
1578
+ journal.retarget(resolveJournalPath(getEngineDataDir(), engine.id, poolKey, record.id));
1579
+ };
1580
+ // 对齐点③:journal 路径权威 = 引擎声明的池 key(writer 初始用占位,retarget 后
1581
+ // 与 handle.poolKey 同源)。模式对齐 SAR 的 journalingOnEvent:先落盘再转发。
1582
+ const runCtx: RunContext = {
1583
+ taskId: record.id,
1584
+ poolKey: "shared",
1585
+ signal,
1586
+ ctxModel: opts.ctxModel,
1587
+ onEvent: (event) => journal.append(event),
1588
+ onPoolResolved: retargetJournal,
1589
+ // D9①:路由层 fallback 留痕投影进 outcome(zcode 无独立 record 通路)
1590
+ ...(record.engineFallback !== undefined ? { engineFallback: record.engineFallback } : {}),
1591
+ // D10 终止链:engine spawn 的子进程注册进 spawnedChildren 记账
1592
+ //(cancelBackground SIGTERM / dispose killAll 收割对非 pi record 生效)
1593
+ onChildSpawned: (child) => registerSpawnedChildForRecord(record.id, child),
1594
+ };
1595
+ try {
1596
+ const { handle, outcome } = await engine.run(executeOptionsToEngineTaskSpec(opts), runCtx);
1597
+ // engineHandle 完整回填(U2:终态迁移落 entry 前)。sessionRef 整体透传——
1598
+ // 失败终态 sessionId 缺失时也回填已有部分(dbPath/poolKey),读侧①级降②级
1599
+ // 的防御形态;journalPath 取 retarget 后的实际落盘路径(writer 是路径权威)。
1600
+ record.engineHandle = {
1601
+ sessionRef: handle.data.sessionRef,
1602
+ poolKey: handle.data.poolKey,
1603
+ journalPath: journal.path,
1604
+ };
1605
+ await journal.close();
1606
+ await this.finalizeEngineOutcome(record, outcome);
1607
+ } catch (err) {
1608
+ // engine.run prepare 期 reject(进程创建前)→ failed 终态(与 runAndFinalize catch 同语义);
1609
+ // journal 尽力而为收口(②级数据源写失败已由 writer 内部 warn 收敛)
1610
+ await journal.close();
1611
+ await this.finalizeFailed(record, err);
1612
+ }
1613
+ }
1614
+
1615
+ /**
1616
+ * 分层并发配额:depth 越深可用配额越少(下限 1)。fork 深度护栏在池维度的投影,
1617
+ * 公式约定以 concurrency-pool.ts 注释为登记处、此处为唯一代码锚点。
1618
+ */
1619
+ private effectiveMaxConcurrentFor(record: ExecutionRecord): number {
1620
+ return Math.max(1, this.pool.maxConcurrent - record.depth);
1621
+ }
1622
+
1623
+ /**
1624
+ * engine.run resolve 的终态迁移:outcome.error → failed(success=false + error 文案);
1625
+ * 否则 done(result=content)。CAS 抢锁(tryTransition)防与 cancelBackground 双收尾。
1626
+ */
1627
+ private async finalizeEngineOutcome(record: ExecutionRecord, outcome: AgentOutcome): Promise<void> {
1628
+ if (outcome.sessionFile !== undefined) {
1629
+ record.sessionFile = outcome.sessionFile;
1630
+ }
1631
+ const result: AgentResult = {
1632
+ text: outcome.content,
1633
+ turns: outcome.usage?.turns ?? 0,
1634
+ durationMs: outcome.durationMs ?? Date.now() - record.startedAt,
1635
+ success: outcome.error === undefined,
1636
+ ...(outcome.error !== undefined ? { error: outcome.error } : {}),
1637
+ sessionId: outcome.sessionId ?? record.id,
1638
+ toolCalls: [],
1639
+ ...(outcome.parsedOutput !== undefined ? { parsedOutput: outcome.parsedOutput } : {}),
1640
+ };
1641
+ if (tryTransition(record, "closed", "gc")) {
1642
+ await this.finalizeRecord(record, result, "closed", "gc");
1643
+ }
1644
+ }
1645
+
1406
1646
  // ── 执行内部:run + finalize(sync/bg 共用)──────────────
1407
1647
 
1408
1648
  /** 共享的"干活 + 收尾"——sync 直接 await,background 在 detached 里调。 */
@@ -1421,9 +1661,8 @@ export class SubagentService {
1421
1661
  const pooled = record.mode === "background";
1422
1662
  let acquired = false;
1423
1663
  if (pooled) {
1424
- const effectiveMaxConcurrent = Math.max(1, this.pool.maxConcurrent - record.depth);
1425
1664
  try {
1426
- await this.pool.acquire(priority, effectiveMaxConcurrent, signal);
1665
+ await this.pool.acquire(priority, this.effectiveMaxConcurrentFor(record), signal);
1427
1666
  acquired = true;
1428
1667
  } catch {
1429
1668
  // S1: 排队中被 abort(signal.aborted)走 cancelled,与已运行被 abort 一致。
@@ -1575,10 +1814,9 @@ export class SubagentService {
1575
1814
  /** resume 选项(M2-B1):透传 runAndFinalize→runSpawn。undefined = 新 session。 */
1576
1815
  resume?: SpawnResumeOpts,
1577
1816
  ): void {
1578
- // 创建 streaming 生命周期对象——streamSink null(session_start 未注入)时降级为 undefined。
1579
- const stream = this.streamSink
1580
- ? new SubagentStream(record.id, this.streamSink)
1581
- : undefined;
1817
+ // 创建 streaming 生命周期对象。策略(含 widget 退役步骤 2:GUI + relay 激活时停发
1818
+ // 私货、TUI/未激活原样创建、sink 未注入降级 undefined)集中在 createBackgroundStream。
1819
+ const stream = createBackgroundStream(record.id, this.streamSink, ctx.mode, process.env);
1582
1820
 
1583
1821
  void this.runAndFinalize(
1584
1822
  record, opts, ctx, identity, signal, priority,
@@ -8,7 +8,9 @@
8
8
  // 接线层级:
9
9
  // [跨模块 port] implements AgentRunner(orchestration/models/ports.ts)
10
10
  // [模块内直调] mapToExecuteOptions + mergeTimeoutSignal(execute-options-mapper)
11
- // [模块内直调] this.subagentService.executeAndAwait
11
+ // [P4 路由层] engine/routing.ts routeEngine(三层优先级 + probe + fallback 三守卫,D9)
12
+ // [引擎层] EnginePort.run(pi = 本地 DI 绑定实例;非 pi = registry.getEngine 动态获取)
13
+ // [模块内直调] this.subagentService.executeAndAwait(PiEngine 内部委托目标)
12
14
  //
13
15
  // 设计基线:
14
16
  // D-A2(映射归 adapter)/ D-A8(onEvent 桥接)/ D-A9(timeoutMs 合并 signal)/
@@ -17,8 +19,19 @@
17
19
  import type { AgentRunner } from "../orchestration/models/ports.ts";
18
20
  import type { AgentCallOpts, AgentResult } from "../orchestration/models/types.ts";
19
21
  import type { AgentEvent } from "../shared/agent-event.ts";
22
+ import { getEngineDataDir } from "./engine/common/data-dir.ts";
23
+ import { JournalWriter } from "./engine/common/event-journal.ts";
24
+ import { createPiEngine, PI_POOL_KEY } from "./engine/engines/pi/registration.ts";
25
+ import { executeOptionsToTaskSpec } from "./engine/engines/pi/task-spec-mapper.ts";
26
+ import { resolveJournalPath } from "./engine/paths.ts";
27
+ import type { EnginePort, RunContext } from "./engine/port.ts";
28
+ import { routeEngine, resolveEngineRouting, type EngineRouteResult, type EngineRoutingInput } from "./engine/routing.ts";
29
+ import { getEngine, hasEngine, listEngines } from "./engine/registry.ts";
30
+ import type { AgentOutcome, EngineHandle } from "./engine/types.ts";
20
31
  import { mapToExecuteOptions, mergeTimeoutSignal } from "./execute-options-mapper.ts";
32
+ import { getModelConfigService } from "./model-config-service.ts";
21
33
  import type { ModelInfo } from "./model-resolver.ts";
34
+ import { registerSpawnedChildForRecord } from "./session-runner.ts";
22
35
  import type { SubagentStream } from "./stream-sink.ts";
23
36
  import type { SubagentService } from "./subagent-service.ts";
24
37
  import type { ExecuteOptions } from "./types.ts";
@@ -51,14 +64,27 @@ export interface SubprocessAgentRunnerDeps {
51
64
  * - result 形状不变(workflow AgentResult: content/parsedOutput/usage/error/toolCalls)
52
65
  * - 不 reject——失败信息入 result.error(与 executeAgentCall 契约一致)
53
66
  * - timeoutMs 合并 signal(D-A9);onEvent 桥接 AgentEvent→workflow liveRecord(D-A8)
67
+ *
68
+ * [P1 引擎接线] 执行经 EnginePort。pi 引擎绑定本 SAR 的服务引用(per-session DI——
69
+ * 单测注入 mock 时全局单例不可见;生产环境两者是同一进程单例对象),行为零变化;
70
+ * 非 pi 引擎(P4 路由可达:frontmatter/调用参数/全局默认指定)经 registry.getEngine
71
+ * 动态获取——「引擎身份」的归属边界在注册表,SAR 不感知具体引擎实现。
72
+ *
73
+ * [P4 配置路由] 每次运行经 engine/routing.ts 解析三层优先级(调用参数 opts.engine >
74
+ * agent frontmatter engine > 全局默认 'pi')+ probe + fallback 三守卫(D9)。路由失败
75
+ * (engine_not_found / engine_probe_failed / model_not_available)不 reject,入
76
+ * result.error——与 executeAgentCall 契约一致。
54
77
  */
55
78
  export class SubprocessAgentRunner implements AgentRunner {
56
79
  private readonly subagentService: SubagentService;
57
80
  private ctxModel: ModelInfo | undefined;
81
+ /** pi 引擎(per-session DI 绑定,缺省执行路径——见类注释)。 */
82
+ private readonly piEngine: EnginePort;
58
83
 
59
84
  constructor(deps: SubprocessAgentRunnerDeps) {
60
85
  this.subagentService = deps.subagentService;
61
86
  this.ctxModel = deps.ctxModel;
87
+ this.piEngine = createPiEngine(() => this.subagentService);
62
88
  }
63
89
 
64
90
  /**
@@ -72,13 +98,16 @@ export class SubprocessAgentRunner implements AgentRunner {
72
98
  }
73
99
 
74
100
  /**
75
- * 执行单次 agent 调用:委托 SubagentService.executeAndAwait。
101
+ * 执行单次 agent 调用:路由(P4)→ EnginePort.run → PiEngine 委托
102
+ * SubagentService.executeAndAwait。
76
103
  *
77
104
  * 接线链路:
78
- * mergeTimeoutSignal → mapToExecuteOptions →
79
- * this.subagentService.executeAndAwait返回 AgentResult
105
+ * routeEngine(三层 + probe + 守卫)→ mergeTimeoutSignal → mapToExecuteOptions →
106
+ * AgentTaskSpec → engine.run(PiEngine:specExecuteOptions 还原 + engine 留痕)→
107
+ * executeAndAwait → AgentOutcome → AgentResult
80
108
  *
81
109
  * 错误处理:不 reject。
110
+ * - 路由失败(未注册 id / probe 失败 + 守卫或 strict)→ AgentResult.error(错误码前缀)
82
111
  * - executeAndAwait 内部失败 → 返回 AgentResult(success:false) → 已映射 error 字段
83
112
  * - executeAndAwait throw(嵌套超限 BC-12)→ catch → AgentResult.error
84
113
  * - spawn 级失败已由 runSpawn 内部收口为 failed AgentResult(不逃逸)
@@ -91,22 +120,105 @@ export class SubprocessAgentRunner implements AgentRunner {
91
120
  ): Promise<AgentResult> {
92
121
  const startedAt = Date.now();
93
122
 
123
+ // ── P4 路由:三层优先级 + probe + fallback 三守卫(D9①/D7)──
124
+ // 路由在最前——引擎身份决定 journal 路径与后续一切执行面;失败(未注册 id /
125
+ // probe 失败 + strict/守卫)按「不 reject」契约转 result.error。
126
+ // pi 快路径同步短路(不经 routeEngine 的 await):pi 恒免探、无 fallback 可言、
127
+ // engineFor('pi') 本地 DI 绑定恒可用——不引入微任务边界,缺省路径时序与 P1 接线
128
+ // 前完全一致(下游依赖「run 内首个 await 前已触达 executeAndAwait」的时序契约)。
129
+ let routingInput: EngineRoutingInput;
130
+ let route: EngineRouteResult;
131
+ try {
132
+ routingInput = this.buildRoutingInput(opts);
133
+ const routing = resolveEngineRouting(routingInput);
134
+ if (routing.engineId === "pi") {
135
+ route = {
136
+ engine: this.piEngine,
137
+ engineId: "pi",
138
+ requestedEngineId: "pi",
139
+ source: routing.source,
140
+ };
141
+ } else {
142
+ route = await routeEngine({
143
+ routing: routingInput,
144
+ taskModel: opts.model,
145
+ strict: getModelConfigService()?.getGlobalConfig().engineRouting?.strict === true,
146
+ probe: (engineId) => this.engineFor(engineId).probe(),
147
+ getEngineFn: (engineId) => this.engineFor(engineId),
148
+ // pi 经本地 DI 绑定恒可用(不依赖 registry 全局注册态——SAR 持有服务引用),
149
+ // has/list 注入同一口径,engine_not_found 文案不会把本地 pi 漏报成未注册
150
+ hasEngineFn: (engineId) => engineId === "pi" || hasEngine(engineId),
151
+ listEnginesFn: () => (hasEngine("pi") ? listEngines() : ["pi", ...listEngines()]),
152
+ });
153
+ }
154
+ } catch (err) {
155
+ // buildRoutingInput 的 agent 解析期校验(未注册 frontmatter engine)与路由失败
156
+ // (probe + strict/守卫)一并在此收口——「不 reject」契约
157
+ return errorResult(err, startedAt);
158
+ }
159
+
160
+ // ── P2 event journal 接线(设计 D6 第②级;对齐点③:路径权威 = 引擎池 key)──
161
+ // host 在 onEvent 回调内统一落盘(全引擎免费获得②级数据源)。初始 poolKey 用 pi
162
+ // 缺省占位(pi 恒 'shared');非池化稳定引擎(zcode)在 prepare 期经
163
+ // RunContext.onPoolResolved 声明实际池 key → writer.retarget——保证 journal 落盘
164
+ // 路径与 handle.poolKey 同源(handle.journalPath 由本方法 run 返回后回填)。
165
+ //
166
+ // taskId 为宿主侧任务标识(journal 文件名与池引用计数 key)——executeAndAwait
167
+ // 不外露内部 record id(取真实 id 需 hook record store 且改 createRecordForMode
168
+ // 签名,影响面大;P4 决策:保留占位,`sa-` 前缀与 record id 同构。影响面:record
169
+ // GC 时无法按 taskId 联动删 journal(journal 依赖 30 天 TTL 自然回收),read ②级
170
+ // 经 handle.journalPath 自描述定位不受影响)。
171
+ const taskId = `sa-${crypto.randomUUID()}`;
172
+ const journal = new JournalWriter({
173
+ path: resolveJournalPath(getEngineDataDir(), route.engineId, PI_POOL_KEY, taskId),
174
+ taskId,
175
+ engineId: route.engineId,
176
+ });
177
+ const retargetJournal = (poolKey: string): void => {
178
+ journal.retarget(resolveJournalPath(getEngineDataDir(), route.engineId, poolKey, taskId));
179
+ };
180
+ // 包装:先写 journal 再转发原 onEvent(原 onEvent 未传时也恒传包装版——
181
+ // 下游 onEvent 通道是事件生成后的纯转发,无行为分支,仅多一次入队)
182
+ const journalingOnEvent = (event: AgentEvent): void => {
183
+ journal.append(event);
184
+ onEvent?.(event);
185
+ };
186
+
94
187
  try {
95
- // ── D-A9: timeoutMs 合并 signal ──
188
+ // ── D-A9: timeoutMs 合并 signal(超时 abort 带 HOST_TIMEOUT_ABORT_REASON 标记)──
96
189
  const mergedSignal = mergeTimeoutSignal(signal, opts.timeoutMs);
97
190
 
98
191
  // ── D-A2 + D-008: AgentCallOpts → ExecuteOptions 映射 ──
99
192
  const mappedOpts: ExecuteOptions = mapToExecuteOptions(opts, this.ctxModel);
100
193
 
101
- // ── D-A8: onEvent 桥接 ──
102
- // executeAndAwait 发强类型 AgentEvent(session-runner handleSdkEvent 出口)。
103
- // workflow 的 onEvent 闭包(error-recovery.ts dispatchAgentCall)类型已升级为
104
- // (event: AgentEvent) => updateFromEvent(liveRecord, event)(D-005)。
105
- // SAR 直接透传 onEvent——类型对齐后零桥接开销。
106
- const bridgedOnEvent = onEvent;
107
-
108
- // ── 核心委托 ──
109
- return await this.subagentService.executeAndAwait(mappedOpts, mergedSignal, bridgedOnEvent, stream);
194
+ // ── P1/P4 引擎接线:EnginePort.run ──
195
+ const runCtx: RunContext = {
196
+ taskId,
197
+ poolKey: PI_POOL_KEY,
198
+ signal: mergedSignal,
199
+ onEvent: journalingOnEvent,
200
+ ctxModel: this.ctxModel,
201
+ ...(route.engineFallback !== undefined ? { engineFallback: route.engineFallback } : {}),
202
+ onPoolResolved: retargetJournal,
203
+ // [U0 D10] 终止链路径①:引擎 spawn 的子进程注册进 session-runner 的
204
+ // spawnedChildren 记账(dispose killAll 收割兜底对 workflow 域引擎任务生效);
205
+ // taskId('sa-' 前缀)即记账 key,与 chat 域 kickOffEngineRun 的 record.id 同构
206
+ onChildSpawned: (child) => registerSpawnedChildForRecord(taskId, child),
207
+ ...(stream !== undefined ? { stream } : {}),
208
+ // 解耦形态(有 schemaEnv 无 schema)的兜底通道——耦合形态下引擎从 task.schema
209
+ // 派生等值,此值被忽略(见 RunContext.schemaEnv 注释)
210
+ ...(mappedOpts.schemaEnv !== undefined ? { schemaEnv: mappedOpts.schemaEnv } : {}),
211
+ };
212
+ const { handle, outcome } = await route.engine.run(
213
+ // 泛化为中立声明(PiEngine 内部再还原回 ExecuteOptions——往返保真,
214
+ // 由 engines/pi/__tests__/task-spec-mapper.test.ts 逐字段锁定)
215
+ executeOptionsToTaskSpec(mappedOpts),
216
+ runCtx,
217
+ );
218
+ // handle.journalPath 回填(§3.3.6:read ②级的自描述定位符——运行期落盘路径
219
+ // 权威在 writer,handle 记录最终路径供跨重启 read 消费)
220
+ backfillJournalPath(handle, journal.path);
221
+ return outcomeToRunnerResult(outcome);
110
222
  } catch (err) {
111
223
  // executeAndAwait throw(嵌套超限 ForkDepthExceededError,BC-12)或未预期异常 → 不 reject,入 error。
112
224
  const message = err instanceof Error ? err.message : String(err);
@@ -116,6 +228,76 @@ export class SubprocessAgentRunner implements AgentRunner {
116
228
  error: message,
117
229
  toolCalls: [],
118
230
  };
231
+ } finally {
232
+ // run 终态后 flush + fsync 一次(§3.3.6 写入纪律);写失败已由 writer 内部
233
+ // warn + failed 收口,close 不抛(journal 是②级尽力而为数据源)
234
+ await journal.close();
119
235
  }
120
236
  }
237
+
238
+ // ── 内部 ──
239
+
240
+ /**
241
+ * 引擎获取:pi 走 per-session DI 绑定(mock 语义 + 生产同单例,P1 行为零变化);
242
+ * 非 pi 经 registry.getEngine 动态获取(P4:引擎身份归属注册表,未注册 id 抛
243
+ * EngineNotFoundError——路由期已前置校验,这里是防御性兜底)。
244
+ */
245
+ private engineFor(engineId: string): EnginePort {
246
+ if (engineId === "pi") return this.piEngine;
247
+ return getEngine(engineId);
248
+ }
249
+
250
+ /**
251
+ * 三层路由输入装配(D9):调用参数(opts.engine,workflow step 级透传)> agent
252
+ * frontmatter(ModelConfigService.getAgentConfig——loadByPath mtime 缓存,幂等)>
253
+ * 全局默认(config.json defaultEngine)。单例未就绪(session_start 前/测试 mock)
254
+ * 时各层缺省——落内置 'pi'。
255
+ */
256
+ private buildRoutingInput(opts: AgentCallOpts): EngineRoutingInput {
257
+ const service = getModelConfigService();
258
+ const agentEngine =
259
+ opts.agent !== undefined && opts.agent !== "" ? service?.getAgentConfig(opts.agent)?.engine : undefined;
260
+ const globalDefault = service?.getGlobalConfig().defaultEngine;
261
+ return {
262
+ ...(opts.engine !== undefined && opts.engine !== "" ? { callEngine: opts.engine } : {}),
263
+ ...(agentEngine !== undefined && agentEngine !== "" ? { agentEngine } : {}),
264
+ ...(globalDefault !== undefined && globalDefault !== "" ? { globalDefaultEngine: globalDefault } : {}),
265
+ };
266
+ }
267
+ }
268
+
269
+ /** 路由期错误 → AgentResult.error(错误码前缀格式保留——engine_not_found 等)。 */
270
+ function errorResult(err: unknown, startedAt: number): AgentResult {
271
+ return {
272
+ content: "",
273
+ durationMs: Date.now() - startedAt,
274
+ error: err instanceof Error ? err.message : String(err),
275
+ toolCalls: [],
276
+ };
277
+ }
278
+
279
+ /** handle.journalPath 回填(一次写者:SAR 是 handle 的首个消费者)。 */
280
+ function backfillJournalPath(handle: EngineHandle, journalPath: string): void {
281
+ handle.data.journalPath = journalPath;
282
+ }
283
+
284
+ /**
285
+ * AgentOutcome → workflow AgentResult:剥离引擎层新增字段(engineId/engineFallback/
286
+ * exitCode——engineFallback 经 record(pi 路径)/outcome(zcode 路径)留痕,GUI 投影
287
+ * 通道在后续 wave 接线,workflow 引擎不消费)。其余字段由 PiEngine 从 executeAndAwait
288
+ * 的返回值逐字段映射而来,字段全集完整性由 pi-engine 单测锁定(缺字段会在该处转红,
289
+ * 不会静默丢失)。
290
+ */
291
+ function outcomeToRunnerResult(outcome: AgentOutcome): AgentResult {
292
+ return {
293
+ content: outcome.content,
294
+ parsedOutput: outcome.parsedOutput,
295
+ usage: outcome.usage,
296
+ durationMs: outcome.durationMs,
297
+ error: outcome.error,
298
+ sessionId: outcome.sessionId,
299
+ sessionFile: outcome.sessionFile,
300
+ worktreePath: outcome.worktreePath,
301
+ toolCalls: outcome.toolCalls,
302
+ };
121
303
  }