@zhushanwen/pi-subagent-workflow 8.7.0 → 8.8.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.
- package/README.md +5 -11
- package/package.json +8 -11
- package/skills/workflow-script-format/SKILL.md +32 -13
- package/src/host/__tests__/pi-host.test.ts +23 -2
- package/src/host/pi-host.ts +61 -6
- package/src/index.ts +155 -528
- package/src/injectors/__tests__/engine-awareness.test.ts +5 -3
- package/src/injectors/__tests__/engine-section-stability.test.ts +48 -33
- package/src/injectors/__tests__/model-list-injector.test.ts +18 -21
- package/src/injectors/__tests__/subagent-list-injector.test.ts +54 -14
- package/src/injectors/__tests__/workflow-list-injector.test.ts +26 -12
- package/src/injectors/engine-awareness.ts +80 -8
- package/src/injectors/model-list-injector.ts +15 -58
- package/src/injectors/resource-list-injector.ts +209 -0
- package/src/injectors/subagent-list-injector.ts +53 -224
- package/src/injectors/workflow-list-injector.ts +36 -196
- package/src/interface/__tests__/detectors.test.ts +46 -35
- package/src/interface/__tests__/subagent-tool-path-guard.test.ts +42 -26
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +14 -3
- package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +216 -0
- package/src/interface/__tests__/tool-workflow-script-generate.test.ts +17 -6
- package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +38 -2
- package/src/interface/bg-notify-render.ts +7 -17
- package/src/interface/command-actions.ts +5 -15
- package/src/interface/commands.ts +116 -76
- package/src/interface/format.ts +354 -245
- package/src/interface/gui-mappers.ts +18 -22
- package/src/interface/helpers.ts +9 -130
- package/src/interface/list-component.ts +164 -117
- package/src/interface/list-shared.ts +3 -3
- package/src/interface/list-view.ts +130 -73
- package/src/interface/subagent-actions.ts +50 -675
- package/src/interface/subagent-tool-schema.ts +7 -9
- package/src/interface/subagent-tool.ts +5 -4
- package/src/interface/subagents.ts +9 -8
- package/src/interface/tool-render.ts +5 -15
- package/src/interface/tool-workflow-script.ts +34 -75
- package/src/interface/tool-workflow.ts +62 -96
- package/src/interface/tui-kit.ts +305 -0
- package/src/interface/views/WorkflowsView.ts +270 -207
- package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +3 -3
- package/src/interface/views/__tests__/detail-content-session-file.test.ts +7 -4
- package/src/interface/views/detail-content.ts +16 -14
- package/src/interface/views/view-constants.ts +19 -0
- package/src/jsonl-run-store.ts +144 -228
- package/agents/analyst.md +0 -61
- package/agents/coder.md +0 -70
- package/agents/debugger.md +0 -67
- package/agents/doc-reviewer.md +0 -50
- package/agents/explorer.md +0 -64
- package/agents/general-purpose.md +0 -32
- package/agents/orchestrator.md +0 -63
- package/agents/planner.md +0 -54
- package/agents/researcher.md +0 -65
- package/agents/reviewer.md +0 -74
- package/src/interface/views/format.ts +0 -320
package/src/index.ts
CHANGED
|
@@ -13,54 +13,30 @@
|
|
|
13
13
|
* 设计基线:D-004(旧包不动)/ ADR-025(进程内执行)/ D-8(pi.__workflowRun 签名)。
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import
|
|
17
|
-
import * as path from "node:path";
|
|
18
|
-
|
|
19
|
-
import type { BeforeAgentStartEvent, ExtensionAPI, ExtensionContext, SessionCompactEvent, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import type { ExtensionAPI, ExtensionContext, SessionCompactEvent, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent } from "@earendil-works/pi-coding-agent";
|
|
20
17
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
21
18
|
import { getLogger, setPiHandle } from "@zhushanwen/pi-extension-logger";
|
|
22
19
|
|
|
23
20
|
// ═══ core 宿主端口接线(subagent-core 包抽离 u0-wire;实现见 src/host/pi-host.ts) ═══
|
|
24
|
-
import { configureCore } from "@zhushanwen/subagent-core
|
|
25
|
-
import { configureNotifyDomain } from "@zhushanwen/subagent-core
|
|
21
|
+
import { configureCore } from "@zhushanwen/subagent-core";
|
|
22
|
+
import { configureNotifyDomain } from "@zhushanwen/subagent-core";
|
|
26
23
|
import { createPiHostServices, createPiNotifyDomainPorts } from "./host/pi-host.ts";
|
|
27
24
|
|
|
28
|
-
import { bestEffort } from "@zhushanwen/subagent-core
|
|
25
|
+
import { bestEffort } from "@zhushanwen/subagent-core";
|
|
29
26
|
// ═══ execution/ 层(subagents 核心 + 运行时) ═══
|
|
30
|
-
import { getOrCreateChannelRegistry } from "@zhushanwen/subagent-core/execution/channel-registry-access.ts";
|
|
31
|
-
import { DialogGlobalQueue } from "@zhushanwen/subagent-core/execution/dialog-queue.ts";
|
|
32
|
-
// [engine-awareness U3] 全局 config 三态读取(检测 poll 与 session_start 基线共用)
|
|
33
|
-
import { readGlobalConfig } from "@zhushanwen/subagent-core/execution/config.ts";
|
|
34
27
|
// [U7] 引擎列表状态文件(registry → engines.json,GUI 引擎选择器数据源)
|
|
35
|
-
import { syncEnginesFile } from "@zhushanwen/subagent-core
|
|
36
|
-
// [U7] 引擎模型段注入(defaultEngine 非 pi 时 system prompt 补 <available_<engine>_models>)
|
|
37
|
-
// [engine-awareness U3] 补恒在状态段 <current_subagent_engine>(D6,pi 引擎也声明)
|
|
38
|
-
import { buildEngineModelsPromptAppend, buildSubagentEngineSection } from "@zhushanwen/subagent-core/execution/engine/model-prompt.ts";
|
|
28
|
+
import { syncEnginesFile } from "@zhushanwen/subagent-core";
|
|
39
29
|
// [P1 引擎接线] 组合根登记 'pi' 引擎进 registry(引擎获取统一经 getEngine,缺省 id 'pi')
|
|
40
|
-
import { registerPiEngine } from "@zhushanwen/subagent-core
|
|
30
|
+
import { registerPiEngine } from "@zhushanwen/subagent-core";
|
|
41
31
|
// [P3 引擎接线] 组合根登记 'zcode' 引擎(spawn 单轮模式;engineDataDir 默认走
|
|
42
32
|
// common/data-dir SSOT,见 engines/zcode/registration.ts)
|
|
43
|
-
import { registerZcodeEngine } from "@zhushanwen/subagent-core
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
} from "
|
|
50
|
-
import { bindNotifyLedgerHost, getBoundNotifyLedger, type NotifyLedgerHost } from "@zhushanwen/subagent-core/execution/notify-ledger.ts";
|
|
51
|
-
import { IDENTITY_CUSTOM_TYPE, type SubagentIdentityData } from "@zhushanwen/subagent-core/execution/session-reconstructor.ts";
|
|
52
|
-
import type { ExecutionMode, SubagentRecord } from "@zhushanwen/subagent-core/execution/types.ts";
|
|
53
|
-
import { maybeCleanupExpiredSessionFiles } from "@zhushanwen/subagent-core/execution/session-file-gc.ts";
|
|
54
|
-
import {
|
|
55
|
-
getSubagentService,
|
|
56
|
-
setSubagentService,
|
|
57
|
-
SubagentService,
|
|
58
|
-
} from "@zhushanwen/subagent-core/execution/subagent-service.ts";
|
|
59
|
-
import { killAllSpawnedChildren } from "@zhushanwen/subagent-core/execution/session-runner.ts";
|
|
60
|
-
import { SubprocessAgentRunner } from "@zhushanwen/subagent-core/execution/subprocess-agent-runner.ts";
|
|
61
|
-
import { WorktreeManager } from "@zhushanwen/subagent-core/execution/worktree-manager.ts";
|
|
62
|
-
// [engine-awareness U3] per-turn 引擎检测编排(D1/D1b/D2/D3/D5)
|
|
63
|
-
import { normalizeEngineId, runEngineAwarenessTurn } from "./injectors/engine-awareness.ts";
|
|
33
|
+
import { registerZcodeEngine } from "@zhushanwen/subagent-core";
|
|
34
|
+
import { getModelConfigService } from "@zhushanwen/subagent-core";
|
|
35
|
+
import { getBoundNotifyLedger } from "@zhushanwen/subagent-core";
|
|
36
|
+
import { getSubagentService } from "@zhushanwen/subagent-core";
|
|
37
|
+
import { killAllSpawnedChildren } from "@zhushanwen/subagent-core";
|
|
38
|
+
// [engine-awareness U3/D7-④] per-turn 引擎检测编排 + before_agent_start 链尾接线
|
|
39
|
+
import { setupEngineAwarenessInjector } from "./injectors/engine-awareness.ts";
|
|
64
40
|
import { setupModelListInjector } from "./injectors/model-list-injector.ts";
|
|
65
41
|
import { setupSubagentListInjector } from "./injectors/subagent-list-injector.ts";
|
|
66
42
|
import { setupWorkflowListInjector } from "./injectors/workflow-list-injector.ts";
|
|
@@ -73,20 +49,26 @@ import { registerSubagentTool } from "./interface/subagent-tool.ts";
|
|
|
73
49
|
import { registerSubagentsCommand } from "./interface/subagents.ts";
|
|
74
50
|
import { registerWorkflowTool } from "./interface/tool-workflow.ts";
|
|
75
51
|
import { registerWorkflowScriptTool } from "./interface/tool-workflow-script.ts";
|
|
76
|
-
import { JsonlRunStore } from "./jsonl-run-store.ts";
|
|
77
|
-
import { clearSkillPathCache } from "@zhushanwen/subagent-core/orchestration/skill-discovery.ts";
|
|
78
52
|
// ═══ orchestration/ 层(workflow engine + infra) ═══
|
|
79
|
-
import type { LauncherDeps } from "@zhushanwen/subagent-core
|
|
80
|
-
import { executeNestedWorkflow, runAndWait, type WorkflowRunResult } from "@zhushanwen/subagent-core
|
|
53
|
+
import type { LauncherDeps } from "@zhushanwen/subagent-core";
|
|
54
|
+
import { executeNestedWorkflow, runAndWait, type WorkflowRunResult } from "@zhushanwen/subagent-core";
|
|
81
55
|
import {
|
|
82
56
|
evictDoneRunsBeyondCap,
|
|
83
57
|
MAX_RETAINED_DONE_RUNS,
|
|
84
58
|
scheduleTimeBudget,
|
|
85
59
|
terminateRunningRuns,
|
|
86
|
-
} from "@zhushanwen/subagent-core
|
|
87
|
-
import type { WorkflowRun } from "@zhushanwen/subagent-core
|
|
88
|
-
import { WorkerHostImpl } from "@zhushanwen/subagent-core
|
|
89
|
-
import { WorkflowScriptRegistryImpl } from "@zhushanwen/subagent-core
|
|
60
|
+
} from "@zhushanwen/subagent-core";
|
|
61
|
+
import type { WorkflowRun } from "@zhushanwen/subagent-core";
|
|
62
|
+
import { WorkerHostImpl } from "@zhushanwen/subagent-core";
|
|
63
|
+
import { WorkflowScriptRegistryImpl } from "@zhushanwen/subagent-core";
|
|
64
|
+
// ═══ session 生命周期装配 seam(bootstrap seam,设计 §3.1/D1) ═══
|
|
65
|
+
import {
|
|
66
|
+
getOrCreateDialogQueue,
|
|
67
|
+
setupSessionLifecycle,
|
|
68
|
+
type SessionLifecycleDeps,
|
|
69
|
+
type SessionLifecycleResult,
|
|
70
|
+
} from "./session-lifecycle.ts";
|
|
71
|
+
import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
|
|
90
72
|
|
|
91
73
|
// ── pi.__workflowRun 类型扩展(D-8 签名) ─────────────────
|
|
92
74
|
|
|
@@ -106,42 +88,6 @@ declare module "@earendil-works/pi-coding-agent" {
|
|
|
106
88
|
// 模块级 logger(setPiHandle 注入后自动走 appendEntry)
|
|
107
89
|
const logger = getLogger("subagents");
|
|
108
90
|
|
|
109
|
-
// ── subagent 状态快照格式化 ──
|
|
110
|
-
//
|
|
111
|
-
// [v4 A-6] before_agent_start 注入 hook 已删(活跃 subagent 清单改由 agent 按需调
|
|
112
|
-
// action:'list' 拉取,消除每 loop 注入的上下文税与盲点)。本函数保留为纯格式化工具:
|
|
113
|
-
// before-agent-start-injection / parent-child-matrix 测试覆盖其正确性,未来 list
|
|
114
|
-
// 视图或其他注入点可复用。
|
|
115
|
-
|
|
116
|
-
/** 活跃 subagent 数量上限(超过截断显示)。 */
|
|
117
|
-
const MAX_STATUS_INJECTION = 10;
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* 将活跃 subagent record 格式化为一行一条的快照文本。
|
|
121
|
-
*
|
|
122
|
-
* 格式:
|
|
123
|
-
* [subagent-status] N active subagents:
|
|
124
|
-
* - sa-xxx (slug): running, rounds 0
|
|
125
|
-
* - sa-yyy (slug): idle, rounds 3
|
|
126
|
-
* +2 more, use action:'list'
|
|
127
|
-
*
|
|
128
|
-
* @param records 已筛选的活跃 record(running + idle)
|
|
129
|
-
*/
|
|
130
|
-
export function formatSubagentStatusSnapshot(records: SubagentRecord[]): string {
|
|
131
|
-
const lines = [`[subagent-status] ${records.length} active subagent${records.length === 1 ? "" : "s"}:`];
|
|
132
|
-
const shown = records.slice(0, MAX_STATUS_INJECTION);
|
|
133
|
-
for (const r of shown) {
|
|
134
|
-
const slug = r.slug || r.agent;
|
|
135
|
-
const roundPart = r.round !== undefined && r.round > 0 ? `, rounds ${r.round}` : "";
|
|
136
|
-
lines.push(`- ${r.id} (${slug}): ${r.status}${roundPart}`);
|
|
137
|
-
}
|
|
138
|
-
const remaining = records.length - MAX_STATUS_INJECTION;
|
|
139
|
-
if (remaining > 0) {
|
|
140
|
-
lines.push(`+${remaining} more, use action:'list'`);
|
|
141
|
-
}
|
|
142
|
-
return lines.join("\n");
|
|
143
|
-
}
|
|
144
|
-
|
|
145
91
|
// ═══ [V2 决策 7 防线 i] process 级 shutdown hook ═══
|
|
146
92
|
//
|
|
147
93
|
// session_shutdown 是 pi 的 async hook,进程被 SIGTERM/SIGINT 强杀或崩溃时来不及
|
|
@@ -160,7 +106,7 @@ function reapSpawnedChildrenOnShutdown(): void {
|
|
|
160
106
|
// 不静默吞错,对齐「错误必须可操作」。
|
|
161
107
|
logger.debug(
|
|
162
108
|
"[subagents] process shutdown reap best-effort failed (killAllSpawnedChildren SIGTERM)",
|
|
163
|
-
{ reason:
|
|
109
|
+
{ reason: toErrorMessage(err) },
|
|
164
110
|
);
|
|
165
111
|
}
|
|
166
112
|
}
|
|
@@ -224,36 +170,14 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
224
170
|
setupWorkflowListInjector(pi);
|
|
225
171
|
setupModelListInjector(pi);
|
|
226
172
|
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return cachedMainSessionFile;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* 按 sessionId 解析主 session 文件路径(文件名约定 `<ISO 时间戳>_<sessionId>.jsonl`)。
|
|
235
|
-
* [E2E 实测] attach 场景下 ctx.sessionManager.getSessionFile() 会返回前一 session 的
|
|
236
|
-
* 文件(session_start(root=01a01bf5) 时仍返回刚新建 session 的路径)——恢复逻辑
|
|
237
|
-
* 读错文件会整段漏判。此处按 id 从 sessions 目录解析为准;新 session 文件未 flush
|
|
238
|
-
* 时(AGENTS.md 规则 6:首条 assistant 消息前可能不存在)返回 undefined,调用方
|
|
239
|
-
* (fork 解析 / 孤儿恢复)对该场景本就无 entry 可读。
|
|
240
|
-
*/
|
|
241
|
-
function resolveMainSessionFileById(sessionId: string): string | undefined {
|
|
242
|
-
const sessionsDir = path.join(getAgentDir(), "..", "sessions");
|
|
243
|
-
try {
|
|
244
|
-
const match = fs.readdirSync(sessionsDir).find((f) => f.endsWith(`_${sessionId}.jsonl`));
|
|
245
|
-
return match === undefined ? undefined : path.join(sessionsDir, match);
|
|
246
|
-
} catch {
|
|
247
|
-
return undefined;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
173
|
+
// [主 session 文件解析随迁] cachedMainSessionFile 缓存与 resolveMainSessionFileById
|
|
174
|
+
// 随 session_start 装配块整体迁入 session-lifecycle.ts(唯一消费方是随迁块;
|
|
175
|
+
// SubagentService 的 getMainSessionFile getter 由 createOrReuseServices 提供)。
|
|
250
176
|
|
|
251
|
-
// resources_discover
|
|
252
|
-
// pi 核心 auto-discovery 已覆盖 .agents/skills
|
|
253
|
-
// --skill 由 agent({skill}) 调用方显式传入,无需
|
|
254
|
-
|
|
255
|
-
return {};
|
|
256
|
-
});
|
|
177
|
+
// resources_discover:不再注册 handler(v4 决策:不再注入额外 skill 目录,
|
|
178
|
+
// ADR-031 废弃 discovery.json)。pi 核心 auto-discovery 已覆盖 .agents/skills
|
|
179
|
+
// 等标准目录,子 session 的 --skill 由 agent({skill}) 调用方显式传入,无需
|
|
180
|
+
// extension 额外补充。
|
|
257
181
|
|
|
258
182
|
// ════════════════════════════════════════════════════════════
|
|
259
183
|
// workflow 域:tools + command + pi.__workflowRun + state
|
|
@@ -266,31 +190,13 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
266
190
|
const workerHost = new WorkerHostImpl();
|
|
267
191
|
const registry = new WorkflowScriptRegistryImpl();
|
|
268
192
|
|
|
269
|
-
// SAR 改为 per-session 构造(需要 ctxModel 填底 D-008 + subagentService
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
// per-session 状态(session_start
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
{
|
|
277
|
-
store: JsonlRunStore;
|
|
278
|
-
runs: Map<string, WorkflowRun>;
|
|
279
|
-
sessionDir: string;
|
|
280
|
-
/** D-008 per-session SAR(需要 ctxModel + subagentService) */
|
|
281
|
-
runner: SubprocessAgentRunner;
|
|
282
|
-
/** session 上下文(notifyDone 需要 GuiContext) */
|
|
283
|
-
ctx?: ExtensionContext;
|
|
284
|
-
/** MF-1: store 健康度。session_start 时 store.loadAll 失败则置 false,
|
|
285
|
-
* workflow 域启动时 fail-fast,避免后续 store.save 再次失败导致 run 状态不落地。
|
|
286
|
-
* subagent 域不依赖 store,不受此标志影响。 */
|
|
287
|
-
storeHealthy: boolean;
|
|
288
|
-
/** [engine-awareness D1b] 上一次已知默认引擎(session_start 初始化,per-turn 检测
|
|
289
|
-
* diff 基准)。undefined = 初始化时 config 读失败——首 turn 检测遇 undefined
|
|
290
|
-
* 静默基线化,不算变更、不发通知(防首 turn 伪通知)。 */
|
|
291
|
-
lastEngine?: string;
|
|
292
|
-
}
|
|
293
|
-
>();
|
|
193
|
+
// SAR 改为 per-session 构造(需要 ctxModel 填底 D-008 + subagentService 委托目标),
|
|
194
|
+
// 构造点随 session_start 装配块迁入 session-lifecycle.ts,经 SessionLifecycleResult
|
|
195
|
+
// 传回(见 setupSessionLifecycle)。
|
|
196
|
+
|
|
197
|
+
// per-session 状态(session_start 时重建)。value = SessionLifecycleResult
|
|
198
|
+
// (setupSessionLifecycle 装配结果,ctx 必有)。
|
|
199
|
+
const sessionState = new Map<string, SessionLifecycleResult>();
|
|
294
200
|
|
|
295
201
|
function log(
|
|
296
202
|
level: "debug" | "info" | "warn" | "error",
|
|
@@ -311,21 +217,20 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
311
217
|
}
|
|
312
218
|
}
|
|
313
219
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
220
|
+
// [workflow state 目录解析随迁] resolveSessionDir 随 session_start 装配块整体迁入
|
|
221
|
+
// session-lifecycle.ts(唯一消费方是随迁块)。
|
|
222
|
+
|
|
223
|
+
/** 组合根侧生产 deps 工厂:SessionLifecycleDeps 全部成员有生产默认实现(住
|
|
224
|
+
* session-lifecycle.ts——createOrReuseServices 单例语义 / WorktreeManager 每次
|
|
225
|
+
* 扫描新建 / JsonlRunStore per-session 新建),此处无本地构造可注入;工厂形态
|
|
226
|
+
* 保留为 index 侧注入点(测试或后续演进可在此覆盖)。
|
|
227
|
+
* 测试注入路径:不挂载 index.ts,直接调 setupSessionLifecycle(pi, ctx, fakeDeps)。 */
|
|
228
|
+
function makeLifecycleDeps(): SessionLifecycleDeps {
|
|
229
|
+
return {};
|
|
320
230
|
}
|
|
321
231
|
|
|
322
232
|
function makeDeps(
|
|
323
|
-
state:
|
|
324
|
-
store: JsonlRunStore;
|
|
325
|
-
runs: Map<string, WorkflowRun>;
|
|
326
|
-
sessionDir: string;
|
|
327
|
-
runner: SubprocessAgentRunner;
|
|
328
|
-
},
|
|
233
|
+
state: Pick<SessionLifecycleResult, "store" | "runs" | "sessionDir" | "runner">,
|
|
329
234
|
sessionCtx?: ExtensionContext,
|
|
330
235
|
) {
|
|
331
236
|
const deps: LauncherDeps = {
|
|
@@ -373,284 +278,16 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
373
278
|
|
|
374
279
|
// ════════════════════════════════════════════════════════════
|
|
375
280
|
// session_start:初始化 subagents + workflow 两域
|
|
281
|
+
//
|
|
282
|
+
// 六职责编排(identity 重建 / ledger 装配 / 双 Service 装配 / GC+恢复 / kill-9
|
|
283
|
+
// 循环 / SAR+engine 基线)已随迁 session-lifecycle.ts(bootstrap seam,设计
|
|
284
|
+
// §3.1/D1/D2 原样搬移)。此处仅接线:lastSessionId 先行赋值(时序与原 handler
|
|
285
|
+
// 开头一致)+ 装配结果写入 per-session sessionState。
|
|
376
286
|
// ════════════════════════════════════════════════════════════
|
|
377
287
|
pi.on("session_start", async (_event: SessionStartEvent, ctx: ExtensionContext) => {
|
|
378
|
-
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
lsRef.lastSessionId = sessionId;
|
|
382
|
-
|
|
383
|
-
// [U7] 引擎列表同步 engines.json(幂等零写 + fail-safe;组合根注册已在
|
|
384
|
-
// extension 工厂体完成,此处 registry 已含全部引擎)
|
|
385
|
-
syncEnginesFile(agentDir);
|
|
386
|
-
|
|
387
|
-
// skill 路径两级缓存 session 级失效:pi 同进程可能有多个 session(TUI /new、/fork),
|
|
388
|
-
// 运行中安装的 skill 需对新 session 可见(含曾 miss 缓存的 undefined 条目与 npm 新装
|
|
389
|
-
// 包的候选目录)。session 内复用收益不变(IF8/DM3 消重发生在同 session 的重复调用)。
|
|
390
|
-
clearSkillPathCache();
|
|
391
|
-
|
|
392
|
-
// ── [M4] identity 子进程写入(V2 决策 5)──
|
|
393
|
-
// 子进程经 env(PI_SUBAGENT_*)接收自己的 identity,在 session_start 用 pi.appendEntry
|
|
394
|
-
// 写 subagent-identity custom entry。pi 自动生成 id/parentId → message tree 连续。
|
|
395
|
-
// 旧实现父进程 fs.appendFileSync 补写的 custom entry 缺 id/parentId → 污染 _buildIndex
|
|
396
|
-
// leafId 指针 → message tree 断成两棵 → 多轮对话丢上下文(bug 根因)。
|
|
397
|
-
// 主/子进程判定:PI_SUBAGENT_SELF_RECORD_ID 仅 session-runner spawn 子进程时注入,
|
|
398
|
-
// 主进程无此 env → 跳过(identity 只在子进程写一次)。
|
|
399
|
-
const selfRecordId = process.env.PI_SUBAGENT_SELF_RECORD_ID;
|
|
400
|
-
if (selfRecordId) {
|
|
401
|
-
try {
|
|
402
|
-
const modeEnv = process.env.PI_SUBAGENT_MODE;
|
|
403
|
-
// ExecutionMode 联合窄化:父进程经 env 注入(record.mode 恒为 "background"),
|
|
404
|
-
// 运行时校验合法值,非法兜底 background(避免裸 cast,符合 taste/no-unsafe-cast)。
|
|
405
|
-
const mode: ExecutionMode = modeEnv === "background" ? modeEnv : "background";
|
|
406
|
-
const identity: SubagentIdentityData = {
|
|
407
|
-
id: selfRecordId,
|
|
408
|
-
agent: process.env.PI_SUBAGENT_AGENT ?? "",
|
|
409
|
-
mode,
|
|
410
|
-
task: process.env.PI_SUBAGENT_TASK ?? "",
|
|
411
|
-
slug: process.env.PI_SUBAGENT_SLUG,
|
|
412
|
-
startedAt: Number(process.env.PI_SUBAGENT_STARTED_AT ?? Date.now()),
|
|
413
|
-
rootSessionId: process.env.PI_SUBAGENT_ROOT_SESSION_ID,
|
|
414
|
-
parentRecordId: process.env.PI_SUBAGENT_PARENT_RECORD_ID,
|
|
415
|
-
depth:
|
|
416
|
-
process.env.PI_SUBAGENT_DEPTH !== undefined
|
|
417
|
-
? Number(process.env.PI_SUBAGENT_DEPTH)
|
|
418
|
-
: undefined,
|
|
419
|
-
forkDepth:
|
|
420
|
-
process.env.PI_SUBAGENT_FORK_DEPTH !== undefined
|
|
421
|
-
? Number(process.env.PI_SUBAGENT_FORK_DEPTH)
|
|
422
|
-
: undefined,
|
|
423
|
-
chatMode: process.env.PI_SUBAGENT_CHAT_MODE === "true",
|
|
424
|
-
// [review round2] worktree 隔离标志(session-runner 注入):跨重启重建路径据此
|
|
425
|
-
// 拒绝续聊(handle 不可序列化,reattach 不可行)。
|
|
426
|
-
worktree: process.env.PI_SUBAGENT_WORKTREE === "true",
|
|
427
|
-
};
|
|
428
|
-
pi.appendEntry(IDENTITY_CUSTOM_TYPE, identity);
|
|
429
|
-
} catch (err) {
|
|
430
|
-
logger.warn("[subagents] identity appendEntry failed in session_start", {
|
|
431
|
-
reason: err instanceof Error ? err.message : String(err),
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// ── [U2] 通知账本装配 + 重启恢复(设计 D4:存在性 / 可达性分离)──
|
|
437
|
-
// bind 先于 service.initSession(notifier.revive 在其内——notify() 经
|
|
438
|
-
// getBoundNotifyLedger 消费账本)。recoverFromSession 扫 ledger/ack 两列 entry
|
|
439
|
-
// 差集:未销账号重放投递(已销账零重发,notifyId 幂等);fork 继承未销账
|
|
440
|
-
// pending 属可接受语义(D4 归属规则——扫描域 = 单 session 文件,幂等键作用域
|
|
441
|
-
// 随文件域隔离)。compaction 存活情况归 session_compact handler 的条件降级(P-B4
|
|
442
|
-
// 探针阶段 5 实测,见 notify-ledger.ts compactionCheck)。
|
|
443
|
-
try {
|
|
444
|
-
const ledgerHost: NotifyLedgerHost = {
|
|
445
|
-
appendLedgerEntry: (customType, data) => {
|
|
446
|
-
pi.appendEntry(customType, data);
|
|
447
|
-
},
|
|
448
|
-
readSessionEntries: () => ctx.sessionManager.getEntries(),
|
|
449
|
-
isIdle: () => ctx.isIdle(),
|
|
450
|
-
onAgentSettled: (handler) => {
|
|
451
|
-
pi.on("agent_settled", handler);
|
|
452
|
-
},
|
|
453
|
-
sendDelivery: (message) => {
|
|
454
|
-
// D5 单通道:唯一发送形态 = sendCustomMessage({triggerTurn:true}),
|
|
455
|
-
// courier 已在发送前二次复查 isIdle,多通道投递选项已删(D5)。
|
|
456
|
-
pi.sendMessage(message, { triggerTurn: true });
|
|
457
|
-
},
|
|
458
|
-
};
|
|
459
|
-
// U4:重放观测已内聚到 ledger 分桶日志(recoveryReplays 桶经 extensionLogger
|
|
460
|
-
// 通道落盘),此处不再重复打日志。
|
|
461
|
-
bindNotifyLedgerHost(ledgerHost).recoverFromSession();
|
|
462
|
-
} catch (err) {
|
|
463
|
-
// 账本装配失败不阻断 session_start(通知退回 notifier 的内核路径)
|
|
464
|
-
logger.warn("[subagents] notify ledger bind failed", {
|
|
465
|
-
reason: err instanceof Error ? err.message : String(err),
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// ── subagents 域:双 Service 装配 ──
|
|
470
|
-
const existingService = getSubagentService();
|
|
471
|
-
const existingModelService = getModelConfigService();
|
|
472
|
-
const modelService = existingModelService ?? new ModelConfigService({ agentDir, cwd });
|
|
473
|
-
const service = existingService ?? new SubagentService({ cwd, modelService, getMainSessionFile: getCachedMainSessionFile });
|
|
474
|
-
|
|
475
|
-
modelService.initModel({
|
|
476
|
-
modelRegistry: ctx.modelRegistry,
|
|
477
|
-
sessionId: ctx.sessionManager.getSessionId(),
|
|
478
|
-
ctxModel: ctx.model ?? undefined,
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
// ── W3: handler 注入链路接通 ──
|
|
482
|
-
// 进程级单例:channel registry + dialog queue 跨 session 复用
|
|
483
|
-
//(与 SubagentService 单例模式一致,globalThis Symbol 持有避免 jiti 多实例分裂)。
|
|
484
|
-
const channelRegistry = getOrCreateChannelRegistry();
|
|
485
|
-
const dialogQueue = getOrCreateDialogQueue();
|
|
486
|
-
const uiRequestHandler = createUiRequestHandlerForMode(ctx, channelRegistry, dialogQueue);
|
|
487
|
-
// SR-3: 无论 new 还是 existing(/resume /fork 复用),session_start 都必须注入 handler
|
|
488
|
-
service.setUiRequestHandler(uiRequestHandler);
|
|
489
|
-
|
|
490
|
-
// 主 session 文件:按 sessionId 解析(getSessionFile() 在 attach 场景会返回前一
|
|
491
|
-
// session 的文件,E2E 实测),未 flush 的新 session 回退 getSessionFile()。
|
|
492
|
-
// 值直传 initSession——jiti 多实例分裂下闭包缓存(cachedMainSessionFile)不跨
|
|
493
|
-
// 实例共享,恢复逻辑经缓存读会拿到滞后一个事件的值(E2E 实测 ENOENT 漏判);
|
|
494
|
-
// 缓存本身保留给既有 getter 消费者(fork source 解析)。
|
|
495
|
-
cachedMainSessionFile =
|
|
496
|
-
resolveMainSessionFileById(ctx.sessionManager.getSessionId()) ??
|
|
497
|
-
ctx.sessionManager.getSessionFile() ??
|
|
498
|
-
undefined;
|
|
499
|
-
|
|
500
|
-
service.initSession({
|
|
501
|
-
pi,
|
|
502
|
-
sessionId: ctx.sessionManager.getSessionId(),
|
|
503
|
-
mainSessionFile: cachedMainSessionFile,
|
|
504
|
-
// 注入 ctx.ui.setWidget 作为 streaming sink(只绑方法,不持有整个 ctx)。
|
|
505
|
-
// background subagent 执行期间,text_delta 经 SubagentStream 合并后由此通道转发。
|
|
506
|
-
// [W1 修复] ctx.mode === 'rpc' 守卫:TUI/json/print 下 streamSink = undefined(无 widget 噪音),
|
|
507
|
-
// rpc mode(GUI/xyz-agent)下保持原行为(ctx.ui.setWidget → sidecar → chatStore)。
|
|
508
|
-
// streamSink API 不变(SubagentStream.onDelta 仍可调,只是 TUI 下 stream 不会被创建)。
|
|
509
|
-
streamSink: ctx.mode === "rpc"
|
|
510
|
-
? { setWidget: (key, lines) => ctx.ui.setWidget(key, lines) }
|
|
511
|
-
: undefined,
|
|
512
|
-
// [#24] uiRequestHandler 单一注入入口:上方 setUiRequestHandler 已注入(SR-3 语义,
|
|
513
|
-
// new/existing service 均覆盖)。此处不再重复传 initSession.uiRequestHandler,避免
|
|
514
|
-
// 同一 handler 双路径注入造成的语义混淆与“哪一个是 source of truth”歧义。
|
|
515
|
-
// mode 仍需 session 级注入(uiObservability.setMode 依赖它,与 handler 无关)。
|
|
516
|
-
mode: ctx.mode,
|
|
517
|
-
// SR-4:注入 L2 dialog 队列——session-runner child close 时调 rejectChildDialogs
|
|
518
|
-
// 清理该 child 在 L2 的 pending dialog,防全局死锁(C1 修复:清理路径接通)。
|
|
519
|
-
dialogQueue,
|
|
520
|
-
// [竞态修复] 注入 ctx.isIdle:notifier flush 在主 agent busy 时退避,idle 后再
|
|
521
|
-
// sendMessage(triggerTurn),规避 agent_end→finishRun 窗口里走 steer 分支丢失通知。
|
|
522
|
-
isIdle: () => ctx.isIdle(),
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
if (!existingService) {
|
|
526
|
-
setModelConfigService(modelService);
|
|
527
|
-
setSubagentService(service);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// S-2: 启动 idle record GC 定时器(30 天 TTL,每小时检查一次)
|
|
531
|
-
service.startGcTimer();
|
|
532
|
-
|
|
533
|
-
try {
|
|
534
|
-
maybeCleanupExpiredSessionFiles(agentDir, cwd);
|
|
535
|
-
} catch (err) {
|
|
536
|
-
logger.warn("[subagents] expired session file cleanup failed", {
|
|
537
|
-
reason: err instanceof Error ? err.message : String(err),
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// ADR-035 启动恢复:扫描 manifest tmp 残留(崩溃打断的 writeManifest 留下),
|
|
542
|
-
// 每次 session_start 都调(与上方 maybeCleanupExpiredSessionFiles 一致)。
|
|
543
|
-
try {
|
|
544
|
-
const recovered = await service.recoverManifestTmpFiles();
|
|
545
|
-
if (recovered.recovered > 0 || recovered.deleted > 0) {
|
|
546
|
-
logger.warn(`[subagents] manifest tmp recovery: ${recovered.recovered} promoted, ${recovered.deleted} deleted`);
|
|
547
|
-
}
|
|
548
|
-
} catch (err) {
|
|
549
|
-
logger.warn("[subagents] manifest tmp recovery failed", {
|
|
550
|
-
reason: err instanceof Error ? err.message : String(err),
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
try {
|
|
555
|
-
const wtm = new WorktreeManager(agentDir);
|
|
556
|
-
await wtm.scan();
|
|
557
|
-
} catch (err) {
|
|
558
|
-
logger.warn("[subagents] worktree reaper scan failed", {
|
|
559
|
-
reason: err instanceof Error ? err.message : String(err),
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// ── workflow 域:per-session store + runs ──
|
|
564
|
-
const sessionDir = resolveSessionDir();
|
|
565
|
-
const store = new JsonlRunStore({
|
|
566
|
-
sessionDir,
|
|
567
|
-
pi,
|
|
568
|
-
ctx,
|
|
569
|
-
});
|
|
570
|
-
const runs = new Map<string, WorkflowRun>();
|
|
571
|
-
|
|
572
|
-
// F-4/D-003: agent 发现走 shared/resource-discovery(ADR-031),modelService
|
|
573
|
-
// 自持 AgentRegistry(subagents/workflow 两域共用同一发现结果)。
|
|
574
|
-
// M2 修正:workflow 域 resolveAgentOpts 不再消费 agentRegistry(agent ref 交
|
|
575
|
-
// resolveIdentity),无需经 state 透传——modelService 是唯一 registry 源。
|
|
576
|
-
|
|
577
|
-
// MF-1: store 健康度跟踪。loadAll 失败 → storeHealthy=false,workflow 域启动时 fail-fast。
|
|
578
|
-
let storeHealthy = true;
|
|
579
|
-
try {
|
|
580
|
-
const loaded = await store.loadAll();
|
|
581
|
-
for (const run of loaded) {
|
|
582
|
-
if (run.state.status === "running") {
|
|
583
|
-
run.state.error = "Process killed (kill-9 or crash recovery)";
|
|
584
|
-
run.transition("done", "failed");
|
|
585
|
-
pi.events.emit("pending:unregister", {
|
|
586
|
-
id: run.runId,
|
|
587
|
-
reason: "failed",
|
|
588
|
-
});
|
|
589
|
-
// 恢复终态必须落盘:save 走冷路径(done 绕过去抖)同步写 state 文件 +
|
|
590
|
-
// append 终态 workflow-record entry——entry_appended 事件驱动 runtime 派生
|
|
591
|
-
// 缓存失效重拉(无 triggerTurn 副作用)。不 save 则 entry/state 双双停留
|
|
592
|
-
// running,侧栏永久卡 running。失败仅记日志不阻断其余 run 的恢复(下次
|
|
593
|
-
// session_start 重开重试,恢复循环天然幂等)。
|
|
594
|
-
try {
|
|
595
|
-
await store.save(run);
|
|
596
|
-
} catch (err) {
|
|
597
|
-
logger.error("[subagent-workflow] kill-9 recovery store.save failed", {
|
|
598
|
-
runId: run.runId,
|
|
599
|
-
reason: err instanceof Error ? err.message : String(err),
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
runs.set(run.runId, run);
|
|
604
|
-
}
|
|
605
|
-
// done run 内存有界性:loadAll 全量重水合后立即裁剪到 K。kill-9 恢复(上方
|
|
606
|
-
// running → transition("done","failed"))的 run completedAt 为 transition 时刻
|
|
607
|
-
// (当前时间=全局最新)参与排序且必在保留端;多条恢复 run 同 ms completedAt →
|
|
608
|
-
// tie 稳定排序。淘汰只 delete runs Map 条目——磁盘 state 文件与
|
|
609
|
-
// workflow-state-link 指针条目均不动(历史审计保留);下次 session_start loadAll
|
|
610
|
-
// 从指针全量重水合后再次裁剪,该循环每次 session 启动重复且可接受:内存峰值只在
|
|
611
|
-
// 启动期,常驻 O(K + 活跃 run)。消除启动峰值需指针 compaction,属 append+replay
|
|
612
|
-
// 长期方案问题域,非本范围。
|
|
613
|
-
const evicted = evictDoneRunsBeyondCap(runs, MAX_RETAINED_DONE_RUNS);
|
|
614
|
-
if (evicted > 0) {
|
|
615
|
-
logger.debug("[subagent-workflow] evicted done runs beyond cap after loadAll", {
|
|
616
|
-
evicted,
|
|
617
|
-
keep: MAX_RETAINED_DONE_RUNS,
|
|
618
|
-
sessionId,
|
|
619
|
-
});
|
|
620
|
-
}
|
|
621
|
-
} catch (err) {
|
|
622
|
-
// QMF-4 fix: store.loadAll 失败是关键路径错误,workflow 域将未初始化
|
|
623
|
-
logger.error("[subagent-workflow] store.loadAll failed, workflow domain uninitialized", {
|
|
624
|
-
reason: err instanceof Error ? err.message : String(err),
|
|
625
|
-
});
|
|
626
|
-
storeHealthy = false;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// D-008: per-session SAR(需要 ctxModel 填底 + subagentService 委托目标)。
|
|
630
|
-
// old: const runner = new SubprocessAgentRunner()(module-level singleton,无 deps)
|
|
631
|
-
// new: per-session session_start 时创建,通过 sessionState 传给 makeDeps。
|
|
632
|
-
const runner = new SubprocessAgentRunner({
|
|
633
|
-
subagentService: service,
|
|
634
|
-
ctxModel: ctx.model ?? undefined,
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
// [engine-awareness D1b] lastEngine 初始化:构造性同源——单次 reloadGlobalConfig
|
|
638
|
-
// 读取同时刷新 Service 路由缓存与 lastEngine 基准,消灭 initModel 与本处两次独立
|
|
639
|
-
// 读取间的分叉窗口(两读值不一致时检测走 unchanged 分支不 reload,状态段/路由
|
|
640
|
-
// 永停旧值且永不通知)。ok/absent → 归一后的当前引擎;failed → undefined(首 turn
|
|
641
|
-
// 检测静默基线化兜底,此时缓存亦保持不动)。/resume、/fork 同样走 session_start
|
|
642
|
-
// (SR-3),基线天然覆盖。
|
|
643
|
-
const engineRead = modelService.reloadGlobalConfig();
|
|
644
|
-
sessionState.set(sessionId, {
|
|
645
|
-
store,
|
|
646
|
-
runs,
|
|
647
|
-
sessionDir,
|
|
648
|
-
runner,
|
|
649
|
-
ctx,
|
|
650
|
-
storeHealthy,
|
|
651
|
-
lastEngine:
|
|
652
|
-
engineRead.status === "failed" ? undefined : normalizeEngineId(engineRead.config.defaultEngine),
|
|
653
|
-
});
|
|
288
|
+
lsRef.lastSessionId = ctx.sessionManager.getSessionId();
|
|
289
|
+
const result = await setupSessionLifecycle(pi, ctx, makeLifecycleDeps());
|
|
290
|
+
sessionState.set(result.sessionId, result);
|
|
654
291
|
});
|
|
655
292
|
|
|
656
293
|
// ════════════════════════════════════════════════════════════
|
|
@@ -667,7 +304,7 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
667
304
|
}
|
|
668
305
|
} catch (err) {
|
|
669
306
|
logger.warn("[subagents] notify ledger compactionCheck failed", {
|
|
670
|
-
reason:
|
|
307
|
+
reason: toErrorMessage(err),
|
|
671
308
|
});
|
|
672
309
|
}
|
|
673
310
|
});
|
|
@@ -675,43 +312,16 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
675
312
|
// ════════════════════════════════════════════════════════════
|
|
676
313
|
// [U7 + engine-awareness U3] before_agent_start:引擎感知注入(链尾注册,D7——
|
|
677
314
|
// 段内容变化只断 system prompt 尾部 cache 前缀)。
|
|
678
|
-
//
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
// (D3,不设 triggerTurn——P1 探针已证此形态消息进入本 turn LLM 上下文,
|
|
682
|
-
// 证据:真机 pi rpc payload dump + 0.84.4 dist sendMessage→_appendCustomMessage
|
|
683
|
-
// →agent.state.messages.push→createContextSnapshot 调用链)→ 更新 lastEngine。
|
|
684
|
-
// ② 恒在状态段 <current_subagent_engine>(D6)+ 引擎清单段 <available_<engine>_models>。
|
|
685
|
-
// apply 后 getGlobalConfig() 即新值——通知、状态段、路由三处同 turn 对齐(G2)。
|
|
686
|
-
// 段序:状态段在前(文案声明 "listed ... below"),清单段在后;provider models 段
|
|
687
|
-
// 由更早注册的 handler 注入、位于上方。fail-safe 任何异常不注入不阻塞 agent loop。
|
|
315
|
+
// D7-④:接线收编于 engine-awareness.ts 的 setupEngineAwarenessInjector(与上方
|
|
316
|
+
// 三个 setup* 同形,注入链序由调用先后表达);per-session lastEngine 经
|
|
317
|
+
// sessionState 存取器注入。编排/渲染/链尾依据的完整注释随迁至该函数。
|
|
688
318
|
// ════════════════════════════════════════════════════════════
|
|
689
|
-
pi
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
readConfig: () => readGlobalConfig(getAgentDir()),
|
|
696
|
-
applyRead: (read) => service.applyGlobalConfig(read),
|
|
697
|
-
sendMessage: (message) => {
|
|
698
|
-
// D3:不设 triggerTurn——切换是用户主动行为,无需唤醒 AI 立即行动
|
|
699
|
-
pi.sendMessage(message, {});
|
|
700
|
-
},
|
|
701
|
-
getLastEngine: () => sessionState.get(sid)?.lastEngine,
|
|
702
|
-
setLastEngine: (engine) => {
|
|
703
|
-
const state = sessionState.get(sid);
|
|
704
|
-
if (state) state.lastEngine = engine;
|
|
705
|
-
},
|
|
706
|
-
});
|
|
707
|
-
const defaultEngine = service.getGlobalConfig().defaultEngine;
|
|
708
|
-
const append = [buildSubagentEngineSection(defaultEngine), buildEngineModelsPromptAppend(defaultEngine)]
|
|
709
|
-
.filter((part) => part !== "")
|
|
710
|
-
.join("\n\n");
|
|
711
|
-
return { systemPrompt: `${event.systemPrompt}\n\n${append}` };
|
|
712
|
-
} catch {
|
|
713
|
-
return undefined;
|
|
714
|
-
}
|
|
319
|
+
setupEngineAwarenessInjector(pi, {
|
|
320
|
+
getLastEngine: (sid) => sessionState.get(sid)?.lastEngine,
|
|
321
|
+
setLastEngine: (sid, engine) => {
|
|
322
|
+
const state = sessionState.get(sid);
|
|
323
|
+
if (state) state.lastEngine = engine;
|
|
324
|
+
},
|
|
715
325
|
});
|
|
716
326
|
|
|
717
327
|
// ════════════════════════════════════════════════════════════
|
|
@@ -820,7 +430,7 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
820
430
|
await state.store.dispose().catch((err: unknown) => {
|
|
821
431
|
logger.debug(
|
|
822
432
|
`[subagent-workflow] session_shutdown store.dispose failed (sessionId=${sessionId}, sessionDir=${state.sessionDir})`,
|
|
823
|
-
{ reason:
|
|
433
|
+
{ reason: toErrorMessage(err) },
|
|
824
434
|
);
|
|
825
435
|
});
|
|
826
436
|
sessionState.delete(sessionId);
|
|
@@ -878,6 +488,32 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
878
488
|
process.on("SIGINT", sigintHandler);
|
|
879
489
|
process.on("beforeExit", reapSpawnedChildrenOnShutdown);
|
|
880
490
|
|
|
491
|
+
// ════════════════════════════════════════════════════════════
|
|
492
|
+
// workflow deps 守卫(单一出口)
|
|
493
|
+
//
|
|
494
|
+
// [守卫合一] 原 pi.__workflowRun 内联守卫 + getDeps 守卫两份重复(state 缺失 /
|
|
495
|
+
// storeHealthy=false),合并为单一 getWorkflowDeps:返回 discriminated union,
|
|
496
|
+
// 两个消费点各自决定失败形态——pi.__workflowRun(D-8 API)返回错误对象(不
|
|
497
|
+
// throw,保住调用方 Promise 契约),getDeps(3 个 tool 的 lazy deps 源)throw
|
|
498
|
+
// (pi tool 框架将其转译为 tool 错误结果)。错误消息逐字保留(crash-recovery
|
|
499
|
+
// 测试锁 "store unavailable" / "loadAll failed" 子串)。
|
|
500
|
+
// ════════════════════════════════════════════════════════════
|
|
501
|
+
type WorkflowDepsResolution =
|
|
502
|
+
| { ok: true; deps: LauncherDeps }
|
|
503
|
+
| { ok: false; reason: string };
|
|
504
|
+
|
|
505
|
+
const getWorkflowDeps = (sessionId: string): WorkflowDepsResolution => {
|
|
506
|
+
const state = sessionState.get(sessionId);
|
|
507
|
+
if (!state) {
|
|
508
|
+
return { ok: false, reason: "Session not initialized" };
|
|
509
|
+
}
|
|
510
|
+
// MF-1: store 不健康时 fail-fast,避免 store.save 再次失败导致 run 状态不落地。
|
|
511
|
+
if (!state.storeHealthy) {
|
|
512
|
+
return { ok: false, reason: "Workflow store unavailable (loadAll failed in session_start)" };
|
|
513
|
+
}
|
|
514
|
+
return { ok: true, deps: makeDeps(state, state.ctx) };
|
|
515
|
+
};
|
|
516
|
+
|
|
881
517
|
// ════════════════════════════════════════════════════════════
|
|
882
518
|
// pi.__workflowRun(D-8 签名)
|
|
883
519
|
// ════════════════════════════════════════════════════════════
|
|
@@ -890,28 +526,19 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
890
526
|
// 注意:lastSessionId 是单值假设——Pi 当前保证单 session 串行(一次只一个活跃 session)。
|
|
891
527
|
// 若未来 Pi 支持多 session 并发,此处需改为从 ctx.sessionManager.getSessionId() 显式传入。
|
|
892
528
|
// M-2 已记录此假设。
|
|
893
|
-
const
|
|
894
|
-
if (!
|
|
529
|
+
const resolved = getWorkflowDeps(lsRef.lastSessionId);
|
|
530
|
+
if (!resolved.ok) {
|
|
895
531
|
return {
|
|
896
532
|
status: "done",
|
|
897
533
|
reason: "failed",
|
|
898
|
-
error:
|
|
899
|
-
runId: "",
|
|
900
|
-
};
|
|
901
|
-
}
|
|
902
|
-
// MF-1: store 不健康时 fail-fast,避免 store.save 再次失败导致 run 状态不落地。
|
|
903
|
-
if (!state.storeHealthy) {
|
|
904
|
-
return {
|
|
905
|
-
status: "done",
|
|
906
|
-
reason: "failed",
|
|
907
|
-
error: "Workflow store unavailable (loadAll failed in session_start)",
|
|
534
|
+
error: resolved.reason,
|
|
908
535
|
runId: "",
|
|
909
536
|
};
|
|
910
537
|
}
|
|
911
538
|
return runAndWait(
|
|
912
539
|
workflowName,
|
|
913
540
|
workflowArgs,
|
|
914
|
-
|
|
541
|
+
resolved.deps,
|
|
915
542
|
workflowSignal,
|
|
916
543
|
workflowTimeoutMs,
|
|
917
544
|
);
|
|
@@ -919,46 +546,65 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
919
546
|
|
|
920
547
|
// ════════════════════════════════════════════════════════════
|
|
921
548
|
// Tools(3 个)—— lazy deps 注入
|
|
549
|
+
//
|
|
550
|
+
// lazyDeps:属性访问触发 getWorkflowDeps 守卫 + makeDeps 求值(每属性独立)。
|
|
551
|
+
// 守卫合一后 getWorkflowDeps 返回 discriminated union,getter 内消费时 throw
|
|
552
|
+
// (与 __workflowRun 的 return 错误对象对齐——同源同消息,session-lifecycle.test.ts 锁定)。
|
|
922
553
|
// ════════════════════════════════════════════════════════════
|
|
923
|
-
const getDeps = () => {
|
|
924
|
-
// 注意:lastSessionId 是单值假设——Pi 当前保证单 session 串行(一次只一个活跃 session)。
|
|
925
|
-
// 若未来 Pi 支持多 session 并发,此处需改为从 ctx.sessionManager.getSessionId() 显式传入。
|
|
926
|
-
// M-2 已记录此假设。
|
|
927
|
-
const state = sessionState.get(lsRef.lastSessionId);
|
|
928
|
-
if (!state) throw new Error("Session not initialized");
|
|
929
|
-
// MF-1: store 不健康时 fail-fast,避免 store.save 再次失败导致 run 状态不落地。
|
|
930
|
-
if (!state.storeHealthy) {
|
|
931
|
-
throw new Error("Workflow store unavailable (loadAll failed in session_start)");
|
|
932
|
-
}
|
|
933
|
-
return makeDeps(state, state.ctx);
|
|
934
|
-
};
|
|
935
|
-
|
|
936
554
|
const lazyDeps: LauncherDeps = {
|
|
937
555
|
get store() {
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
get runner() {
|
|
942
|
-
return getDeps().runner;
|
|
556
|
+
const resolved = getWorkflowDeps(lsRef.lastSessionId);
|
|
557
|
+
if (!resolved.ok) throw new Error(resolved.reason);
|
|
558
|
+
return resolved.deps.store;
|
|
943
559
|
},
|
|
944
560
|
get runs() {
|
|
945
|
-
|
|
561
|
+
const resolved = getWorkflowDeps(lsRef.lastSessionId);
|
|
562
|
+
if (!resolved.ok) throw new Error(resolved.reason);
|
|
563
|
+
return resolved.deps.runs;
|
|
564
|
+
},
|
|
565
|
+
get registry() {
|
|
566
|
+
const resolved = getWorkflowDeps(lsRef.lastSessionId);
|
|
567
|
+
if (!resolved.ok) throw new Error(resolved.reason);
|
|
568
|
+
return resolved.deps.registry;
|
|
946
569
|
},
|
|
947
|
-
registry,
|
|
948
570
|
get onRunDone() {
|
|
949
|
-
|
|
571
|
+
const resolved = getWorkflowDeps(lsRef.lastSessionId);
|
|
572
|
+
if (!resolved.ok) throw new Error(resolved.reason);
|
|
573
|
+
return resolved.deps.onRunDone;
|
|
950
574
|
},
|
|
951
575
|
get eventBus() {
|
|
952
|
-
|
|
576
|
+
const resolved = getWorkflowDeps(lsRef.lastSessionId);
|
|
577
|
+
if (!resolved.ok) throw new Error(resolved.reason);
|
|
578
|
+
return resolved.deps.eventBus;
|
|
579
|
+
},
|
|
580
|
+
get workerHost() {
|
|
581
|
+
const resolved = getWorkflowDeps(lsRef.lastSessionId);
|
|
582
|
+
if (!resolved.ok) throw new Error(resolved.reason);
|
|
583
|
+
return resolved.deps.workerHost;
|
|
584
|
+
},
|
|
585
|
+
get runner() {
|
|
586
|
+
const resolved = getWorkflowDeps(lsRef.lastSessionId);
|
|
587
|
+
if (!resolved.ok) throw new Error(resolved.reason);
|
|
588
|
+
return resolved.deps.runner;
|
|
953
589
|
},
|
|
590
|
+
// scheduleTimeBudget / onWorkflowCall 不可缺席(ports.ts D-12 regression fix):
|
|
591
|
+
// rebuildRuntime 重排 run 级墙钟预算计时器、worker 脚本嵌套 workflow() 调用都经
|
|
592
|
+
// 这两个成员消费——lazyDeps 缺席会让消费点拿到 undefined(可选属性静默放行),
|
|
593
|
+
// 带时间预算的 run 命中一次错误重试后计时器静默失效。转发形态与其余成员一致。
|
|
954
594
|
get scheduleTimeBudget() {
|
|
955
|
-
|
|
595
|
+
const resolved = getWorkflowDeps(lsRef.lastSessionId);
|
|
596
|
+
if (!resolved.ok) throw new Error(resolved.reason);
|
|
597
|
+
return resolved.deps.scheduleTimeBudget;
|
|
956
598
|
},
|
|
957
599
|
get onWorkflowCall() {
|
|
958
|
-
|
|
600
|
+
const resolved = getWorkflowDeps(lsRef.lastSessionId);
|
|
601
|
+
if (!resolved.ok) throw new Error(resolved.reason);
|
|
602
|
+
return resolved.deps.onWorkflowCall;
|
|
959
603
|
},
|
|
960
604
|
get log() {
|
|
961
|
-
|
|
605
|
+
const resolved = getWorkflowDeps(lsRef.lastSessionId);
|
|
606
|
+
if (!resolved.ok) throw new Error(resolved.reason);
|
|
607
|
+
return resolved.deps.log;
|
|
962
608
|
},
|
|
963
609
|
};
|
|
964
610
|
|
|
@@ -983,31 +629,12 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
983
629
|
// ============================================================
|
|
984
630
|
|
|
985
631
|
// channel registry 经 channel-registry-access.ts 公开访问(跨扩展 API)。
|
|
986
|
-
// dialog queue
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
Reflect.set(globalThis, DIALOG_QUEUE_KEY, queue);
|
|
996
|
-
}
|
|
997
|
-
return queue;
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
// ============================================================
|
|
1001
|
-
// Public cross-extension API(channel handler 注册入口)
|
|
1002
|
-
// ============================================================
|
|
1003
|
-
//
|
|
1004
|
-
// 跨扩展消费者(ask-user 等)通过包根 import 注册 channel handler,
|
|
1005
|
-
// 让 subagent 子进程的 UI 请求(ask_user 等)透传到主进程渲染。
|
|
1006
|
-
// 重新导出 channel-registry-access 的公开 API——稳定 surface,
|
|
1007
|
-
// 内部存储实现演进不影响消费者。
|
|
1008
|
-
|
|
1009
|
-
export {
|
|
1010
|
-
getOrCreateChannelRegistry,
|
|
1011
|
-
type UiChannelRegistry,
|
|
1012
|
-
type ChannelHandler,
|
|
1013
|
-
} from "@zhushanwen/subagent-core/execution/channel-registry-access.ts";
|
|
632
|
+
// dialog queue 单例随 session_start 装配域迁居 session-lifecycle.ts
|
|
633
|
+
// (getOrCreateDialogQueue;消费方 = 该文件 createOrReuseServices + 本文件
|
|
634
|
+
// session_shutdown),Symbol key 单定义点随迁,避免双份定义漂移。
|
|
635
|
+
|
|
636
|
+
// 跨扩展 channel handler 注册入口已收口到 core 深路径
|
|
637
|
+
// `@zhushanwen/subagent-core/execution/channel-registry-access.ts`
|
|
638
|
+
// (getOrCreateChannelRegistry / UiChannelRegistry / ChannelHandler)。
|
|
639
|
+
// 历史上的包根 re-export 已删:ask-user 等跨扩展消费者经 globalThis 握手
|
|
640
|
+
// (DIALOG_QUEUE_KEY 同款进程级单例),不再经包根 import 消费本模块。
|