@zhushanwen/pi-subagent-workflow 0.1.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.
- package/agents/context-builder.md +17 -0
- package/agents/general-purpose.md +16 -0
- package/agents/oracle.md +17 -0
- package/agents/planner.md +17 -0
- package/agents/researcher.md +17 -0
- package/agents/reviewer.md +17 -0
- package/agents/scout.md +17 -0
- package/agents/worker.md +16 -0
- package/examples/README.md +43 -0
- package/examples/chain.example.js +92 -0
- package/examples/map-reduce.example.js +99 -0
- package/examples/parallel.example.js +82 -0
- package/examples/scatter-gather.example.js +106 -0
- package/index.ts +1 -0
- package/package.json +66 -0
- package/skills/workflow-script-format/SKILL.md +328 -0
- package/src/execution/__tests__/agent-registry.test.ts +164 -0
- package/src/execution/__tests__/agent-result-mapper.test.ts +128 -0
- package/src/execution/__tests__/alive-store.test.ts +147 -0
- package/src/execution/__tests__/bg-notify-render.test.ts +256 -0
- package/src/execution/__tests__/concurrency-pool.test.ts +217 -0
- package/src/execution/__tests__/config.test.ts +110 -0
- package/src/execution/__tests__/crash-recovery.test.ts +311 -0
- package/src/execution/__tests__/execute-nesting.test.ts +359 -0
- package/src/execution/__tests__/execute-options-mapper.test.ts +138 -0
- package/src/execution/__tests__/execution-record.test.ts +959 -0
- package/src/execution/__tests__/finalized-marker.test.ts +82 -0
- package/src/execution/__tests__/format-schema-instruction.test.ts +135 -0
- package/src/execution/__tests__/format.test.ts +320 -0
- package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
- package/src/execution/__tests__/list-component.test.ts +347 -0
- package/src/execution/__tests__/model-resolver.test.ts +356 -0
- package/src/execution/__tests__/output-collector.test.ts +61 -0
- package/src/execution/__tests__/path-encoding.test.ts +75 -0
- package/src/execution/__tests__/pi-invocation.test.ts +73 -0
- package/src/execution/__tests__/record-store.test.ts +545 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +439 -0
- package/src/execution/__tests__/run-spawn-integration.test.ts +897 -0
- package/src/execution/__tests__/sdk-contract.test.ts +272 -0
- package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
- package/src/execution/__tests__/session-file-gc.test.ts +247 -0
- package/src/execution/__tests__/session-reconstructor.test.ts +359 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +314 -0
- package/src/execution/__tests__/session-start-reaper.test.ts +227 -0
- package/src/execution/__tests__/spawn-args.test.ts +244 -0
- package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
- package/src/execution/__tests__/subagent-service.test.ts +678 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +389 -0
- package/src/execution/__tests__/temp-prompt.test.ts +53 -0
- package/src/execution/__tests__/timeout-integration.test.ts +381 -0
- package/src/execution/__tests__/tombstone-store.test.ts +73 -0
- package/src/execution/__tests__/tool-action.test.ts +330 -0
- package/src/execution/__tests__/turn-limiter.test.ts +65 -0
- package/src/execution/__tests__/worktree-manager.test.ts +423 -0
- package/src/execution/__tests__/worktree-registry.test.ts +161 -0
- package/src/execution/agent-registry.ts +252 -0
- package/src/execution/agent-result-mapper.ts +84 -0
- package/src/execution/alive-store.ts +92 -0
- package/src/execution/best-effort.ts +30 -0
- package/src/execution/concurrency-pool.ts +84 -0
- package/src/execution/config.ts +73 -0
- package/src/execution/execute-options-mapper.ts +86 -0
- package/src/execution/execution-record.ts +778 -0
- package/src/execution/finalized-marker.ts +51 -0
- package/src/execution/model-config-service.ts +225 -0
- package/src/execution/model-resolver.ts +247 -0
- package/src/execution/notifier.ts +168 -0
- package/src/execution/output-collector.ts +88 -0
- package/src/execution/path-encoding.ts +34 -0
- package/src/execution/pi-invocation.ts +70 -0
- package/src/execution/record-store.ts +350 -0
- package/src/execution/session-context-resolver.ts +64 -0
- package/src/execution/session-file-gc.ts +98 -0
- package/src/execution/session-reconstructor.ts +450 -0
- package/src/execution/session-runner.ts +725 -0
- package/src/execution/spawn-event-adapter.ts +150 -0
- package/src/execution/subagent-service.ts +973 -0
- package/src/execution/subprocess-agent-runner.ts +108 -0
- package/src/execution/temp-prompt.ts +57 -0
- package/src/execution/tombstone-store.ts +72 -0
- package/src/execution/turn-limiter.ts +88 -0
- package/src/execution/types.ts +634 -0
- package/src/execution/worktree-manager.ts +285 -0
- package/src/execution/worktree-registry.ts +144 -0
- package/src/index.ts +454 -0
- package/src/interface/bg-notify-render.ts +286 -0
- package/src/interface/commands.ts +157 -0
- package/src/interface/format.ts +501 -0
- package/src/interface/gui-adapter.ts +136 -0
- package/src/interface/helpers.ts +110 -0
- package/src/interface/list-component.ts +643 -0
- package/src/interface/list-shared.ts +84 -0
- package/src/interface/list-view.ts +373 -0
- package/src/interface/reentry-guard.ts +30 -0
- package/src/interface/subagent-actions.ts +294 -0
- package/src/interface/subagent-tool.ts +294 -0
- package/src/interface/subagents.ts +30 -0
- package/src/interface/tool-render.ts +333 -0
- package/src/interface/tool-workflow-script.ts +351 -0
- package/src/interface/tool-workflow.ts +485 -0
- package/src/interface/views/WorkflowsView.ts +944 -0
- package/src/interface/views/detail-content.ts +298 -0
- package/src/interface/views/format.ts +320 -0
- package/src/orchestration/__tests__/concurrency-gate.test.ts +125 -0
- package/src/orchestration/__tests__/config-loader.test.ts +381 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +332 -0
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +248 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +385 -0
- package/src/orchestration/__tests__/script-lint.test.ts +347 -0
- package/src/orchestration/__tests__/worker-script-builder.test.ts +42 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +319 -0
- package/src/orchestration/agent-opts-resolver.ts +128 -0
- package/src/orchestration/concurrency-gate.ts +69 -0
- package/src/orchestration/config-loader.ts +313 -0
- package/src/orchestration/error-recovery.ts +578 -0
- package/src/orchestration/execute-agent-call.ts +174 -0
- package/src/orchestration/jsonl-run-store.ts +292 -0
- package/src/orchestration/launcher.ts +368 -0
- package/src/orchestration/lifecycle.ts +373 -0
- package/src/orchestration/models/__tests__/budget.test.ts +367 -0
- package/src/orchestration/models/agent-call.ts +76 -0
- package/src/orchestration/models/budget.ts +148 -0
- package/src/orchestration/models/ports.ts +165 -0
- package/src/orchestration/models/run-runtime.ts +91 -0
- package/src/orchestration/models/run-spec.ts +54 -0
- package/src/orchestration/models/run-state.ts +44 -0
- package/src/orchestration/models/trace.ts +102 -0
- package/src/orchestration/models/types.ts +242 -0
- package/src/orchestration/models/workflow-run.ts +275 -0
- package/src/orchestration/models/workflow-script-registry.ts +32 -0
- package/src/orchestration/models/workflow-script.ts +90 -0
- package/src/orchestration/node-ops.ts +192 -0
- package/src/orchestration/script-lint.ts +387 -0
- package/src/orchestration/skill-discovery.ts +60 -0
- package/src/orchestration/worker-handle.ts +115 -0
- package/src/orchestration/worker-host.ts +93 -0
- package/src/orchestration/worker-script-builder.ts +281 -0
- package/src/orchestration/workflow-files.ts +85 -0
- package/src/orchestration/workflow-script-registry-impl.ts +128 -0
- package/src/shared/__tests__/resource-discovery.test.ts +226 -0
- package/src/shared/agent-event.ts +13 -0
- package/src/shared/resource-discovery.ts +535 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/runtime/execution/finalized-marker.ts
|
|
2
|
+
//
|
|
3
|
+
// Finalized 状态的 sidecar 持久化。
|
|
4
|
+
//
|
|
5
|
+
// 背景:subagent 执行完成(done/failed)后,session.jsonl 的最终写入可能因进程
|
|
6
|
+
// 异常终止而丢失。`.finalized` sidecar 标记该 session 已正常结束,collectRecords
|
|
7
|
+
// 重建时可用它区分「正常结束但文件截断」与「执行中断」。
|
|
8
|
+
//
|
|
9
|
+
// 设计:对称 cancelled sidecar(tombstone-store.ts)。`.finalized` 为空文件,
|
|
10
|
+
// 存在性即信号(done/failed 的细分仍靠 recon.stopReason 推断)。与 `.cancelled`
|
|
11
|
+
// 互斥——一个 session 要么 finalized 要么 cancelled,不可能两者兼有。
|
|
12
|
+
//
|
|
13
|
+
// best-effort:写 IO 错静默,不阻断主流程。
|
|
14
|
+
|
|
15
|
+
import * as fs from "node:fs";
|
|
16
|
+
|
|
17
|
+
// ============================================================
|
|
18
|
+
// 公开函数
|
|
19
|
+
// ============================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 在 session.jsonl 旁写 `.finalized` sidecar(空文件)。
|
|
23
|
+
* best-effort:任何 I/O 错误静默(finalize 标记是次要信号,status 已在内存 record 上设好)。
|
|
24
|
+
*
|
|
25
|
+
* 与 `.cancelled` 互斥(BC-4):写前删除 `.cancelled`(如存在),确保不会两者共存。
|
|
26
|
+
*/
|
|
27
|
+
export function writeFinalized(sessionFile: string): void {
|
|
28
|
+
try {
|
|
29
|
+
// BC-4 互斥:清理可能存在的 .cancelled。force:true 静默 ENOENT——
|
|
30
|
+
// 未 cancel 过的 session(done/failed/aborted)本就无 .cancelled,属正常路径。
|
|
31
|
+
// 此前用 unlinkSync+bestEffort 记 console.debug,取消嵌套链条时每层 ENOENT 刷屏。
|
|
32
|
+
fs.rmSync(`${sessionFile}.cancelled`, { force: true });
|
|
33
|
+
fs.writeFileSync(`${sessionFile}.finalized`, "", "utf-8");
|
|
34
|
+
} catch (_e) {
|
|
35
|
+
void _e; // 静默:写失败不阻断 finalize 主流程。
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 读 session.jsonl 旁的 `.finalized` sidecar。
|
|
41
|
+
* 返回 true:sidecar 存在(内容不校验,存在性即信号)。
|
|
42
|
+
* 返回 false:sidecar 不存在 / 读取失败。
|
|
43
|
+
*/
|
|
44
|
+
export function readFinalized(sessionFile: string): boolean {
|
|
45
|
+
try {
|
|
46
|
+
fs.accessSync(`${sessionFile}.finalized`);
|
|
47
|
+
return true;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// src/runtime/model-config-service.ts
|
|
2
|
+
//
|
|
3
|
+
// 配置 + 模型解析领域 Service。"给定 agent 名 + 用户参数 + 主 agent 模型,用哪个模型?"
|
|
4
|
+
//
|
|
5
|
+
// 与 SubagentService(执行/记录/通知域)正交——本 Service 不碰 pool/store/notifier。
|
|
6
|
+
// 上游:SubagentService.execute 内部调 resolveModel。
|
|
7
|
+
// session_start 时经 initModel 注入 modelRegistry。
|
|
8
|
+
|
|
9
|
+
import { findWorkspaceRoot } from "../shared/resource-discovery.ts";
|
|
10
|
+
import { AgentRegistry, createPackageBuiltinRegistry } from "./agent-registry.ts";
|
|
11
|
+
import {
|
|
12
|
+
loadGlobalConfig,
|
|
13
|
+
} from "./config.ts";
|
|
14
|
+
import {
|
|
15
|
+
type AgentConfig,
|
|
16
|
+
type ModelInfo,
|
|
17
|
+
type ModelRegistryLike,
|
|
18
|
+
type ResolvedModel,
|
|
19
|
+
resolveModel,
|
|
20
|
+
} from "./model-resolver.ts";
|
|
21
|
+
import type { SubagentsGlobalConfig } from "./types.ts";
|
|
22
|
+
|
|
23
|
+
// ============================================================
|
|
24
|
+
// 类型
|
|
25
|
+
// ============================================================
|
|
26
|
+
|
|
27
|
+
/** Service 构造参数(进程级,跨 session 不变)。 */
|
|
28
|
+
export interface ModelConfigServiceInit {
|
|
29
|
+
agentDir: string;
|
|
30
|
+
/** 项目根目录(ctx.cwd,用于推导 workspaceRoot 扫描 project 级资源)。 */
|
|
31
|
+
cwd: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** session_start 注入参数(session 级,每次重建)。 */
|
|
35
|
+
export interface ModelServiceSessionInit {
|
|
36
|
+
/** 模型注册表(鉴权 + 发现)。null 立即抛错(fail-fast)。 */
|
|
37
|
+
modelRegistry: ModelRegistryLike | null;
|
|
38
|
+
/** 当前 session ID。 */
|
|
39
|
+
sessionId: string;
|
|
40
|
+
/**
|
|
41
|
+
* 主 agent 当前 model(session_start 时注入,model_select 时刷新)。
|
|
42
|
+
*
|
|
43
|
+
* renderCall 阶段的 ToolRenderContext 不含 model 字段(SDK 限制),无法直接拿到
|
|
44
|
+
* 主 agent model。缓存后 renderCall 的 resolveModel 能命中第三层(ctxModel),
|
|
45
|
+
* 让标题行恢复显示 model——即使未显式传 model 也能展示默认 model。
|
|
46
|
+
*
|
|
47
|
+
* [HISTORICAL] 99f20da1e 引入三层 fallback 后,renderCall 因拿不到 ctxModel
|
|
48
|
+
* 而 resolveModel 拗错→降级不显示 model。此缓存修复该降级。
|
|
49
|
+
*/
|
|
50
|
+
ctxModel?: ModelInfo;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ============================================================
|
|
54
|
+
// ModelConfigService
|
|
55
|
+
// ============================================================
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 配置 + 模型解析 Service。进程级单例。
|
|
59
|
+
*
|
|
60
|
+
* ┌──────────────────────────────────────────────────────┐
|
|
61
|
+
* │ globalConfig(~/.pi/.../config.json,仅 maxConcurrent)│
|
|
62
|
+
* │ agentRegistry(agent .md 发现 + frontmatter) │
|
|
63
|
+
* │ modelRegistry(SDK 注入的可用模型) │
|
|
64
|
+
* │ │
|
|
65
|
+
* │ resolveModel: override → agentConfig → 主 agent model │
|
|
66
|
+
* └──────────────────────────────────────────────────────┘
|
|
67
|
+
*/
|
|
68
|
+
export class ModelConfigService {
|
|
69
|
+
private globalConfig: SubagentsGlobalConfig;
|
|
70
|
+
private readonly agentRegistry: AgentRegistry;
|
|
71
|
+
private readonly agentRegistryDir: string;
|
|
72
|
+
private readonly workspaceRoot: string;
|
|
73
|
+
private modelRegistry: ModelRegistryLike | null = null;
|
|
74
|
+
private _sessionId: string | undefined;
|
|
75
|
+
/** 主 agent 当前 model 缓存(session_start 注入,model_select 刷新)。 */
|
|
76
|
+
private _ctxModel: ModelInfo | undefined;
|
|
77
|
+
|
|
78
|
+
/** 包内 builtin agent(agents/*.md,优先级最低,被用户覆盖)。 */
|
|
79
|
+
private readonly builtinRegistry = createPackageBuiltinRegistry();
|
|
80
|
+
|
|
81
|
+
constructor(init: ModelConfigServiceInit) {
|
|
82
|
+
this.agentRegistryDir = init.agentDir;
|
|
83
|
+
this.workspaceRoot = findWorkspaceRoot(init.cwd);
|
|
84
|
+
this.globalConfig = loadGlobalConfig(init.agentDir);
|
|
85
|
+
// 统一资源发现(ADR-031):扫描源由 workspaceRoot + agentDir 推导
|
|
86
|
+
this.agentRegistry = new AgentRegistry({
|
|
87
|
+
workspaceRoot: this.workspaceRoot,
|
|
88
|
+
agentDir: this.agentRegistryDir,
|
|
89
|
+
});
|
|
90
|
+
this.agentRegistry.discoverAll(this.builtinRegistry);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── 生命周期(index.ts 调)──────────────────────────────
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* session_start 注入。封装 3 步固定时序:
|
|
97
|
+
* 1. reloadGlobalConfig(复用时拿最新 config)
|
|
98
|
+
* 2. injectModelRegistry(fail-fast:null 抛错)
|
|
99
|
+
* 3. setSessionId
|
|
100
|
+
*/
|
|
101
|
+
initModel(init: ModelServiceSessionInit): void {
|
|
102
|
+
// 1. 重载配置 + 重扫 agent(hot-reload:用户可能新增/修改 agent .md)
|
|
103
|
+
this.globalConfig = loadGlobalConfig(this.agentRegistryDir);
|
|
104
|
+
this.agentRegistry.discoverAll(this.builtinRegistry);
|
|
105
|
+
|
|
106
|
+
// 2. modelRegistry(fail-fast)
|
|
107
|
+
if (init.modelRegistry === null) {
|
|
108
|
+
throw new Error("modelRegistry is required but got null");
|
|
109
|
+
}
|
|
110
|
+
this.modelRegistry = init.modelRegistry;
|
|
111
|
+
|
|
112
|
+
// 3. sessionId + ctxModel 缓存(model_select 后续调 setCtxModel 刷新)
|
|
113
|
+
this._sessionId = init.sessionId;
|
|
114
|
+
this._ctxModel = init.ctxModel;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 刷新主 agent model 缓存。model_select 事件时调用。
|
|
119
|
+
* renderCall 的 resolveModel 读此缓存以显示标题行 model。
|
|
120
|
+
*/
|
|
121
|
+
setCtxModel(model: ModelInfo | undefined): void {
|
|
122
|
+
this._ctxModel = model;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── 模型解析(SubagentService.execute 内部调)──────────────
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 解析 agent 的模型(三层:override → agentConfig → 主 agent model)。
|
|
129
|
+
*
|
|
130
|
+
* @param agentName agent 名(查 agentConfig.model override)
|
|
131
|
+
* @param override 调用方显式 override(最高优先级)
|
|
132
|
+
* @param ctxModel 主 agent 当前模型(兜底,直接透传)
|
|
133
|
+
*/
|
|
134
|
+
resolveModel(
|
|
135
|
+
agentName: string,
|
|
136
|
+
override?: { model?: string; thinkingLevel?: string },
|
|
137
|
+
ctxModel?: ModelInfo,
|
|
138
|
+
): ResolvedModel {
|
|
139
|
+
this.assertReady();
|
|
140
|
+
const agentConfig = this.agentRegistry.get(agentName);
|
|
141
|
+
// ctxModel 优先用显式传入(execute 路径),其次用 session 缓存(renderCall 路径)
|
|
142
|
+
return resolveModel(agentConfig, this.modelRegistry!, override, ctxModel ?? this._ctxModel);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 查询 agent 配置(SubagentService 内部判定 defaultBackground 用)。 */
|
|
146
|
+
getAgentConfig(name?: string): AgentConfig | undefined {
|
|
147
|
+
return name ? this.agentRegistry.get(name) : undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── 配置读取(subagent-service 调)────────────────────────
|
|
151
|
+
|
|
152
|
+
/** 全局配置深拷贝(调用方拿到副本,改不影响 Service 内部)。 */
|
|
153
|
+
getGlobalConfig(): SubagentsGlobalConfig {
|
|
154
|
+
return structuredClone(this.globalConfig);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** 内部:session id 缓存(initModel 注入;当前无消费者,保留供未来 session 作用域需求)。 */
|
|
158
|
+
get sessionId(): string | undefined {
|
|
159
|
+
return this._sessionId;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** agent 配置目录(SubagentService 构造 store/SessionRunnerContext 时读)。 */
|
|
163
|
+
getAgentDir(): string {
|
|
164
|
+
return this.agentRegistryDir;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* AgentRegistry 实例(workflow 域复用,F-4/D-003 统一)。
|
|
169
|
+
*
|
|
170
|
+
* workflow 域(resolveAgentOpts via LifecycleDeps.agentRegistry)需要与 subagents 域
|
|
171
|
+
* 完全相同的 agent 名→config 解析,复用本实例避免二次扫描。registry 在 initModel
|
|
172
|
+
* (每次 session_start)调 discoverAll hot-reload,两域共享同一份发现结果。
|
|
173
|
+
*/
|
|
174
|
+
getAgentRegistry(): AgentRegistry {
|
|
175
|
+
return this.agentRegistry;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** modelRegistry(SubagentService 构造 factoryCtx 时读)。已注入保证非 null。 */
|
|
179
|
+
getModelRegistry(): ModelRegistryLike {
|
|
180
|
+
if (this.modelRegistry === null) {
|
|
181
|
+
throw new Error("modelRegistry not injected (initModel not called?)");
|
|
182
|
+
}
|
|
183
|
+
return this.modelRegistry;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── 内部 ────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
/** 校验 modelRegistry 已注入。 */
|
|
189
|
+
private assertReady(): void {
|
|
190
|
+
if (this.modelRegistry === null) {
|
|
191
|
+
throw new Error("modelRegistry not injected (initModel not called?)");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ============================================================
|
|
197
|
+
// 进程单例访问器
|
|
198
|
+
// ============================================================
|
|
199
|
+
|
|
200
|
+
// 用 globalThis[Symbol.for] 持有进程单例,避免 jiti 因路径字符串不同加载多份模块
|
|
201
|
+
// 导致单例分裂(详见 docs/standards.md §7.5)。
|
|
202
|
+
const MODEL_SERVICE_SLOT_KEY = Symbol.for("@zhushanwen/pi-subagents.model-service");
|
|
203
|
+
|
|
204
|
+
type ModelServiceSlot = { current: ModelConfigService | null };
|
|
205
|
+
|
|
206
|
+
function getModelServiceSlot(): ModelServiceSlot {
|
|
207
|
+
// globalThis 无 symbol 索引签名,但运行时支持 symbol 键——用 Reflect 安全读写,
|
|
208
|
+
// 避免双重断言。ModelServiceSlot 是运行时保证的固定形状(同文件唯一写入点)。
|
|
209
|
+
let slot = Reflect.get(globalThis, MODEL_SERVICE_SLOT_KEY) as ModelServiceSlot | undefined;
|
|
210
|
+
if (!slot) {
|
|
211
|
+
slot = { current: null };
|
|
212
|
+
Reflect.set(globalThis, MODEL_SERVICE_SLOT_KEY, slot);
|
|
213
|
+
}
|
|
214
|
+
return slot;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** 获取进程单例。session_start 前为 null。 */
|
|
218
|
+
export function getModelConfigService(): ModelConfigService | null {
|
|
219
|
+
return getModelServiceSlot().current;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** 设置进程单例(session_start 首次创建时)。 */
|
|
223
|
+
export function setModelConfigService(service: ModelConfigService): void {
|
|
224
|
+
getModelServiceSlot().current = service;
|
|
225
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// src/core/model-resolver.ts
|
|
2
|
+
//
|
|
3
|
+
// 模型解析(三层):
|
|
4
|
+
// 1. 用户显式 override(tool 参数 startParam.model)→ registry lookup + auth
|
|
5
|
+
// 2. agent .md frontmatter model(agent 作者指定)→ registry lookup + auth
|
|
6
|
+
// 3. 主 agent 当前模型(ctx.model)→ 直接透传,无需 lookup
|
|
7
|
+
//
|
|
8
|
+
// 设计:默认与主 agent 同模型(零配置)。只有「有人显式指定 model」时才查
|
|
9
|
+
// registry 做解析 + 鉴权校验。thinkingLevel 同链路,无指定时 undefined。
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ModelRegistry 的最小接口(duck-typed,测试可 mock)。
|
|
13
|
+
* 字段结构与 Pi SDK 的 ctx.modelRegistry 对齐(见 shared/types stub)。
|
|
14
|
+
*/
|
|
15
|
+
export interface ModelRegistryLike {
|
|
16
|
+
/** 返回所有已配置鉴权的可用模型。 */
|
|
17
|
+
getAvailable(): ModelInfo[];
|
|
18
|
+
/** 按 (provider, modelId) 查找。 */
|
|
19
|
+
find(provider: string, modelId: string): ModelInfo | undefined;
|
|
20
|
+
/** 校验模型鉴权是否就绪。 */
|
|
21
|
+
hasConfiguredAuth(model: unknown): boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 模型信息(registry 返回元素 / ctx.model 鸭子类型兼容)。
|
|
26
|
+
* ctx.model(SDK Model<Api>)是此类型的超集,运行时直接当 ModelInfo 用。
|
|
27
|
+
*/
|
|
28
|
+
export interface ModelInfo {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
provider: string;
|
|
32
|
+
reasoning: boolean;
|
|
33
|
+
thinkingLevelMap?: Record<string, unknown>;
|
|
34
|
+
contextWindow?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** agent .md frontmatter 解析结果。 */
|
|
38
|
+
export interface AgentConfig {
|
|
39
|
+
/** agent 名(文件名 basename)。 */
|
|
40
|
+
name: string;
|
|
41
|
+
/** system prompt(markdown 正文)。 */
|
|
42
|
+
systemPrompt: string;
|
|
43
|
+
/** tool allowlist(三层过滤之一)。 */
|
|
44
|
+
tools?: string[];
|
|
45
|
+
/** 默认模型 override("provider/modelId")。agent 作者显式指定。 */
|
|
46
|
+
model?: string;
|
|
47
|
+
/** 默认 thinkingLevel override。 */
|
|
48
|
+
thinkingLevel?: string;
|
|
49
|
+
/** 默认 background 模式(true 时无显式 wait 走 background)。 */
|
|
50
|
+
defaultBackground?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 解析结果(model 实例 + 生效的 thinkingLevel)。 */
|
|
54
|
+
export interface ResolvedModel {
|
|
55
|
+
model: ModelInfo;
|
|
56
|
+
thinkingLevel: string | undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ============================================================
|
|
60
|
+
// 常量
|
|
61
|
+
// ============================================================
|
|
62
|
+
|
|
63
|
+
/** thinking level 支持顺序(低→高),用于 clamp 到 model 可用级别。 */
|
|
64
|
+
const THINKING_ORDER = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
65
|
+
|
|
66
|
+
/** 解析失败时错误信息列出的可用模型上限(防超长错误信息)。 */
|
|
67
|
+
const MODEL_LIST_LIMIT = 20;
|
|
68
|
+
|
|
69
|
+
// ============================================================
|
|
70
|
+
// 解析
|
|
71
|
+
// ============================================================
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 三层模型解析:
|
|
75
|
+
*
|
|
76
|
+
* ╔═══════════════════════════════════════════════════════════════╗
|
|
77
|
+
// ║ 优先级(高→低): ║
|
|
78
|
+
// ║ 1. paramOverride.model (调用方显式指定,tool 参数) ║
|
|
79
|
+
// ║ 2. agentConfig.model (agent .md frontmatter) ║
|
|
80
|
+
// ║ 3. ctxModel (主 agent 当前模型,直接透传) ║
|
|
81
|
+
// ║ ║
|
|
82
|
+
// ║ 1/2 级查 registry + auth 校验;3 级无需 lookup(主 agent 在用 ║
|
|
83
|
+
// ║ 说明 auth OK)。thinkingLevel 无指定时 undefined(model 默认) ║
|
|
84
|
+
// ║ ║
|
|
85
|
+
// ║ 显式指定但 lookup/auth 失败 → 抛错(不静默降级到主 agent, ║
|
|
86
|
+
// ║ 因为用户明确要求了某个 model,降级会造成「以为用了 X 实际用 Y」║
|
|
87
|
+
// ╚═══════════════════════════════════════════════════════════════╝
|
|
88
|
+
*
|
|
89
|
+
* @param agentConfig agent .md 解析结果(查 model override + thinkingLevel)
|
|
90
|
+
* @param modelRegistry registry(仅 override 路径用)
|
|
91
|
+
* @param paramOverride 调用方显式 override(最高优先级)
|
|
92
|
+
* @param ctxModel 主 agent 当前模型(兜底,直接透传)
|
|
93
|
+
*/
|
|
94
|
+
export function resolveModel(
|
|
95
|
+
agentConfig: AgentConfig | undefined,
|
|
96
|
+
modelRegistry: ModelRegistryLike,
|
|
97
|
+
paramOverride?: { model?: string; thinkingLevel?: string },
|
|
98
|
+
ctxModel?: ModelInfo,
|
|
99
|
+
): ResolvedModel {
|
|
100
|
+
// 1. paramOverride(最高优先级)。显式指定但 lookup/auth 失败 → 直接抛错,
|
|
101
|
+
// 不降级到下层(避免「以为用了 X 实际用 Y」的静默错误)。
|
|
102
|
+
if (paramOverride?.model) {
|
|
103
|
+
return lookupAndResolve(
|
|
104
|
+
paramOverride.model,
|
|
105
|
+
paramOverride.thinkingLevel,
|
|
106
|
+
modelRegistry,
|
|
107
|
+
"paramOverride",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 2. agentConfig.model(agent 作者指定)。同样显式 → 失败即抛错。
|
|
112
|
+
if (agentConfig?.model) {
|
|
113
|
+
return lookupAndResolve(
|
|
114
|
+
agentConfig.model,
|
|
115
|
+
agentConfig.thinkingLevel,
|
|
116
|
+
modelRegistry,
|
|
117
|
+
"agentConfig",
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 3. 主 agent model(直接透传,thinkingLevel 用 override 或 undefined)
|
|
122
|
+
if (ctxModel) {
|
|
123
|
+
return {
|
|
124
|
+
model: ctxModel,
|
|
125
|
+
thinkingLevel: paramOverride?.thinkingLevel ?? agentConfig?.thinkingLevel,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 全部不可用 → 列出可用模型辅助调试
|
|
130
|
+
const available = modelRegistry.getAvailable().map((m) => `${m.provider}/${m.id}`);
|
|
131
|
+
throw new Error(
|
|
132
|
+
`No available model. Main agent has no active model, and no override was resolved.` +
|
|
133
|
+
(available.length > 0
|
|
134
|
+
? `\nAvailable models:\n ${available.slice(0, MODEL_LIST_LIMIT).join("\n ")}`
|
|
135
|
+
: ""),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* lookup + auth 校验 + thinkingLevel clamp。显式指定但失败 → 抛错(不降级)。
|
|
141
|
+
*
|
|
142
|
+
* 错误信息区分两种失败(避免误导排查方向):
|
|
143
|
+
* - model 不存在 → 提示检查拼写 + 列出相近可用 model
|
|
144
|
+
* - model 存在但 auth 未配置 → 提示在 models.json 配置鉴权
|
|
145
|
+
*/
|
|
146
|
+
function lookupAndResolve(
|
|
147
|
+
modelStr: string,
|
|
148
|
+
requestedThinking: string | undefined,
|
|
149
|
+
registry: ModelRegistryLike,
|
|
150
|
+
source: "paramOverride" | "agentConfig",
|
|
151
|
+
): ResolvedModel {
|
|
152
|
+
const model = lookupModel(modelStr, registry);
|
|
153
|
+
if (!model) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`Model "${modelStr}" (${source}) not found in registry. ` +
|
|
156
|
+
suggestSimilarModels(modelStr, registry),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
if (!registry.hasConfiguredAuth(model)) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`Model "${modelStr}" (${source}) exists but auth is not configured. ` +
|
|
162
|
+
`Configure auth in models.json or switch to an authorized model.`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
model,
|
|
167
|
+
thinkingLevel: resolveThinkingLevel(model, requestedThinking),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 解析 "provider/modelId" 并查 registry。
|
|
173
|
+
*
|
|
174
|
+
* 容错:剥离尾部 ":thinkingLevel" 后缀(off/minimal/low/medium/high/xhigh)。
|
|
175
|
+
* 原因:LLM 常把平台复合标识 "provider/modelId:thinkingLevel"(如
|
|
176
|
+
* "deepseek-router/ds-pro:xhigh")整体当 model 参数传入。registry 仅存
|
|
177
|
+
* "provider/modelId"(无后缀),不剥离则 modelId="ds-pro:xhigh" 查不到。
|
|
178
|
+
* 剥离后 thinkingLevel 仍由独立的 thinkingLevel 参数/resolveThinkingLevel 处理。
|
|
179
|
+
*
|
|
180
|
+
* modelId 可含 /,按第一个 / 分割 provider 与 modelId。
|
|
181
|
+
*/
|
|
182
|
+
function lookupModel(modelStr: string, registry: ModelRegistryLike): ModelInfo | undefined {
|
|
183
|
+
const cleanStr = stripThinkingSuffix(modelStr);
|
|
184
|
+
const idx = cleanStr.indexOf("/");
|
|
185
|
+
if (idx <= 0) return undefined;
|
|
186
|
+
return registry.find(cleanStr.slice(0, idx), cleanStr.slice(idx + 1));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 剥离模型字符串尾部 ":thinkingLevel" 后缀(如 "ds-pro:xhigh" → "ds-pro")。
|
|
191
|
+
* 仅匹配合法 thinking level,避免误剥 "foo:bar" 这类无关冒号。
|
|
192
|
+
* 返回去除后缀的字符串;无后缀则原样返回。
|
|
193
|
+
*/
|
|
194
|
+
function stripThinkingSuffix(modelStr: string): string {
|
|
195
|
+
// THINKING_ORDER 含 off/minimal/low/medium/high/xhigh,按长度降序拼正则避免短串误匹配
|
|
196
|
+
const alt = THINKING_ORDER.slice().sort((a, b) => b.length - a.length).join("|");
|
|
197
|
+
return modelStr.replace(new RegExp(`:(${alt})$`), "");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 为 not-found 错误生成「相近可用 model」建议,辅助定位拼写错误。
|
|
202
|
+
* 策略:取 provider/modelId 的末段,与每个可用 model 的末段做小写包含匹配,
|
|
203
|
+
* 命中则列出。无命中则列出前 N 个全部可用 model(兜底)。空 registry 不列。
|
|
204
|
+
*/
|
|
205
|
+
function suggestSimilarModels(modelStr: string, registry: ModelRegistryLike): string {
|
|
206
|
+
const available = registry.getAvailable();
|
|
207
|
+
if (available.length === 0) return "Registry has no available models.";
|
|
208
|
+
const target = modelStr.split("/").pop()?.toLowerCase() ?? "";
|
|
209
|
+
// ponytail: 末段子串包含足够定位拼写错误,无需编辑距离/相似度库
|
|
210
|
+
const similar = available
|
|
211
|
+
.filter((m) => m.id.toLowerCase().includes(target) || target.includes(m.id.toLowerCase()))
|
|
212
|
+
.map((m) => `${m.provider}/${m.id}`)
|
|
213
|
+
.slice(0, MODEL_LIST_LIMIT);
|
|
214
|
+
const list = similar.length > 0 ? similar : available.slice(0, MODEL_LIST_LIMIT).map((m) => `${m.provider}/${m.id}`);
|
|
215
|
+
return `Check the model string (maybe a typo, or a ":thinkingLevel" suffix that should be passed via the thinkingLevel param instead). Similar available models:\n ${list.join("\n ")}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 从 model.thinkingLevelMap 提取可用级别,clamp 到最高可用。
|
|
220
|
+
* model.reasoning === false → undefined(不支持 thinking)
|
|
221
|
+
*/
|
|
222
|
+
function resolveThinkingLevel(
|
|
223
|
+
model: { reasoning: boolean; thinkingLevelMap?: Record<string, unknown> },
|
|
224
|
+
requested?: string,
|
|
225
|
+
): string | undefined {
|
|
226
|
+
const levels = availableThinkingLevels(model);
|
|
227
|
+
if (levels.length === 0) return model.reasoning ? requested : undefined;
|
|
228
|
+
if (requested && levels.includes(requested)) return requested;
|
|
229
|
+
// requested 不可用 → 降级到最高可用
|
|
230
|
+
return levels[levels.length - 1];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 列出 model 实际支持的 thinking level(升序)。
|
|
235
|
+
*
|
|
236
|
+
* - model.reasoning === false → [] (不支持 thinking)
|
|
237
|
+
* - 无 thinkingLevelMap → [] (无级别信息,调用方按需透传)
|
|
238
|
+
* - 有 map → THINKING_ORDER 中 map[lvl] != null 的子集(保留升序)
|
|
239
|
+
*/
|
|
240
|
+
export function availableThinkingLevels(
|
|
241
|
+
model: { reasoning: boolean; thinkingLevelMap?: Record<string, unknown> },
|
|
242
|
+
): readonly string[] {
|
|
243
|
+
if (!model.reasoning) return [];
|
|
244
|
+
const map = model.thinkingLevelMap;
|
|
245
|
+
if (!map) return [];
|
|
246
|
+
return THINKING_ORDER.filter((lvl) => map[lvl] != null);
|
|
247
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// src/execution/notifier.ts
|
|
2
|
+
//
|
|
3
|
+
// Background 完成回注主对话。sync 不用(调用方还在 await,结果直接返回)。
|
|
4
|
+
//
|
|
5
|
+
// 职责:
|
|
6
|
+
// - 合并窗口:MERGE_WINDOW_MS 内多个完成合并为一条通知
|
|
7
|
+
// - 去重 TTL:同 id 在 TTL 内不重复通知
|
|
8
|
+
// - 通过 pi.sendMessage({ deliverAs:"followUp", triggerTurn:true }) 注入——
|
|
9
|
+
// 当前 turn 结束后唤醒父 agent 处理结果(followUp 不打断 streaming、不锁滚动)
|
|
10
|
+
|
|
11
|
+
/** 一条待发送的完成通知记录。 */
|
|
12
|
+
export interface BgNotifyRecord {
|
|
13
|
+
id: string;
|
|
14
|
+
status: "done" | "failed" | "cancelled";
|
|
15
|
+
agent: string;
|
|
16
|
+
/** 执行所用 model(RecordSnapshot.model),用于完成通知显示。 */
|
|
17
|
+
model?: string;
|
|
18
|
+
result?: string;
|
|
19
|
+
error?: string;
|
|
20
|
+
startedAt: number;
|
|
21
|
+
endedAt: number | undefined;
|
|
22
|
+
/** [MF#1] fork+worktree 模式下子 agent 改动的 patch 路径(worktree 外,cleanup 后留存)。
|
|
23
|
+
* done 时通知文本显式提示 `git apply`,否则 background 子 agent 在隔离 worktree 的改动
|
|
24
|
+
* 会静默丢失——父 LLM 不知 patch 路径,无法应用。 */
|
|
25
|
+
patchFile?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** notifier 依赖的 pi 最小接口(解耦,便于测试)。 */
|
|
29
|
+
export interface NotifierHost {
|
|
30
|
+
/** 注入消息到主对话。
|
|
31
|
+
* triggerTurn:true + deliverAs:"followUp" → 当前 streaming 结束后唤醒父 agent
|
|
32
|
+
* 处理结果(不打断、不锁滚动);空闲时立即 prompt 新 turn。 */
|
|
33
|
+
sendMessage(
|
|
34
|
+
message: { customType: string; content: string; display: boolean; details?: unknown },
|
|
35
|
+
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
|
36
|
+
): void;
|
|
37
|
+
/** 是否还有 running 的 background 任务(用于滑动窗口立即 flush 判断)。 */
|
|
38
|
+
hasRunningBackground(): boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 合并窗口(ms)。窗口内多个完成合并为一条消息。 */
|
|
42
|
+
const MERGE_WINDOW_MS = 60_000;
|
|
43
|
+
/** 去重 TTL(ms)。同 id 在此窗口内不重复通知。 */
|
|
44
|
+
const DEDUP_TTL_MS = 60_000;
|
|
45
|
+
|
|
46
|
+
/** 发送给主对话的 customType(bg-notify-render 消费)。 */
|
|
47
|
+
const NOTIFY_CUSTOM_TYPE = "subagent-bg-notify";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Background 完成通知器(滑动窗口合并)。
|
|
51
|
+
*
|
|
52
|
+
* notify(record):
|
|
53
|
+
* 1. dedup TTL 检查:同 id 在 TTL 内 → 跳过
|
|
54
|
+
* 2. 入 pending 队列
|
|
55
|
+
* 3. 清除旧 timer(滑动窗口重置)
|
|
56
|
+
* 4. 已无 running background → 立即 flush(最后一批)
|
|
57
|
+
* 5. 否则重启 MERGE_WINDOW_MS timer(等后续完成合并)
|
|
58
|
+
*
|
|
59
|
+
* flushPendingNotifications(): timer 到期 / 无 running / shutdown 触发
|
|
60
|
+
* 1. 取出 pending 全部 record
|
|
61
|
+
* 2. 合并为一条消息(多条时列 bullet list)
|
|
62
|
+
* 3. sendMessage({ customType:"subagent-bg-notify",
|
|
63
|
+
* content, display:true,
|
|
64
|
+
* triggerTurn:true, deliverAs:"followUp" })
|
|
65
|
+
* 4. 清空 pending + timer
|
|
66
|
+
*
|
|
67
|
+
* 滑动窗口:每次有新完成都重置 60s 计时器,密集完成的任务尽量合并到一条通知。
|
|
68
|
+
* 无 running 时立即 flush——避免最后一条等满窗口。
|
|
69
|
+
*/
|
|
70
|
+
export class BgNotifier {
|
|
71
|
+
private readonly pending: BgNotifyRecord[] = [];
|
|
72
|
+
/** dedup:id → 上次通知时间戳。 */
|
|
73
|
+
private readonly dedup = new Map<string, number>();
|
|
74
|
+
private timer: ReturnType<typeof setTimeout> | undefined;
|
|
75
|
+
private _disposed = false;
|
|
76
|
+
|
|
77
|
+
constructor(private readonly host: NotifierHost) {}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 入队一条完成通知(去重 + 滑动窗口合并)。dispose 后短路。
|
|
81
|
+
*/
|
|
82
|
+
notify(record: BgNotifyRecord): void {
|
|
83
|
+
if (this._disposed) return;
|
|
84
|
+
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
// sweep 过期 dedup 条目(防 Map 无限增长)
|
|
87
|
+
if (this.dedup.size > 0) {
|
|
88
|
+
for (const [id, ts] of this.dedup) {
|
|
89
|
+
if (now - ts >= DEDUP_TTL_MS) this.dedup.delete(id);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const lastSeen = this.dedup.get(record.id);
|
|
93
|
+
if (lastSeen !== undefined && now - lastSeen < DEDUP_TTL_MS) return;
|
|
94
|
+
this.dedup.set(record.id, now);
|
|
95
|
+
|
|
96
|
+
this.pending.push(record);
|
|
97
|
+
|
|
98
|
+
if (this.timer !== undefined) {
|
|
99
|
+
clearTimeout(this.timer);
|
|
100
|
+
this.timer = undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!this.host.hasRunningBackground()) {
|
|
104
|
+
this.flushPendingNotifications();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.timer = setTimeout(() => this.flushPendingNotifications(), MERGE_WINDOW_MS);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 立即 flush(session_shutdown 调用,防丢失)。 */
|
|
112
|
+
flushPendingNotifications(): void {
|
|
113
|
+
if (this.timer !== undefined) {
|
|
114
|
+
clearTimeout(this.timer);
|
|
115
|
+
this.timer = undefined;
|
|
116
|
+
}
|
|
117
|
+
if (this.pending.length === 0) return;
|
|
118
|
+
|
|
119
|
+
const records = this.pending.splice(0);
|
|
120
|
+
const content = records.length === 1
|
|
121
|
+
? this.buildLlmContent(records[0])
|
|
122
|
+
: records.map((r) => this.buildLlmContent(r)).join("\n\n---\n\n");
|
|
123
|
+
const details = records.length === 1
|
|
124
|
+
? records[0]
|
|
125
|
+
: { batch: true, items: records };
|
|
126
|
+
|
|
127
|
+
this.host.sendMessage({
|
|
128
|
+
customType: NOTIFY_CUSTOM_TYPE,
|
|
129
|
+
content,
|
|
130
|
+
display: true,
|
|
131
|
+
details,
|
|
132
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private buildLlmContent(record: BgNotifyRecord): string {
|
|
136
|
+
const agent = record.agent;
|
|
137
|
+
const id = record.id;
|
|
138
|
+
switch (record.status) {
|
|
139
|
+
case "done": {
|
|
140
|
+
const base = `Subagent "${agent}" (${id}) completed. Result:\n${record.result ?? "(empty)"}`;
|
|
141
|
+
if (record.patchFile) {
|
|
142
|
+
return `${base}\n\nThis subagent ran in an isolated worktree; its file changes were captured as a patch:\n ${record.patchFile}\nTo bring these changes into the current repo, run: \`git apply ${record.patchFile}\``;
|
|
143
|
+
}
|
|
144
|
+
return base;
|
|
145
|
+
}
|
|
146
|
+
case "failed":
|
|
147
|
+
return `Subagent "${agent}" (${id}) failed: ${record.error ?? "(unknown error)"}`;
|
|
148
|
+
case "cancelled":
|
|
149
|
+
return `Subagent "${agent}" (${id}) cancelled.`;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** session 结束:清 timer,丢弃 pending。 */
|
|
154
|
+
dispose(): void {
|
|
155
|
+
this._disposed = true;
|
|
156
|
+
if (this.timer !== undefined) {
|
|
157
|
+
clearTimeout(this.timer);
|
|
158
|
+
this.timer = undefined;
|
|
159
|
+
}
|
|
160
|
+
this.pending.length = 0;
|
|
161
|
+
this.dedup.clear();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** /resume /fork /new 后复活。 */
|
|
165
|
+
revive(): void {
|
|
166
|
+
this._disposed = false;
|
|
167
|
+
}
|
|
168
|
+
}
|