@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.
Files changed (143) hide show
  1. package/agents/context-builder.md +17 -0
  2. package/agents/general-purpose.md +16 -0
  3. package/agents/oracle.md +17 -0
  4. package/agents/planner.md +17 -0
  5. package/agents/researcher.md +17 -0
  6. package/agents/reviewer.md +17 -0
  7. package/agents/scout.md +17 -0
  8. package/agents/worker.md +16 -0
  9. package/examples/README.md +43 -0
  10. package/examples/chain.example.js +92 -0
  11. package/examples/map-reduce.example.js +99 -0
  12. package/examples/parallel.example.js +82 -0
  13. package/examples/scatter-gather.example.js +106 -0
  14. package/index.ts +1 -0
  15. package/package.json +66 -0
  16. package/skills/workflow-script-format/SKILL.md +328 -0
  17. package/src/execution/__tests__/agent-registry.test.ts +164 -0
  18. package/src/execution/__tests__/agent-result-mapper.test.ts +128 -0
  19. package/src/execution/__tests__/alive-store.test.ts +147 -0
  20. package/src/execution/__tests__/bg-notify-render.test.ts +256 -0
  21. package/src/execution/__tests__/concurrency-pool.test.ts +217 -0
  22. package/src/execution/__tests__/config.test.ts +110 -0
  23. package/src/execution/__tests__/crash-recovery.test.ts +311 -0
  24. package/src/execution/__tests__/execute-nesting.test.ts +359 -0
  25. package/src/execution/__tests__/execute-options-mapper.test.ts +138 -0
  26. package/src/execution/__tests__/execution-record.test.ts +959 -0
  27. package/src/execution/__tests__/finalized-marker.test.ts +82 -0
  28. package/src/execution/__tests__/format-schema-instruction.test.ts +135 -0
  29. package/src/execution/__tests__/format.test.ts +320 -0
  30. package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
  31. package/src/execution/__tests__/list-component.test.ts +347 -0
  32. package/src/execution/__tests__/model-resolver.test.ts +356 -0
  33. package/src/execution/__tests__/output-collector.test.ts +61 -0
  34. package/src/execution/__tests__/path-encoding.test.ts +75 -0
  35. package/src/execution/__tests__/pi-invocation.test.ts +73 -0
  36. package/src/execution/__tests__/record-store.test.ts +545 -0
  37. package/src/execution/__tests__/run-spawn-edges.test.ts +439 -0
  38. package/src/execution/__tests__/run-spawn-integration.test.ts +897 -0
  39. package/src/execution/__tests__/sdk-contract.test.ts +272 -0
  40. package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
  41. package/src/execution/__tests__/session-file-gc.test.ts +247 -0
  42. package/src/execution/__tests__/session-reconstructor.test.ts +359 -0
  43. package/src/execution/__tests__/session-runner-schema-env.test.ts +314 -0
  44. package/src/execution/__tests__/session-start-reaper.test.ts +227 -0
  45. package/src/execution/__tests__/spawn-args.test.ts +244 -0
  46. package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
  47. package/src/execution/__tests__/subagent-service.test.ts +678 -0
  48. package/src/execution/__tests__/subprocess-agent-runner.test.ts +389 -0
  49. package/src/execution/__tests__/temp-prompt.test.ts +53 -0
  50. package/src/execution/__tests__/timeout-integration.test.ts +381 -0
  51. package/src/execution/__tests__/tombstone-store.test.ts +73 -0
  52. package/src/execution/__tests__/tool-action.test.ts +330 -0
  53. package/src/execution/__tests__/turn-limiter.test.ts +65 -0
  54. package/src/execution/__tests__/worktree-manager.test.ts +423 -0
  55. package/src/execution/__tests__/worktree-registry.test.ts +161 -0
  56. package/src/execution/agent-registry.ts +252 -0
  57. package/src/execution/agent-result-mapper.ts +84 -0
  58. package/src/execution/alive-store.ts +92 -0
  59. package/src/execution/best-effort.ts +30 -0
  60. package/src/execution/concurrency-pool.ts +84 -0
  61. package/src/execution/config.ts +73 -0
  62. package/src/execution/execute-options-mapper.ts +86 -0
  63. package/src/execution/execution-record.ts +778 -0
  64. package/src/execution/finalized-marker.ts +51 -0
  65. package/src/execution/model-config-service.ts +225 -0
  66. package/src/execution/model-resolver.ts +247 -0
  67. package/src/execution/notifier.ts +168 -0
  68. package/src/execution/output-collector.ts +88 -0
  69. package/src/execution/path-encoding.ts +34 -0
  70. package/src/execution/pi-invocation.ts +70 -0
  71. package/src/execution/record-store.ts +350 -0
  72. package/src/execution/session-context-resolver.ts +64 -0
  73. package/src/execution/session-file-gc.ts +98 -0
  74. package/src/execution/session-reconstructor.ts +450 -0
  75. package/src/execution/session-runner.ts +725 -0
  76. package/src/execution/spawn-event-adapter.ts +150 -0
  77. package/src/execution/subagent-service.ts +973 -0
  78. package/src/execution/subprocess-agent-runner.ts +108 -0
  79. package/src/execution/temp-prompt.ts +57 -0
  80. package/src/execution/tombstone-store.ts +72 -0
  81. package/src/execution/turn-limiter.ts +88 -0
  82. package/src/execution/types.ts +634 -0
  83. package/src/execution/worktree-manager.ts +285 -0
  84. package/src/execution/worktree-registry.ts +144 -0
  85. package/src/index.ts +454 -0
  86. package/src/interface/bg-notify-render.ts +286 -0
  87. package/src/interface/commands.ts +157 -0
  88. package/src/interface/format.ts +501 -0
  89. package/src/interface/gui-adapter.ts +136 -0
  90. package/src/interface/helpers.ts +110 -0
  91. package/src/interface/list-component.ts +643 -0
  92. package/src/interface/list-shared.ts +84 -0
  93. package/src/interface/list-view.ts +373 -0
  94. package/src/interface/reentry-guard.ts +30 -0
  95. package/src/interface/subagent-actions.ts +294 -0
  96. package/src/interface/subagent-tool.ts +294 -0
  97. package/src/interface/subagents.ts +30 -0
  98. package/src/interface/tool-render.ts +333 -0
  99. package/src/interface/tool-workflow-script.ts +351 -0
  100. package/src/interface/tool-workflow.ts +485 -0
  101. package/src/interface/views/WorkflowsView.ts +944 -0
  102. package/src/interface/views/detail-content.ts +298 -0
  103. package/src/interface/views/format.ts +320 -0
  104. package/src/orchestration/__tests__/concurrency-gate.test.ts +125 -0
  105. package/src/orchestration/__tests__/config-loader.test.ts +381 -0
  106. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +332 -0
  107. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
  108. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +248 -0
  109. package/src/orchestration/__tests__/lifecycle.test.ts +385 -0
  110. package/src/orchestration/__tests__/script-lint.test.ts +347 -0
  111. package/src/orchestration/__tests__/worker-script-builder.test.ts +42 -0
  112. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +319 -0
  113. package/src/orchestration/agent-opts-resolver.ts +128 -0
  114. package/src/orchestration/concurrency-gate.ts +69 -0
  115. package/src/orchestration/config-loader.ts +313 -0
  116. package/src/orchestration/error-recovery.ts +578 -0
  117. package/src/orchestration/execute-agent-call.ts +174 -0
  118. package/src/orchestration/jsonl-run-store.ts +292 -0
  119. package/src/orchestration/launcher.ts +368 -0
  120. package/src/orchestration/lifecycle.ts +373 -0
  121. package/src/orchestration/models/__tests__/budget.test.ts +367 -0
  122. package/src/orchestration/models/agent-call.ts +76 -0
  123. package/src/orchestration/models/budget.ts +148 -0
  124. package/src/orchestration/models/ports.ts +165 -0
  125. package/src/orchestration/models/run-runtime.ts +91 -0
  126. package/src/orchestration/models/run-spec.ts +54 -0
  127. package/src/orchestration/models/run-state.ts +44 -0
  128. package/src/orchestration/models/trace.ts +102 -0
  129. package/src/orchestration/models/types.ts +242 -0
  130. package/src/orchestration/models/workflow-run.ts +275 -0
  131. package/src/orchestration/models/workflow-script-registry.ts +32 -0
  132. package/src/orchestration/models/workflow-script.ts +90 -0
  133. package/src/orchestration/node-ops.ts +192 -0
  134. package/src/orchestration/script-lint.ts +387 -0
  135. package/src/orchestration/skill-discovery.ts +60 -0
  136. package/src/orchestration/worker-handle.ts +115 -0
  137. package/src/orchestration/worker-host.ts +93 -0
  138. package/src/orchestration/worker-script-builder.ts +281 -0
  139. package/src/orchestration/workflow-files.ts +85 -0
  140. package/src/orchestration/workflow-script-registry-impl.ts +128 -0
  141. package/src/shared/__tests__/resource-discovery.test.ts +226 -0
  142. package/src/shared/agent-event.ts +13 -0
  143. package/src/shared/resource-discovery.ts +535 -0
@@ -0,0 +1,252 @@
1
+ // src/execution/agent-registry.ts
2
+ //
3
+ // agent .md 文件发现与解析。
4
+ //
5
+ // 发现逻辑统一走 shared/resource-discovery(ADR-031),与 workflow 共享同一套
6
+ // 扫描源前缀 + manifest 校验。hot-reload:每次调用重扫(mtime 缓存跳过未变文件)。
7
+ //
8
+ // builtin agent(包内 agents/*.md)走 pi.agents manifest(与 npm 包内发现规则一致)。
9
+
10
+ import * as fs from "node:fs";
11
+ import * as path from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ import {
15
+ type DiscoveredResource,
16
+ discoverResourcesSync,
17
+ type ScanConfig,
18
+ } from "../shared/resource-discovery.ts";
19
+ import type { AgentConfig } from "./model-resolver.ts";
20
+
21
+ /** 内置 agent(代码硬编码,如 default worker)。 */
22
+ export interface BuiltinAgentRegistry {
23
+ get(name: string): AgentConfig | undefined;
24
+ list(): string[];
25
+ }
26
+
27
+ /**
28
+ * 包内自带 agents(与 src/ 同级的 agents/ 目录)。
29
+ *
30
+ * 走 pi.agents manifest(package.json 的 pi.agents 字段),与 npm 包内发现规则一致。
31
+ * manifest 缺失时 fallback 扫约定目录 agents/。
32
+ *
33
+ * [HISTORICAL] 此前 discoverAll 从未被调用,agentRegistry 永远为空——包内
34
+ * agents/*.md(worker/reviewer/scout 等)pi install 后开箱不可用。修复:构造时扫描
35
+ * 包内 agents/ 作为 builtin(优先级最低,被用户同名文件覆盖)。
36
+ */
37
+ export function createPackageBuiltinRegistry(): BuiltinAgentRegistry {
38
+ const packageRoot = path.resolve(
39
+ path.dirname(fileURLToPath(import.meta.url)),
40
+ "..",
41
+ "..",
42
+ );
43
+ const cache = new Map<string, AgentConfig>();
44
+ try {
45
+ const config = discoverPackageAgentsSync(packageRoot);
46
+ for (const resource of config) {
47
+ if (!resource.available) continue;
48
+ try {
49
+ const raw = fs.readFileSync(resource.path, "utf-8");
50
+ const agentConfig = parseAgentFrontmatter(resource.path, raw);
51
+ if (agentConfig) cache.set(agentConfig.name, agentConfig);
52
+ } catch (err) {
53
+ // 单个 builtin agent 文件损坏不影响其他——降级跳过该文件。
54
+ void err;
55
+ console.warn(`[subagents] skip malformed builtin agent: ${resource.path}`, err);
56
+ }
57
+ }
58
+ } catch (err) {
59
+ // agents/ 目录不存在(打包遗漏)→ 空 builtin,不崩。
60
+ void err;
61
+ console.warn("[subagents] builtin agents/ directory unreadable, falling back to empty set:", err);
62
+ }
63
+ return {
64
+ get: (name) => cache.get(name),
65
+ list: () => [...cache.keys()],
66
+ };
67
+ }
68
+
69
+ /** mtime 缓存条目(跨 discoverAll 保留,靠 mtime 判失效)。 */
70
+ interface FileCacheEntry {
71
+ mtimeMs: number;
72
+ config: AgentConfig;
73
+ }
74
+
75
+ // ============================================================
76
+ // frontmatter 解析
77
+ // ============================================================
78
+
79
+ /** frontmatter 分隔符。 */
80
+ const FM_DELIM = "---";
81
+
82
+ /**
83
+ * 解析 .md frontmatter(name/tools/model/thinkingLevel/defaultBackground)+ body(systemPrompt)。
84
+ * 兼容简单 YAML(key: value 单行格式)。body 作为 systemPrompt。
85
+ */
86
+ export function parseAgentFrontmatter(filePath: string, content: string): AgentConfig {
87
+ const name = path.basename(filePath, ".md");
88
+
89
+ // 无 frontmatter → 整个内容作为 systemPrompt
90
+ if (!content.startsWith(FM_DELIM)) {
91
+ return { name, systemPrompt: content.trim() };
92
+ }
93
+
94
+ const closeIdx = content.indexOf(FM_DELIM, FM_DELIM.length);
95
+ if (closeIdx === -1) {
96
+ // 未闭合 frontmatter:提取 name,其余作为 systemPrompt
97
+ const yamlBlock = content.slice(FM_DELIM.length);
98
+ return {
99
+ name: extractYamlField(yamlBlock, "name") ?? name,
100
+ systemPrompt: content.trim(),
101
+ };
102
+ }
103
+
104
+ const yamlBlock = content.slice(FM_DELIM.length, closeIdx);
105
+ const body = content.slice(closeIdx + FM_DELIM.length).trim();
106
+
107
+ const toolsRaw = extractYamlField(yamlBlock, "tools");
108
+ const tools = toolsRaw
109
+ ? toolsRaw.split(",").map((s) => s.trim()).filter(Boolean)
110
+ : undefined;
111
+
112
+ const defaultBackgroundRaw = extractYamlField(yamlBlock, "defaultBackground");
113
+
114
+ return {
115
+ name: extractYamlField(yamlBlock, "name") ?? name,
116
+ systemPrompt: body,
117
+ model: extractYamlField(yamlBlock, "model") ?? undefined,
118
+ thinkingLevel: extractYamlField(yamlBlock, "thinkingLevel") ?? undefined,
119
+ tools: tools && tools.length > 0 ? tools : undefined,
120
+ defaultBackground: defaultBackgroundRaw === "true" ? true : undefined,
121
+ };
122
+ }
123
+
124
+ /** 提取简单 `key: value` 字段,剥离引号。 */
125
+ function extractYamlField(yaml: string, key: string): string | undefined {
126
+ const regex = new RegExp(`^${key}:\\s*(.+)$`, "m");
127
+ const match = yaml.match(regex);
128
+ if (!match) return undefined;
129
+ let value = match[1].trim();
130
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
131
+ value = value.slice(1, -1);
132
+ }
133
+ return value || undefined;
134
+ }
135
+
136
+ // ============================================================
137
+ // AgentRegistry
138
+ // ============================================================
139
+
140
+ /**
141
+ * 发现配置:用于统一资源发现的扫描参数。
142
+ */
143
+ export interface AgentDiscoveryConfig {
144
+ /** 项目根目录(findWorkspaceRoot 推导结果) */
145
+ workspaceRoot: string;
146
+ /** agent 配置目录(getAgentDir() 结果) */
147
+ agentDir: string;
148
+ }
149
+
150
+ /**
151
+ * agent 注册表。通过统一资源发现(shared/resource-discovery)扫描所有源。
152
+ * hot-reload:每次 discoverAll 重扫,mtime 未变的文件跳过 read+parse。
153
+ *
154
+ * 优先级(低→高):user .pi/agent → user .agents → npm global → npm dev →
155
+ * project .pi → project .agents。builtin(包内)优先级最低。
156
+ * 详见 ADR-031。
157
+ */
158
+ export class AgentRegistry {
159
+ private readonly cache = new Map<string, AgentConfig>();
160
+ /** 文件级 mtime 缓存(key=绝对路径,跨 discoverAll 保留)。 */
161
+ private readonly fileCache = new Map<string, FileCacheEntry>();
162
+ /** 本轮扫描到的路径集(清理已删除文件的缓存)。 */
163
+ private currentScanPaths = new Set<string>();
164
+
165
+ constructor(private readonly discoveryConfig: AgentDiscoveryConfig) {}
166
+
167
+ /** 扫描所有源 + 合并 builtin(hot-reload,每次重扫)。 */
168
+ discoverAll(builtin: BuiltinAgentRegistry): void {
169
+ this.cache.clear();
170
+ this.currentScanPaths = new Set();
171
+
172
+ const scanConfig: ScanConfig = {
173
+ kind: "agents",
174
+ workspaceRoot: this.discoveryConfig.workspaceRoot,
175
+ agentDir: this.discoveryConfig.agentDir,
176
+ };
177
+ const resources = discoverResourcesSync(scanConfig);
178
+
179
+ for (const resource of resources) {
180
+ if (!resource.available) continue;
181
+ this.currentScanPaths.add(resource.path);
182
+ try {
183
+ const config = this.loadWithMtimeCache(resource.path);
184
+ if (config) this.cache.set(config.name, config);
185
+ } catch (_err) {
186
+ // 有意吞掉:文件不可读/解析失败 → 跳过(不阻断其他 agent 发现)
187
+ void _err;
188
+ }
189
+ }
190
+
191
+ // builtin 优先级最低(先写入,被文件 agent 覆盖)
192
+ for (const agentName of builtin.list()) {
193
+ if (!this.cache.has(agentName)) {
194
+ const config = builtin.get(agentName);
195
+ if (config) this.cache.set(agentName, config);
196
+ }
197
+ }
198
+
199
+ // 清理本轮未扫描到的文件缓存条目(文件被删除/移走)
200
+ for (const cachedPath of this.fileCache.keys()) {
201
+ if (!this.currentScanPaths.has(cachedPath)) {
202
+ this.fileCache.delete(cachedPath);
203
+ }
204
+ }
205
+ }
206
+
207
+ /** 按 name 查找。require=false 时找不到返回 undefined;true 时抛错。 */
208
+ get(name: string, require?: boolean): AgentConfig | undefined {
209
+ const config = this.cache.get(name);
210
+ if (!config && require) {
211
+ throw new Error(
212
+ `Agent "${name}" not found. Discovered: ${[...this.cache.keys()].join(", ") || "(none)"}`,
213
+ );
214
+ }
215
+ return config;
216
+ }
217
+
218
+ /** 列出所有已发现 agent 名(诊断/wizard 用)。 */
219
+ list(): string[] {
220
+ return [...this.cache.keys()];
221
+ }
222
+
223
+ // ── 内部 ──────────────────────────────────────────────────
224
+
225
+ /** 带 mtime 缓存的单文件加载。mtime 未变复用缓存,否则 read+parse。 */
226
+ private loadWithMtimeCache(filePath: string): AgentConfig | undefined {
227
+ const stat = fs.statSync(filePath);
228
+ const mtimeMs = stat.mtimeMs;
229
+ const cached = this.fileCache.get(filePath);
230
+ if (cached && cached.mtimeMs === mtimeMs) {
231
+ return cached.config;
232
+ }
233
+ const content = fs.readFileSync(filePath, "utf-8");
234
+ const config = parseAgentFrontmatter(filePath, content);
235
+ this.fileCache.set(filePath, { mtimeMs, config });
236
+ return config;
237
+ }
238
+ }
239
+
240
+ // ============================================================
241
+ // 包内 agent 发现(builtin 用,走 pi.agents manifest)
242
+ // ============================================================
243
+
244
+ import { processPackageSync } from "../shared/resource-discovery.ts";
245
+
246
+ /**
247
+ * 发现包内 agent 文件(走 pi.agents manifest 或约定目录 agents/)。
248
+ * builtin 专用——不参与优先级合并,优先级最低。
249
+ */
250
+ function discoverPackageAgentsSync(packageRoot: string): DiscoveredResource[] {
251
+ return processPackageSync(packageRoot, "agents");
252
+ }
@@ -0,0 +1,84 @@
1
+ // src/execution/agent-result-mapper.ts
2
+ //
3
+ // D-A10: subagents AgentResult → workflow AgentResult 映射。
4
+ // 纯 DTO 映射函数——executeAndAwait 出口调,SAR 不感知形状差异。
5
+ //
6
+ // 接线层级:[模块内直调] —— SubagentService.executeAndAwait 出口调。
7
+
8
+ import type { AgentResult as WorkflowAgentResult, AgentUsage as WorkflowAgentUsage, ToolCallEntry } from "../orchestration/models/types.ts";
9
+ import type { AgentResult as SubagentsAgentResult, AgentUsageTotal, ToolCall } from "./types.ts";
10
+
11
+ /**
12
+ * D-A10: subagents AgentResult → workflow AgentResult 映射。
13
+ *
14
+ * 字段映射表:
15
+ * subagents → workflow
16
+ * ─────────────────────────────────────────────
17
+ * text → content
18
+ * parsedOutput → parsedOutput(structured-output 契约,BC-8)
19
+ * !success && error → error(失败时填,成功时 undefined)
20
+ * durationMs → durationMs
21
+ * sessionId → sessionId
22
+ * usage (AgentUsageTotal) → usage (AgentUsage: input/output/cacheRead/cacheWrite/cost/contextTokens/turns)
23
+ * toolCalls (ToolCall[]) → toolCalls (ToolCallEntry[]: name/input)
24
+ *
25
+ * @param r subagents 管道产出的 AgentResult(runSpawn/collectResult 出口形状)
26
+ * @returns workflow 编排层消费的 AgentResult(executeAgentCall/finalizeCall 入参形状)
27
+ */
28
+ export function mapToWorkflowAgentResult(
29
+ r: SubagentsAgentResult,
30
+ ): WorkflowAgentResult {
31
+ return {
32
+ content: r.text,
33
+ parsedOutput: r.parsedOutput,
34
+ error: r.success ? undefined : r.error,
35
+ durationMs: r.durationMs,
36
+ sessionId: r.sessionId,
37
+ usage: r.usage ? mapUsage(r.usage, r.turns) : undefined,
38
+ toolCalls: r.toolCalls ? mapToolCalls(r.toolCalls) : undefined,
39
+ };
40
+ }
41
+
42
+ /**
43
+ * AgentUsageTotal(subagents)→ AgentUsage(workflow)映射。
44
+ *
45
+ * subagents AgentUsageTotal: { input, output, cacheRead, cacheWrite, total, cost }
46
+ * workflow AgentUsage: { input, output, cacheRead, cacheWrite, cost, contextTokens, turns }
47
+ *
48
+ * contextTokens ≈ total(subagents 的四项之和,近似上下文 token 量)。
49
+ * turns 来自 AgentResult.turns(非 usage 内字段)。
50
+ */
51
+ function mapUsage(u: AgentUsageTotal, turns: number): WorkflowAgentUsage {
52
+ return {
53
+ input: u.input,
54
+ output: u.output,
55
+ cacheRead: u.cacheRead,
56
+ cacheWrite: u.cacheWrite,
57
+ cost: u.cost,
58
+ contextTokens: u.total,
59
+ turns,
60
+ };
61
+ }
62
+
63
+ /**
64
+ * ToolCall(subagents)→ ToolCallEntry(workflow)映射。
65
+ *
66
+ * subagents ToolCall: { toolName, args?, result?, isError? }
67
+ * workflow ToolCallEntry: { name, input }
68
+ */
69
+ function mapToolCalls(calls: ToolCall[]): ToolCallEntry[] {
70
+ return calls.map((c) => ({
71
+ name: c.toolName,
72
+ input: c.args === undefined ? "" : safeStringify(c.args),
73
+ }));
74
+ }
75
+
76
+ /** 安全序列化(args 可能含循环引用或大对象,截断防 OOM)。 */
77
+ function safeStringify(value: unknown): string {
78
+ try {
79
+ const s = JSON.stringify(value);
80
+ return s.length > 500 ? `${s.slice(0, 500)}...` : s;
81
+ } catch {
82
+ return String(value);
83
+ }
84
+ }
@@ -0,0 +1,92 @@
1
+ // src/runtime/execution/alive-store.ts
2
+ //
3
+ // .alive sidecar 生产者 + pid 探活。
4
+ //
5
+ // 子进程启动时写 .alive(pid+id+startedAt),心跳检测时读它 + isProcessAlive
6
+ // 判活。finalize/cancel 收尾时 remove。与 .cancelled/.finalized 构成三件套。
7
+ //
8
+ // 设计对齐 tombstone-store:单文件 sidecar、best-effort I/O、无全局 index。
9
+
10
+ import * as fs from "node:fs";
11
+
12
+ import type { AliveMarker } from "./types.ts";
13
+
14
+ // ============================================================
15
+ // 公开函数
16
+ // ============================================================
17
+
18
+ /**
19
+ * 在 sessionFile 旁写 .alive sidecar(单行 JSON)。
20
+ * 覆盖写——同一 sessionFile 只有最后一个 alive marker 有意义。
21
+ */
22
+ export function writeAliveMarker(sessionFile: string, marker: AliveMarker): void {
23
+ const alivePath = `${sessionFile}.alive`;
24
+ fs.writeFileSync(alivePath, `${JSON.stringify(marker)}\n`, "utf-8");
25
+ }
26
+
27
+ /**
28
+ * 读 sessionFile 旁的 .alive sidecar。
29
+ * 返回 undefined:不存在 / 损坏 / 解析失败。
30
+ */
31
+ export function readAliveMarker(sessionFile: string): AliveMarker | undefined {
32
+ let raw: string;
33
+ try {
34
+ raw = fs.readFileSync(`${sessionFile}.alive`, "utf-8");
35
+ } catch {
36
+ return undefined;
37
+ }
38
+ try {
39
+ const parsed = JSON.parse(raw) as Partial<AliveMarker>;
40
+ if (
41
+ typeof parsed.pid === "number" &&
42
+ typeof parsed.id === "string" &&
43
+ typeof parsed.startedAt === "number"
44
+ ) {
45
+ return parsed as AliveMarker;
46
+ }
47
+ return undefined;
48
+ } catch {
49
+ return undefined;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * 删除 sessionFile 旁的 .alive sidecar。
55
+ * best-effort:不存在不抛(finalize/cancel 收尾调,sidecar 可能已被清理)。
56
+ */
57
+ export function removeAliveMarker(sessionFile: string): void {
58
+ try {
59
+ fs.unlinkSync(`${sessionFile}.alive`);
60
+ } catch {
61
+ void 0; // best-effort
62
+ }
63
+ }
64
+
65
+ /**
66
+ * 检测 pid 是否存活。
67
+ *
68
+ * process.kill(pid, 0) 语义:不发信号,仅检查进程是否存在。
69
+ * - 无异常 → 存活
70
+ * - ESRCH(No such process)→ 死
71
+ * - EPERM(Process exists but no permission)→ 存活(保守)
72
+ * - 其他异常 → 保守判死 false,避免误删活进程
73
+ */
74
+ export function isProcessAlive(pid: number): boolean {
75
+ try {
76
+ process.kill(pid, 0);
77
+ return true;
78
+ } catch (err: unknown) {
79
+ if (isErrnoException(err) && err.code === "EPERM") {
80
+ return true; // 存在但无权限发信号 → 判活
81
+ }
82
+ return false; // ESRCH 或其他异常 → 保守判死
83
+ }
84
+ }
85
+
86
+ // ============================================================
87
+ // 内部工具
88
+ // ============================================================
89
+
90
+ function isErrnoException(err: unknown): err is NodeJS.ErrnoException {
91
+ return err instanceof Error && "code" in err;
92
+ }
@@ -0,0 +1,30 @@
1
+ // src/utils/best-effort.ts
2
+ //
3
+ // best-effort IO 清理的错误吞咽 helper。
4
+ //
5
+ // 用途:sidecar 写入 / worktree remove / alive marker 删除等次要 IO,失败不影响
6
+ // 主流程(session 已完成或正在收尾)。这类 catch 故意吞错——但 taste/no-silent-catch
7
+ // 规则禁止空 catch 或仅 console 的 catch。本 helper 提供一条「实质调用语句」让
8
+ // catch 合规,同时把错误记录到 debug/error 便于排查。
9
+ //
10
+ // 规则绕过原理:taste/no-silent-catch 仅检查 CatchClause 直接 body 是否为空或仅
11
+ // console 调用。本 helper 是普通函数调用(ExpressionStatement),既非空也非仅
12
+ // console,故合规。helper 函数体内部的 console 不被该规则检查。
13
+
14
+ /** 错误日志级别。debug = 次要清理(默认);error = 关键步骤但需继续后续清理。 */
15
+ export type BestEffortLevel = "debug" | "error";
16
+
17
+ /**
18
+ * 吞咽 best-effort IO 的错误,按 level 记录到 console。
19
+ *
20
+ * - debug(默认):次要清理(sidecar/worktree/alive marker),失败属预期路径
21
+ * - error:关键步骤抛错但需继续后续清理(如 finalizeRecord 的 B9 链:completeRecord
22
+ * 抛错后仍要执行 finalized/cleanup,错误需可见但不阻断)
23
+ *
24
+ * 错误对象优先取 message(避免打印巨大堆栈/对象),其他类型原样打印。
25
+ */
26
+ export function bestEffort(err: unknown, context: string, level: BestEffortLevel = "debug"): void {
27
+ const detail = err instanceof Error ? err.message : err;
28
+ const fn = level === "error" ? console.error : console.debug;
29
+ fn(`[subagents] best-effort ${context} failed:`, detail);
30
+ }
@@ -0,0 +1,84 @@
1
+ // src/core/concurrency-pool.ts
2
+ //
3
+ // 并发控制 + 优先级排队。background=1000(单一优先级,保留 priority 机制供未来扩展)。
4
+
5
+ /** 队列条目:优先级 + resolver + 入队序号(同优先级 FIFO)。 */
6
+ interface QueueEntry {
7
+ priority: number;
8
+ resolve: () => void;
9
+ seq: number;
10
+ }
11
+
12
+ /** 并发池接口(可注入,便于测试 mock)。 */
13
+ export interface ConcurrencyPool {
14
+ /** 按优先级排队获取槽位(0=最高)。可选 effectiveMaxConcurrent 覆盖实例级默认配额。 */
15
+ acquire(priority: number, effectiveMaxConcurrent?: number): Promise<void>;
16
+ /** 归还槽位。必须无条件执行(finally)。 */
17
+ release(): void;
18
+ /** 当前已占用槽位数(诊断/widget 用)。 */
19
+ readonly active: number;
20
+ /** 实例级最大并发配额。调用方可据此计算分层配额(max(1, maxConcurrent - depth))。 */
21
+ readonly maxConcurrent: number;
22
+ }
23
+
24
+ /**
25
+ * 默认实现:maxConcurrent 槽位 + 优先级队列。
26
+ *
27
+ * acquire(priority, effectiveMaxConcurrent?):
28
+ * effective = effectiveMaxConcurrent ?? maxConcurrent
29
+ * active < effective → active++, resolve
30
+ * 否则 → 入队 { priority, resolve, seq }, 队列按 priority 升序 + seq FIFO
31
+ *
32
+ * release():
33
+ * queue 非空 → 出队最高优先级 resolve(active 不变)
34
+ * queue 空 → active--(防下溢)
35
+ */
36
+ export class DefaultConcurrencyPool implements ConcurrencyPool {
37
+ private _active = 0;
38
+ private readonly queue: QueueEntry[] = [];
39
+ private seqCounter = 0;
40
+
41
+ /** 下限 1——maxConcurrent=0 会让 acquire 永久排队死锁(C3 修复)。 */
42
+ readonly maxConcurrent: number;
43
+
44
+ constructor(maxConcurrent: number) {
45
+ this.maxConcurrent = Math.max(1, maxConcurrent);
46
+ }
47
+
48
+ acquire(priority: number, effectiveMaxConcurrent?: number): Promise<void> {
49
+ // effectiveMaxConcurrent 覆盖实例级默认配额(分层配额:调用方传 max(1, maxConcurrent - depth))。
50
+ // 不修改实例级 maxConcurrent——实例配额是全局共享上限,分层配额是本次 acquire 的局部上限。
51
+ const effective = effectiveMaxConcurrent ?? this.maxConcurrent;
52
+ if (this._active < effective) {
53
+ this._active += 1;
54
+ return Promise.resolve();
55
+ }
56
+ return new Promise<void>((resolve) => {
57
+ this.queue.push({ priority, resolve, seq: this.seqCounter++ });
58
+ });
59
+ }
60
+
61
+ release(): void {
62
+ if (this.queue.length > 0) {
63
+ // 取优先级最高(priority 最小)的;同优先级 FIFO(seq 最小)
64
+ let bestIdx = 0;
65
+ for (let i = 1; i < this.queue.length; i++) {
66
+ const cur = this.queue[i];
67
+ const best = this.queue[bestIdx];
68
+ if (cur.priority < best.priority || (cur.priority === best.priority && cur.seq < best.seq)) {
69
+ bestIdx = i;
70
+ }
71
+ }
72
+ const next = this.queue.splice(bestIdx, 1)[0];
73
+ next.resolve();
74
+ // active 不变(一个离开队列立即进入活跃)
75
+ } else if (this._active > 0) {
76
+ // 防御性下界:release 调用次数多于 acquire 时不让 active 为负
77
+ this._active -= 1;
78
+ }
79
+ }
80
+
81
+ get active(): number {
82
+ return this._active;
83
+ }
84
+ }
@@ -0,0 +1,73 @@
1
+ // src/runtime/config/config.ts
2
+ //
3
+ // 全局配置(~/.pi/agent/subagents/config.json)。
4
+ // 仅保留 maxConcurrent(pool 大小)。模型解析已退化为「主 agent model 优先」,
5
+ // 不再有 category/fallback/session 级覆盖——相关字段读取时忽略。
6
+
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+
10
+ import type { SubagentsGlobalConfig } from "./types.ts";
11
+
12
+ // ============================================================
13
+ // 常量
14
+ // ============================================================
15
+
16
+ /**
17
+ * 开箱默认配置(单一真相源,内联在代码里)。
18
+ *
19
+ * 历史教训 [HISTORICAL]:曾用包内 config.json(与 src/ 同级)作为默认值源,
20
+ * 但 config.json 被 .gitignore 排除且不应随 npm 包分发用户私有配置——导致
21
+ * npm pack 后读不到文件,catch 兜底用空字段,pi install 后首次执行抛错。
22
+ * 修复:默认值内联在代码里,不依赖任何包内文件。
23
+ */
24
+ const DEFAULT_CONFIG: SubagentsGlobalConfig = {
25
+ version: 1,
26
+ maxConcurrent: 6,
27
+ };
28
+
29
+ /** 默认 maxConcurrent(DEFAULT_CONFIG 的镜像,sanitize 用)。 */
30
+ const DEFAULT_MAX_CONCURRENT = 6;
31
+
32
+ // ============================================================
33
+ // 路径
34
+ // ============================================================
35
+
36
+ /**
37
+ * 配置文件路径(<agentDir>/subagents/config.json)。
38
+ * agentDir 由 Pi 核心 getAgentDir() 决定(读 PI_CODING_AGENT_DIR,默认 ~/.pi/agent),
39
+ * 与 Pi 主进程的目录约定完全一致——支持宿主经环境变量整体重定向。
40
+ */
41
+ export function getGlobalConfigPath(agentDir: string): string {
42
+ return path.join(agentDir, "subagents", "config.json");
43
+ }
44
+
45
+ // ============================================================
46
+ // 全局配置加载
47
+ // ============================================================
48
+
49
+ /**
50
+ * 加载全局配置。文件不存在 / JSON 解析失败 / 字段缺失时返回默认配置。
51
+ * 旧 config.json 中的 categories/fallback/yoloByDefault 等字段读取时忽略
52
+ * (模型解析已退化为「主 agent model 优先」)。
53
+ */
54
+ export function loadGlobalConfig(agentDir: string): SubagentsGlobalConfig {
55
+ const configPath = getGlobalConfigPath(agentDir);
56
+ try {
57
+ const raw = fs.readFileSync(configPath, "utf-8");
58
+ const parsed = JSON.parse(raw) as Partial<SubagentsGlobalConfig>;
59
+ return {
60
+ version: parsed.version ?? DEFAULT_CONFIG.version,
61
+ maxConcurrent: sanitizeMaxConcurrent(parsed.maxConcurrent),
62
+ };
63
+ } catch {
64
+ return { ...DEFAULT_CONFIG };
65
+ }
66
+ }
67
+
68
+ /** maxConcurrent 校验:正整数,否则默认。 */
69
+ function sanitizeMaxConcurrent(value: unknown): number {
70
+ return typeof value === "number" && Number.isInteger(value) && value > 0
71
+ ? value
72
+ : DEFAULT_MAX_CONCURRENT;
73
+ }