@zhushanwen/pi-subagents 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/agents/context-builder.md +19 -0
  2. package/agents/oracle.md +19 -0
  3. package/agents/planner.md +19 -0
  4. package/agents/researcher.md +19 -0
  5. package/agents/reviewer.md +19 -0
  6. package/agents/scout.md +19 -0
  7. package/agents/worker.md +18 -0
  8. package/index.ts +1 -0
  9. package/package.json +59 -0
  10. package/src/commands/subagents.ts +78 -0
  11. package/src/core/agent-registry.ts +222 -0
  12. package/src/core/concurrency-pool.ts +78 -0
  13. package/src/core/event-bridge.ts +199 -0
  14. package/src/core/execution-record.ts +500 -0
  15. package/src/core/model-resolver.ts +206 -0
  16. package/src/core/output-collector.ts +118 -0
  17. package/src/core/path-encoding.ts +16 -0
  18. package/src/core/session-factory.ts +365 -0
  19. package/src/core/session-runner.ts +303 -0
  20. package/src/core/turn-limiter.ts +71 -0
  21. package/src/index.ts +104 -0
  22. package/src/runtime/config/config.ts +170 -0
  23. package/src/runtime/discovery-config.ts +135 -0
  24. package/src/runtime/execution/history-store.ts +196 -0
  25. package/src/runtime/execution/notifier.ts +209 -0
  26. package/src/runtime/execution/record-store.ts +280 -0
  27. package/src/runtime/model-config-service.ts +265 -0
  28. package/src/runtime/session-file-gc.ts +70 -0
  29. package/src/runtime/subagent-service.ts +549 -0
  30. package/src/tools/subagent-tool.ts +286 -0
  31. package/src/tui/bg-notify-render.ts +139 -0
  32. package/src/tui/config-wizard.ts +253 -0
  33. package/src/tui/format-helpers.ts +37 -0
  34. package/src/tui/format.ts +332 -0
  35. package/src/tui/list-view.ts +883 -0
  36. package/src/tui/tool-render.ts +467 -0
  37. package/src/types.ts +334 -0
@@ -0,0 +1,265 @@
1
+ // src/runtime/model-config-service.ts
2
+ //
3
+ // 配置 + 模型解析领域 Service。"给定 agent 名 + 用户参数,用哪个模型?"
4
+ //
5
+ // 与 SubagentService(执行/记录/通知域)正交——本 Service 不碰 pool/store/notifier。
6
+ // 上游:SubagentService.execute 内部调 resolveModel;command/wizard 直接用(不经 SubagentService)。
7
+ // session_start 时经 initModel 注入 modelRegistry + 恢复 sessionState。
8
+
9
+ import { AgentRegistry, createPackageBuiltinRegistry } from "../core/agent-registry.ts";
10
+ import {
11
+ type AgentConfig,
12
+ inferCategory,
13
+ type ModelRegistryLike,
14
+ type ResolvedModel,
15
+ resolveModelForAgent,
16
+ } from "../core/model-resolver.ts";
17
+ import type {
18
+ SessionModelState,
19
+ SubagentsGlobalConfig,
20
+ } from "../types.ts";
21
+ import {
22
+ createSessionState,
23
+ loadGlobalConfig,
24
+ restoreSessionState,
25
+ saveGlobalConfig as saveConfig,
26
+ } from "./config/config.ts";
27
+ import { DiscoveryConfigLoader } from "./discovery-config.ts";
28
+
29
+ // ============================================================
30
+ // 类型
31
+ // ============================================================
32
+
33
+ /** Service 构造参数(进程级,跨 session 不变)。 */
34
+ export interface ModelConfigServiceInit {
35
+ agentDir: string;
36
+ /**
37
+ * 资源发现契约加载器(宿主声明的多 skill/agent 目录)。
38
+ * undefined 时仅用 agentDir 单目录(默认行为,零破坏)。
39
+ * 详见 ADR-025。
40
+ */
41
+ discoveryLoader?: DiscoveryConfigLoader;
42
+ }
43
+
44
+ /** session_start 注入参数(session 级,每次重建)。 */
45
+ export interface ModelServiceSessionInit {
46
+ /** 模型注册表(鉴权 + 发现)。null 立即抛错(fail-fast)。 */
47
+ modelRegistry: ModelRegistryLike | null;
48
+ /** 当前 session ID。 */
49
+ sessionId: string;
50
+ /** session 历史条目(/resume /fork 时恢复 sessionState)。 */
51
+ entries: ReadonlyArray<{ type: string; data?: unknown }>;
52
+ }
53
+
54
+ // ============================================================
55
+ // ModelConfigService
56
+ // ============================================================
57
+
58
+ /**
59
+ * 配置 + 模型解析 Service。进程级单例。
60
+ *
61
+ * ┌──────────────────────────────────────────────────────┐
62
+ * │ globalConfig(~/.pi/.../config.json) │
63
+ * │ sessionState(内存,经 entries 持久化/恢复) │
64
+ * │ agentRegistry(agent .md 发现 + frontmatter) │
65
+ * │ modelRegistry(SDK 注入的可用模型) │
66
+ * │ │
67
+ * │ resolveModel: agent → category → 5级fallback → 确认 │
68
+ * └──────────────────────────────────────────────────────┘
69
+ */
70
+ export class ModelConfigService {
71
+ private globalConfig: SubagentsGlobalConfig;
72
+ private sessionState: SessionModelState;
73
+ private readonly agentRegistry: AgentRegistry;
74
+ private readonly agentRegistryDir: string;
75
+ /** discovery 加载器(resources_discover 时重新读,喂主 agent skill)。 */
76
+ private readonly discoveryLoader: DiscoveryConfigLoader | undefined;
77
+ private modelRegistry: ModelRegistryLike | null = null;
78
+ private _sessionId: string | undefined;
79
+
80
+ /** 包内 builtin agent(agents/*.md,优先级最低,被用户覆盖)。 */
81
+ private readonly builtinRegistry = createPackageBuiltinRegistry();
82
+
83
+ constructor(init: ModelConfigServiceInit) {
84
+ this.agentRegistryDir = init.agentDir;
85
+ this.discoveryLoader = init.discoveryLoader;
86
+ this.globalConfig = loadGlobalConfig(init.agentDir);
87
+ this.sessionState = createSessionState();
88
+ // agentDirs:discovery 声明的目录(靠前覆盖靠后),空则回退默认 agentDir 单目录
89
+ const agentDirs = this.resolveAgentDirs();
90
+ this.agentRegistry = new AgentRegistry(agentDirs);
91
+ // 接通发现机制:扫描 agentDirs + 合并 builtin(此前从未调用,registry 永远为空)
92
+ this.agentRegistry.discoverAll(this.builtinRegistry);
93
+ }
94
+
95
+ /**
96
+ * 解析 agent 发现目录列表。
97
+ * discovery.json 的 agentDirs 非空时用之(靠前覆盖靠后),否则回退 [agentDir] 默认。
98
+ */
99
+ private resolveAgentDirs(): string[] {
100
+ const discovery = this.discoveryLoader?.load();
101
+ if (discovery && discovery.agentDirs.length > 0) {
102
+ return [...discovery.agentDirs];
103
+ }
104
+ return [this.agentRegistryDir];
105
+ }
106
+
107
+ // ── 生命周期(index.ts 调)──────────────────────────────
108
+
109
+ /**
110
+ * session_start 注入。封装 4 步固定时序:
111
+ * 1. reloadGlobalConfig(复用时拿最新 config)
112
+ * 2. injectModelRegistry(fail-fast:null 抛错)
113
+ * 3. setSessionId
114
+ * 4. restoreFromEntries(恢复 sessionState)
115
+ */
116
+ initModel(init: ModelServiceSessionInit): void {
117
+ // 1. 重载配置 + 重扫 agent(hot-reload:用户可能新增/修改 agent .md)
118
+ this.globalConfig = loadGlobalConfig(this.agentRegistryDir);
119
+ this.agentRegistry.discoverAll(this.builtinRegistry);
120
+
121
+ // 2. modelRegistry(fail-fast)
122
+ if (init.modelRegistry === null) {
123
+ throw new Error("modelRegistry is required but got null");
124
+ }
125
+ this.modelRegistry = init.modelRegistry;
126
+
127
+ // 3. sessionId
128
+ this._sessionId = init.sessionId;
129
+
130
+ // 4. 恢复 sessionState
131
+ Object.assign(this.sessionState, restoreSessionState(init.entries));
132
+ }
133
+
134
+ // ── 模型解析(SubagentService.execute 内部调)──────────────
135
+
136
+ /**
137
+ * 解析 agent 的模型(纯解析)。
138
+ *
139
+ * D-1:取消首次确认拦截——categoryConfirmed 默认 true,本方法直接解析不再阻塞。
140
+ * 用户改 category 模型走 /subagents config(写 globalConfig)。
141
+ */
142
+ resolveModel(
143
+ agentName: string,
144
+ override?: { model?: string; thinkingLevel?: string },
145
+ ): ResolvedModel {
146
+ this.assertReady();
147
+ const agentConfig = this.agentRegistry.get(agentName);
148
+ const category = inferCategory(
149
+ agentName,
150
+ agentConfig,
151
+ this.globalConfig.agentCategoryOverrides,
152
+ "general",
153
+ );
154
+ return this.doResolve(agentName, agentConfig, category, override);
155
+ }
156
+
157
+ /** 查询 agent 配置(SubagentService 内部判定 defaultBackground + resolveIdentity 用)。 */
158
+ getAgentConfig(name?: string): AgentConfig | undefined {
159
+ return name ? this.agentRegistry.get(name) : undefined;
160
+ }
161
+
162
+ // ── 配置读写(command/wizard 调)────────────────────────
163
+
164
+ /** 全局配置深拷贝(调用方拿到副本,改不影响 Service 内部)。 */
165
+ getGlobalConfig(): SubagentsGlobalConfig {
166
+ return structuredClone(this.globalConfig);
167
+ }
168
+
169
+ /** session 状态深拷贝。 */
170
+ getSessionState(): SessionModelState {
171
+ return structuredClone(this.sessionState);
172
+ }
173
+
174
+ /** 更新全局配置 + 落盘(config-wizard 改完调)。 */
175
+ async saveGlobalConfig(config: SubagentsGlobalConfig): Promise<void> {
176
+ this.globalConfig = config;
177
+ await saveConfig(this.agentRegistryDir, config);
178
+ }
179
+
180
+ /** 翻转 YOLO 模式。返回翻转后的新值。 */
181
+ toggleYolo(): boolean {
182
+ this.sessionState.yoloMode = !this.sessionState.yoloMode;
183
+ return this.sessionState.yoloMode;
184
+ }
185
+
186
+ /** 内部:用于 SubagentService 经 session id 过滤 history(只读访问 sessionId)。 */
187
+ get sessionId(): string | undefined {
188
+ return this._sessionId;
189
+ }
190
+
191
+ /** agent 配置目录(SubagentService 构造 history/store/SessionRunnerContext 时读)。 */
192
+ getAgentDir(): string {
193
+ return this.agentRegistryDir;
194
+ }
195
+
196
+ /**
197
+ * discovery.json 声明的 skill 目录(供 SubagentService 注入子 session)。
198
+ * 每次调用重新读 loader(mtime 缓存),支持宿主运行时修改 discovery.json 后下次生效。
199
+ */
200
+ getDiscoverySkillDirs(): string[] {
201
+ const discovery = this.discoveryLoader?.load();
202
+ return discovery ? [...discovery.skillDirs] : [];
203
+ }
204
+
205
+ /** modelRegistry(SubagentService 构造 factoryCtx 时读)。已注入保证非 null。 */
206
+ getModelRegistry(): ModelRegistryLike {
207
+ if (this.modelRegistry === null) {
208
+ throw new Error("modelRegistry not injected (initModel not called?)");
209
+ }
210
+ return this.modelRegistry;
211
+ }
212
+
213
+ // ── 内部 ────────────────────────────────────────────────
214
+
215
+ /** 实际的 5 级 fallback 解析(不含确认逻辑)。 */
216
+ private doResolve(
217
+ agentName: string,
218
+ agentConfig: AgentConfig | undefined,
219
+ category: string,
220
+ override?: { model?: string; thinkingLevel?: string },
221
+ ): ResolvedModel {
222
+ return resolveModelForAgent({
223
+ agentName,
224
+ agentConfig,
225
+ category,
226
+ globalConfig: this.globalConfig,
227
+ sessionState: this.sessionState,
228
+ modelRegistry: this.modelRegistry!,
229
+ paramOverride: override,
230
+ });
231
+ }
232
+
233
+ /** 校验 modelRegistry 已注入。 */
234
+ private assertReady(): void {
235
+ if (this.modelRegistry === null) {
236
+ throw new Error("modelRegistry not injected (initModel not called?)");
237
+ }
238
+ }
239
+ }
240
+
241
+ // ============================================================
242
+ // 进程单例访问器
243
+ // ============================================================
244
+
245
+ // 用 globalThis[Symbol.for] 持有进程单例,避免 jiti 因路径字符串不同加载多份模块
246
+ // 导致单例分裂(详见 docs/pi-extension-standards.md §7.5)。
247
+ const MODEL_SERVICE_SLOT_KEY = Symbol.for("@zhushanwen/pi-subagents.model-service");
248
+
249
+ type ModelServiceSlot = { current: ModelConfigService | null };
250
+
251
+ function getModelServiceSlot(): ModelServiceSlot {
252
+ const record = globalThis as unknown as Record<symbol, unknown>;
253
+ if (!record[MODEL_SERVICE_SLOT_KEY]) record[MODEL_SERVICE_SLOT_KEY] = { current: null };
254
+ return record[MODEL_SERVICE_SLOT_KEY] as ModelServiceSlot;
255
+ }
256
+
257
+ /** 获取进程单例。session_start 前为 null。 */
258
+ export function getModelConfigService(): ModelConfigService | null {
259
+ return getModelServiceSlot().current;
260
+ }
261
+
262
+ /** 设置进程单例(session_start 首次创建时)。 */
263
+ export function setModelConfigService(service: ModelConfigService): void {
264
+ getModelServiceSlot().current = service;
265
+ }
@@ -0,0 +1,70 @@
1
+ // src/runtime/session-file-gc.ts
2
+ //
3
+ // 概率性清理过期 subagent session 文件(TTL 30 天)。
4
+ // session_start 时调用,best-effort(失败不影响启动)。
5
+
6
+ import * as fs from "node:fs";
7
+ import * as path from "node:path";
8
+
9
+ /** 30 天 TTL(毫秒)。 */
10
+ const TTL_DAYS = 30;
11
+ const HOURS_PER_DAY = 24;
12
+ const MINUTES_PER_HOUR = 60;
13
+ const SECONDS_PER_MINUTE = 60;
14
+ const MS_PER_SECOND = 1000;
15
+ const TTL_MS = TTL_DAYS * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MS_PER_SECOND;
16
+
17
+ /** 触发概率(1/20 = 5%,避免每次 session_start 都全扫)。 */
18
+ const CLEANUP_PROBABILITY_DIVISOR = 20;
19
+ const CLEANUP_PROBABILITY = 1 / CLEANUP_PROBABILITY_DIVISOR;
20
+
21
+ /** subagent session 文件目录(相对 agentDir)。 */
22
+ const SUBAGENTS_DIR = "subagents";
23
+
24
+ /**
25
+ * 概率性清理过期 session 文件。best-effort——任何异常不外抛。
26
+ *
27
+ * 1. 概率性触发(CLEANUP_PROBABILITY)
28
+ * 2. 递归扫描 <agentDir>/subagents 下所有 .jsonl 文件
29
+ * 3. mtime 超 TTL → unlink
30
+ */
31
+ export function maybeCleanupExpiredSessionFiles(agentDir: string, cwd: string): void {
32
+ void cwd;
33
+ try {
34
+ if (Math.random() >= CLEANUP_PROBABILITY) return;
35
+ const dir = path.join(agentDir, SUBAGENTS_DIR);
36
+ if (!fs.existsSync(dir)) return;
37
+ const now = Date.now();
38
+ walkAndClean(dir, now);
39
+ } catch (_e) {
40
+ // best-effort:任何异常不阻断 session_start
41
+ void _e;
42
+ }
43
+ }
44
+
45
+ /** 递归扫描目录,unlink 超 TTL 的 .jsonl 文件。 */
46
+ function walkAndClean(dir: string, now: number): void {
47
+ let entries: fs.Dirent[];
48
+ try {
49
+ entries = fs.readdirSync(dir, { withFileTypes: true });
50
+ } catch (_e) {
51
+ void _e;
52
+ return;
53
+ }
54
+ for (const entry of entries) {
55
+ const full = path.join(dir, entry.name);
56
+ if (entry.isDirectory()) {
57
+ walkAndClean(full, now);
58
+ } else if (entry.name.endsWith(".jsonl")) {
59
+ try {
60
+ const stat = fs.statSync(full);
61
+ if (now - stat.mtimeMs > TTL_MS) {
62
+ fs.unlinkSync(full);
63
+ }
64
+ } catch (_e) {
65
+ // 文件可能已被删除,忽略
66
+ void _e;
67
+ }
68
+ }
69
+ }
70
+ }