@zhushanwen/pi-subagent-workflow 7.4.0 → 8.0.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/README.md +15 -0
- package/package.json +1 -1
- package/skills/workflow-script-format/SKILL.md +1 -1
- package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +10 -4
- package/src/execution/__tests__/before-agent-start-injection.test.ts +132 -0
- package/src/execution/__tests__/bg-notify-render.test.ts +15 -15
- package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +365 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
- package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +215 -0
- package/src/execution/__tests__/conversation-wiring.test.ts +198 -0
- package/src/execution/__tests__/crash-recovery.test.ts +8 -2
- package/src/execution/__tests__/delivery-methods.test.ts +385 -0
- package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +49 -2
- package/src/execution/__tests__/execute-nesting.test.ts +20 -72
- package/src/execution/__tests__/execution-record.test.ts +199 -0
- package/src/execution/__tests__/finalize-record.test.ts +170 -14
- package/src/execution/__tests__/format.test.ts +131 -7
- package/src/execution/__tests__/gc-timer.test.ts +184 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +25 -7
- package/src/execution/__tests__/index-session-start-identity.test.ts +371 -0
- package/src/execution/__tests__/index-session-start.test.ts +257 -5
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +337 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
- package/src/execution/__tests__/list-component.test.ts +59 -5
- package/src/execution/__tests__/list-fields.test.ts +109 -0
- package/src/execution/__tests__/model-resolver.test.ts +38 -1
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
- package/src/execution/__tests__/nested-visibility.test.ts +325 -0
- package/src/execution/__tests__/notifier-flush.test.ts +209 -7
- package/src/execution/__tests__/one-shot-upgrade.test.ts +205 -0
- package/src/execution/__tests__/parent-child-matrix.test.ts +336 -0
- package/src/execution/__tests__/record-store.test.ts +158 -52
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +11 -12
- package/src/execution/__tests__/recursive-visibility-env.test.ts +18 -20
- package/src/execution/__tests__/resource-policy.test.ts +109 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +18 -25
- package/src/execution/__tests__/run-spawn-integration.test.ts +29 -25
- package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +14 -11
- package/src/execution/__tests__/session-pending.test.ts +61 -2
- package/src/execution/__tests__/session-reconstructor.test.ts +4 -4
- package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +15 -21
- package/src/execution/__tests__/session-start-reaper.test.ts +10 -8
- package/src/execution/__tests__/spawn-args.test.ts +127 -49
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +1 -0
- package/src/execution/__tests__/spawned-children.test.ts +92 -0
- package/src/execution/__tests__/status-refactor.test.ts +345 -0
- package/src/execution/__tests__/stdin-writer.test.ts +97 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +598 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
- package/src/execution/__tests__/subagent-service.test.ts +49 -11
- package/src/execution/__tests__/timeout-integration.test.ts +27 -13
- package/src/execution/__tests__/tool-action.test.ts +12 -10
- package/src/execution/__tests__/truncline-snapshot.test.ts +81 -0
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
- package/src/execution/__tests__/worktree-manager.test.ts +292 -89
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +13 -12
- package/src/execution/argv-mirror.ts +21 -2
- package/src/execution/execution-record.ts +126 -9
- package/src/execution/finalize-record.ts +90 -13
- package/src/execution/lifecycle-manager.ts +484 -0
- package/src/execution/lifecycle-predicates.ts +65 -0
- package/src/execution/manifest-store.ts +61 -16
- package/src/execution/model-resolver.ts +26 -5
- package/src/execution/notifier.ts +69 -12
- package/src/execution/pi-invocation.ts +21 -1
- package/src/execution/record-store.ts +554 -107
- package/src/execution/session-pending.ts +116 -45
- package/src/execution/session-reconstructor.ts +224 -7
- package/src/execution/session-runner.ts +270 -68
- package/src/execution/sessions-index.ts +304 -0
- package/src/execution/stdin-writer.ts +93 -7
- package/src/execution/stream-sink.ts +20 -3
- package/src/execution/subagent-service.ts +867 -138
- package/src/execution/turn-limiter.ts +14 -0
- package/src/execution/types.ts +200 -19
- package/src/execution/worktree-manager.ts +128 -49
- package/src/execution/worktree-registry.ts +13 -2
- package/src/index.ts +277 -19
- package/src/injectors/subagent-list-injector.ts +26 -8
- package/src/injectors/workflow-list-injector.ts +25 -8
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +17 -4
- package/src/interface/__tests__/tool-render.test.ts +10 -8
- package/src/interface/bg-notify-render.ts +32 -8
- package/src/interface/command-actions.ts +26 -7
- package/src/interface/commands.ts +21 -22
- package/src/interface/format.ts +43 -16
- package/src/interface/gui-mappers.ts +6 -8
- package/src/interface/helpers.ts +170 -10
- package/src/interface/list-component.ts +53 -14
- package/src/interface/subagent-actions.ts +235 -17
- package/src/interface/subagent-tool.ts +81 -16
- package/src/interface/subagents.ts +2 -1
- package/src/interface/tool-render.ts +11 -24
- package/src/interface/tool-workflow.ts +20 -35
- package/src/interface/views/WorkflowsView.ts +89 -32
- package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
- package/src/interface/views/format.ts +3 -3
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +325 -0
- package/src/orchestration/__tests__/args-validator.test.ts +1 -1
- package/src/orchestration/__tests__/config-loader.test.ts +38 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +394 -4
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +4 -4
- package/src/orchestration/__tests__/execute-agent-call.test.ts +95 -0
- package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +657 -18
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
- package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
- package/src/orchestration/__tests__/skill-discovery.test.ts +130 -61
- package/src/orchestration/__tests__/test-mocks.ts +191 -0
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +98 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -2
- package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
- package/src/orchestration/__tests__/workflows-e2e.test.ts +1 -1
- package/src/orchestration/agent-opts-resolver.ts +4 -1
- package/src/orchestration/args-validator.ts +2 -2
- package/src/orchestration/config-loader.ts +30 -1
- package/src/orchestration/error-recovery.ts +133 -29
- package/src/orchestration/execute-agent-call.ts +31 -7
- package/src/orchestration/jsonl-run-store.ts +287 -40
- package/src/orchestration/launcher.ts +7 -1
- package/src/orchestration/lifecycle.ts +135 -132
- package/src/orchestration/models/__tests__/trace.test.ts +408 -0
- package/src/orchestration/models/budget.ts +1 -1
- package/src/orchestration/models/run-runtime.ts +15 -17
- package/src/orchestration/models/run-spec.ts +2 -2
- package/src/orchestration/models/run-state.ts +3 -3
- package/src/orchestration/models/trace.ts +95 -15
- package/src/orchestration/models/types.ts +8 -9
- package/src/orchestration/models/workflow-run.ts +50 -71
- package/src/orchestration/models/workflow-script.ts +32 -1
- package/src/orchestration/skill-discovery.ts +30 -0
- package/src/orchestration/worker-handle.ts +1 -1
- package/src/orchestration/worker-host.ts +1 -1
- package/src/orchestration/worker-script-builder.ts +29 -10
- package/src/shared/__tests__/agent-ref.test.ts +34 -0
- package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +280 -0
- package/src/shared/__tests__/resource-discovery.test.ts +55 -0
- package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
- package/src/shared/agent-ref.ts +16 -0
- package/src/shared/resource-discovery.ts +147 -58
- package/src/shared/schema-jsonify.ts +53 -0
package/src/index.ts
CHANGED
|
@@ -30,12 +30,15 @@ import {
|
|
|
30
30
|
ModelConfigService,
|
|
31
31
|
setModelConfigService,
|
|
32
32
|
} from "./execution/model-config-service.ts";
|
|
33
|
+
import { IDENTITY_CUSTOM_TYPE, type SubagentIdentityData } from "./execution/session-reconstructor.ts";
|
|
34
|
+
import type { ExecutionMode, SubagentRecord } from "./execution/types.ts";
|
|
33
35
|
import { maybeCleanupExpiredSessionFiles } from "./execution/session-file-gc.ts";
|
|
34
36
|
import {
|
|
35
37
|
getSubagentService,
|
|
36
38
|
setSubagentService,
|
|
37
39
|
SubagentService,
|
|
38
40
|
} from "./execution/subagent-service.ts";
|
|
41
|
+
import { killAllSpawnedChildren } from "./execution/session-runner.ts";
|
|
39
42
|
import { SubprocessAgentRunner } from "./execution/subprocess-agent-runner.ts";
|
|
40
43
|
import { WorktreeManager } from "./execution/worktree-manager.ts";
|
|
41
44
|
import { setupSubagentListInjector } from "./injectors/subagent-list-injector.ts";
|
|
@@ -43,17 +46,23 @@ import { setupWorkflowListInjector } from "./injectors/workflow-list-injector.ts
|
|
|
43
46
|
import { renderBgNotifyMessage } from "./interface/bg-notify-render.ts";
|
|
44
47
|
import { registerWorkflowsCommand } from "./interface/commands.ts";
|
|
45
48
|
import { toGuiCtx } from "./interface/gui-mappers.ts";
|
|
46
|
-
import { notifyDone } from "./interface/helpers.ts";
|
|
49
|
+
import { notifyDone, trackNotifiedRunId } from "./interface/helpers.ts";
|
|
47
50
|
import { registerSubagentTool } from "./interface/subagent-tool.ts";
|
|
48
51
|
// ═══ interface/ 层(tools/commands/tui 合并) ═══
|
|
49
52
|
import { registerSubagentsCommand } from "./interface/subagents.ts";
|
|
50
53
|
import { registerWorkflowTool } from "./interface/tool-workflow.ts";
|
|
51
54
|
import { registerWorkflowScriptTool } from "./interface/tool-workflow-script.ts";
|
|
52
55
|
import { JsonlRunStore } from "./orchestration/jsonl-run-store.ts";
|
|
56
|
+
import { clearSkillPathCache } from "./orchestration/skill-discovery.ts";
|
|
53
57
|
// ═══ orchestration/ 层(workflow engine + infra) ═══
|
|
54
58
|
import type { LauncherDeps } from "./orchestration/launcher.ts";
|
|
55
59
|
import { executeNestedWorkflow, runAndWait, type WorkflowRunResult } from "./orchestration/launcher.ts";
|
|
56
|
-
import {
|
|
60
|
+
import {
|
|
61
|
+
evictDoneRunsBeyondCap,
|
|
62
|
+
MAX_RETAINED_DONE_RUNS,
|
|
63
|
+
scheduleTimeBudget,
|
|
64
|
+
terminateRunningRuns,
|
|
65
|
+
} from "./orchestration/lifecycle.ts";
|
|
57
66
|
import type { WorkflowRun } from "./orchestration/models/workflow-run.ts";
|
|
58
67
|
import { WorkerHostImpl } from "./orchestration/worker-host.ts";
|
|
59
68
|
import { WorkflowScriptRegistryImpl } from "./orchestration/workflow-script-registry-impl.ts";
|
|
@@ -76,6 +85,75 @@ declare module "@earendil-works/pi-coding-agent" {
|
|
|
76
85
|
// 模块级 logger(setPiHandle 注入后自动走 appendEntry)
|
|
77
86
|
const logger = getLogger("subagents");
|
|
78
87
|
|
|
88
|
+
// ── subagent 状态快照格式化 ──
|
|
89
|
+
//
|
|
90
|
+
// [v4 A-6] before_agent_start 注入 hook 已删(活跃 subagent 清单改由 agent 按需调
|
|
91
|
+
// action:'list' 拉取,消除每 loop 注入的上下文税与盲点)。本函数保留为纯格式化工具:
|
|
92
|
+
// before-agent-start-injection / parent-child-matrix 测试覆盖其正确性,未来 list
|
|
93
|
+
// 视图或其他注入点可复用。
|
|
94
|
+
|
|
95
|
+
/** 活跃 subagent 数量上限(超过截断显示)。 */
|
|
96
|
+
const MAX_STATUS_INJECTION = 10;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 将活跃 subagent record 格式化为一行一条的快照文本。
|
|
100
|
+
*
|
|
101
|
+
* 格式:
|
|
102
|
+
* [subagent-status] N active subagents:
|
|
103
|
+
* - sa-xxx (slug): running, rounds 0
|
|
104
|
+
* - sa-yyy (slug): idle, rounds 3
|
|
105
|
+
* +2 more, use action:'list'
|
|
106
|
+
*
|
|
107
|
+
* @param records 已筛选的活跃 record(running + idle)
|
|
108
|
+
*/
|
|
109
|
+
export function formatSubagentStatusSnapshot(records: SubagentRecord[]): string {
|
|
110
|
+
const lines = [`[subagent-status] ${records.length} active subagent${records.length === 1 ? "" : "s"}:`];
|
|
111
|
+
const shown = records.slice(0, MAX_STATUS_INJECTION);
|
|
112
|
+
for (const r of shown) {
|
|
113
|
+
const slug = r.slug || r.agent;
|
|
114
|
+
const roundPart = r.round !== undefined && r.round > 0 ? `, rounds ${r.round}` : "";
|
|
115
|
+
lines.push(`- ${r.id} (${slug}): ${r.status}${roundPart}`);
|
|
116
|
+
}
|
|
117
|
+
const remaining = records.length - MAX_STATUS_INJECTION;
|
|
118
|
+
if (remaining > 0) {
|
|
119
|
+
lines.push(`+${remaining} more, use action:'list'`);
|
|
120
|
+
}
|
|
121
|
+
return lines.join("\n");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ═══ [V2 决策 7 防线 i] process 级 shutdown hook ═══
|
|
125
|
+
//
|
|
126
|
+
// session_shutdown 是 pi 的 async hook,进程被 SIGTERM/SIGINT 强杀或崩溃时来不及
|
|
127
|
+
// 触发;sync 子进程(controller 为 undefined,abortRunningControllers 跳过它们)会
|
|
128
|
+
// 泄漏为孤儿。process.on 兜底显式 killAllSpawnedChildren 收割全部活子进程。
|
|
129
|
+
// guard 防多信号叠加(如 SIGINT 后又 beforeExit)重复 kill。
|
|
130
|
+
let processShutdownHookFired = false;
|
|
131
|
+
|
|
132
|
+
function reapSpawnedChildrenOnShutdown(): void {
|
|
133
|
+
if (processShutdownHookFired) return;
|
|
134
|
+
processShutdownHookFired = true;
|
|
135
|
+
try {
|
|
136
|
+
killAllSpawnedChildren("SIGTERM");
|
|
137
|
+
} catch (err) {
|
|
138
|
+
// best-effort:收割失败不阻断退出流程——debug 留痕(孤儿子进程排查线索),
|
|
139
|
+
// 不静默吞错,对齐「错误必须可操作」。
|
|
140
|
+
logger.debug(
|
|
141
|
+
"[subagents] process shutdown reap best-effort failed (killAllSpawnedChildren SIGTERM)",
|
|
142
|
+
{ reason: err instanceof Error ? err.message : String(err) },
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 测试钩子:重置 module 级 shutdown guard。
|
|
149
|
+
*
|
|
150
|
+
* 对齐 lifecycle-manager._resetLifecycleState 模式——processShutdownHookFired 是
|
|
151
|
+
* module 级单例状态,跨 test 持久,单测需显式重置以验证 idempotent 行为。
|
|
152
|
+
*/
|
|
153
|
+
export function _resetProcessShutdownGuardForTest(): void {
|
|
154
|
+
processShutdownHookFired = false;
|
|
155
|
+
}
|
|
156
|
+
|
|
79
157
|
export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
80
158
|
// 注入 pi handle 给全局 extension-logger,让深层代码(best-effort / error-recovery)
|
|
81
159
|
// 的 getLogger("subagents") 也能走 appendEntry。
|
|
@@ -187,7 +265,23 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
187
265
|
runner: state.runner,
|
|
188
266
|
runs: state.runs,
|
|
189
267
|
registry,
|
|
190
|
-
onRunDone
|
|
268
|
+
// onRunDone 是全部 done 路径的单点汇聚(abortRun + error-recovery),顺序固化为
|
|
269
|
+
// notify → track → evict:notifyDone 先发完整聚合通知(淘汰后聚合根仍在闭包参数
|
|
270
|
+
// run 引用上不受影响),trackNotifiedRunId 有界化去重窗口,最后裁剪 done run 内存。
|
|
271
|
+
// 本轮 run 的 completedAt 在 transition("done") 时同步设为当前时刻=全局最新,
|
|
272
|
+
// 恒在保留端——结构性保证其不被自身触发的裁剪淘汰,无需 protectRunId。
|
|
273
|
+
onRunDone: (run: WorkflowRun) => {
|
|
274
|
+
notifyDone(pi, run.runId, run, notifiedRunIds, toGuiCtx(sessionCtx));
|
|
275
|
+
trackNotifiedRunId(notifiedRunIds, run.runId);
|
|
276
|
+
const evicted = evictDoneRunsBeyondCap(state.runs, MAX_RETAINED_DONE_RUNS);
|
|
277
|
+
if (evicted > 0) {
|
|
278
|
+
logger.debug("[subagent-workflow] evicted done runs beyond cap", {
|
|
279
|
+
evicted,
|
|
280
|
+
keep: MAX_RETAINED_DONE_RUNS,
|
|
281
|
+
sessionId: lsRef.lastSessionId,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
},
|
|
191
285
|
eventBus: pi.events,
|
|
192
286
|
scheduleTimeBudget: (runId: string, budgetTimeMs: number) =>
|
|
193
287
|
scheduleTimeBudget(runId, deps, budgetTimeMs),
|
|
@@ -217,6 +311,55 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
217
311
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
218
312
|
lsRef.lastSessionId = sessionId;
|
|
219
313
|
|
|
314
|
+
// skill 路径两级缓存 session 级失效:pi 同进程可能有多个 session(TUI /new、/fork),
|
|
315
|
+
// 运行中安装的 skill 需对新 session 可见(含曾 miss 缓存的 undefined 条目与 npm 新装
|
|
316
|
+
// 包的候选目录)。session 内复用收益不变(IF8/DM3 消重发生在同 session 的重复调用)。
|
|
317
|
+
clearSkillPathCache();
|
|
318
|
+
|
|
319
|
+
// ── [M4] identity 子进程写入(V2 决策 5)──
|
|
320
|
+
// 子进程经 env(PI_SUBAGENT_*)接收自己的 identity,在 session_start 用 pi.appendEntry
|
|
321
|
+
// 写 subagent-identity custom entry。pi 自动生成 id/parentId → message tree 连续。
|
|
322
|
+
// 旧实现父进程 fs.appendFileSync 补写的 custom entry 缺 id/parentId → 污染 _buildIndex
|
|
323
|
+
// leafId 指针 → message tree 断成两棵 → 多轮对话丢上下文(bug 根因)。
|
|
324
|
+
// 主/子进程判定:PI_SUBAGENT_SELF_RECORD_ID 仅 session-runner spawn 子进程时注入,
|
|
325
|
+
// 主进程无此 env → 跳过(identity 只在子进程写一次)。
|
|
326
|
+
const selfRecordId = process.env.PI_SUBAGENT_SELF_RECORD_ID;
|
|
327
|
+
if (selfRecordId) {
|
|
328
|
+
try {
|
|
329
|
+
const modeEnv = process.env.PI_SUBAGENT_MODE;
|
|
330
|
+
// ExecutionMode 联合窄化:父进程经 env 注入(record.mode 恒为 "background"),
|
|
331
|
+
// 运行时校验合法值,非法兜底 background(避免裸 cast,符合 taste/no-unsafe-cast)。
|
|
332
|
+
const mode: ExecutionMode = modeEnv === "background" ? modeEnv : "background";
|
|
333
|
+
const identity: SubagentIdentityData = {
|
|
334
|
+
id: selfRecordId,
|
|
335
|
+
agent: process.env.PI_SUBAGENT_AGENT ?? "",
|
|
336
|
+
mode,
|
|
337
|
+
task: process.env.PI_SUBAGENT_TASK ?? "",
|
|
338
|
+
slug: process.env.PI_SUBAGENT_SLUG,
|
|
339
|
+
startedAt: Number(process.env.PI_SUBAGENT_STARTED_AT ?? Date.now()),
|
|
340
|
+
rootSessionId: process.env.PI_SUBAGENT_ROOT_SESSION_ID,
|
|
341
|
+
parentRecordId: process.env.PI_SUBAGENT_PARENT_RECORD_ID,
|
|
342
|
+
depth:
|
|
343
|
+
process.env.PI_SUBAGENT_DEPTH !== undefined
|
|
344
|
+
? Number(process.env.PI_SUBAGENT_DEPTH)
|
|
345
|
+
: undefined,
|
|
346
|
+
forkDepth:
|
|
347
|
+
process.env.PI_SUBAGENT_FORK_DEPTH !== undefined
|
|
348
|
+
? Number(process.env.PI_SUBAGENT_FORK_DEPTH)
|
|
349
|
+
: undefined,
|
|
350
|
+
chatMode: process.env.PI_SUBAGENT_CHAT_MODE === "true",
|
|
351
|
+
// [review round2] worktree 隔离标志(session-runner 注入):跨重启重建路径据此
|
|
352
|
+
// 拒绝续聊(handle 不可序列化,reattach 不可行)。
|
|
353
|
+
worktree: process.env.PI_SUBAGENT_WORKTREE === "true",
|
|
354
|
+
};
|
|
355
|
+
pi.appendEntry(IDENTITY_CUSTOM_TYPE, identity);
|
|
356
|
+
} catch (err) {
|
|
357
|
+
logger.warn("[subagents] identity appendEntry failed in session_start", {
|
|
358
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
220
363
|
// ── subagents 域:双 Service 装配 ──
|
|
221
364
|
const existingService = getSubagentService();
|
|
222
365
|
const existingModelService = getModelConfigService();
|
|
@@ -267,6 +410,9 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
267
410
|
setSubagentService(service);
|
|
268
411
|
}
|
|
269
412
|
|
|
413
|
+
// S-2: 启动 idle record GC 定时器(30 天 TTL,每小时检查一次)
|
|
414
|
+
service.startGcTimer();
|
|
415
|
+
|
|
270
416
|
cachedMainSessionFile = ctx.sessionManager.getSessionFile() ?? undefined;
|
|
271
417
|
|
|
272
418
|
try {
|
|
@@ -292,7 +438,7 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
292
438
|
|
|
293
439
|
try {
|
|
294
440
|
const wtm = new WorktreeManager(agentDir);
|
|
295
|
-
wtm.scan();
|
|
441
|
+
await wtm.scan();
|
|
296
442
|
} catch (err) {
|
|
297
443
|
logger.warn("[subagents] worktree reaper scan failed", {
|
|
298
444
|
reason: err instanceof Error ? err.message : String(err),
|
|
@@ -328,6 +474,22 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
328
474
|
}
|
|
329
475
|
runs.set(run.runId, run);
|
|
330
476
|
}
|
|
477
|
+
// done run 内存有界性:loadAll 全量重水合后立即裁剪到 K。kill-9 恢复(上方
|
|
478
|
+
// running → transition("done","failed"))的 run completedAt 为 transition 时刻
|
|
479
|
+
// (当前时间=全局最新)参与排序且必在保留端;多条恢复 run 同 ms completedAt →
|
|
480
|
+
// tie 稳定排序。淘汰只 delete runs Map 条目——磁盘 state 文件与
|
|
481
|
+
// workflow-state-link 指针条目均不动(历史审计保留);下次 session_start loadAll
|
|
482
|
+
// 从指针全量重水合后再次裁剪,该循环每次 session 启动重复且可接受:内存峰值只在
|
|
483
|
+
// 启动期,常驻 O(K + 活跃 run)。消除启动峰值需指针 compaction,属 append+replay
|
|
484
|
+
// 长期方案问题域,非本范围。
|
|
485
|
+
const evicted = evictDoneRunsBeyondCap(runs, MAX_RETAINED_DONE_RUNS);
|
|
486
|
+
if (evicted > 0) {
|
|
487
|
+
logger.debug("[subagent-workflow] evicted done runs beyond cap after loadAll", {
|
|
488
|
+
evicted,
|
|
489
|
+
keep: MAX_RETAINED_DONE_RUNS,
|
|
490
|
+
sessionId,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
331
493
|
} catch (err) {
|
|
332
494
|
// QMF-4 fix: store.loadAll 失败是关键路径错误,workflow 域将未初始化
|
|
333
495
|
logger.error("[subagent-workflow] store.loadAll failed, workflow domain uninitialized", {
|
|
@@ -371,7 +533,7 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
371
533
|
});
|
|
372
534
|
|
|
373
535
|
// ════════════════════════════════════════════════════════════
|
|
374
|
-
// session_tree
|
|
536
|
+
// session_tree:切分支前终止所有 running run(一次性生命周期——切走即作废)
|
|
375
537
|
// ════════════════════════════════════════════════════════════
|
|
376
538
|
pi.on("session_tree", async (_event: SessionTreeEvent, ctx: ExtensionContext) => {
|
|
377
539
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
@@ -379,33 +541,90 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
379
541
|
|
|
380
542
|
const state = sessionState.get(sessionId);
|
|
381
543
|
if (state) {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
}
|
|
389
|
-
}
|
|
544
|
+
// 一次性生命周期(D-2):running run 转 done,failed 落盘(helper 内部自过滤
|
|
545
|
+
// running,单 run 失败不中断其余)。此处不再挂起待恢复。
|
|
546
|
+
try {
|
|
547
|
+
await terminateRunningRuns(makeDeps(state, ctx), "Session switched: run terminated");
|
|
548
|
+
} catch (err) {
|
|
549
|
+
bestEffort(err, "terminateRunningRuns (session_tree handler)");
|
|
390
550
|
}
|
|
391
551
|
}
|
|
392
552
|
});
|
|
393
553
|
|
|
394
554
|
// ════════════════════════════════════════════════════════════
|
|
395
|
-
//
|
|
555
|
+
// SP-4: session_before_fork(/fork)/ session_before_switch(/new)级联关闭
|
|
556
|
+
// ════════════════════════════════════════════════════════════
|
|
557
|
+
// 主 session /fork 或 /new 时,清理旧 record(disposeAllRecords:CAS 转终态 +
|
|
558
|
+
// archive + worktree 清理)。before 事件在 session 替换前触发,确保旧 session 的
|
|
559
|
+
// subagent 在新 session 创建前被清理(随后的 session_shutdown → dispose 收割子进程)。
|
|
560
|
+
//
|
|
561
|
+
// [M2 修复] 旧实现把 /new 级联挂在 session_before_tree 上——SDK 中该事件只由
|
|
562
|
+
// AgentSession.navigateTree()(/tree 同 session 分支切换)触发,/new 走
|
|
563
|
+
// session_before_switch(reason:"new") + session_shutdown(reason:"new"),从不触发
|
|
564
|
+
// before_tree。后果双向:/new 级联是死代码;普通 /tree 分支导航反而误杀全部活跃
|
|
565
|
+
// subagent。现 /new 改挂 session_before_switch(reason==="new"),before_tree handler
|
|
566
|
+
// 移除(/tree 是同 session 内导航,record/子进程归属不变,无级联关闭诉求)。
|
|
567
|
+
pi.on("session_before_fork", (_event, _ctx) => {
|
|
568
|
+
const service = getSubagentService();
|
|
569
|
+
if (service) {
|
|
570
|
+
const count = service.onParentFork();
|
|
571
|
+
if (count > 0) {
|
|
572
|
+
logger.warn(`[subagents] /fork 级联关闭 ${count} 个 subagent`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
pi.on("session_before_switch", (event, _ctx) => {
|
|
578
|
+
// /new(reason:"new")创建全新 session → 级联关闭旧 record。
|
|
579
|
+
// reason:"resume"(/resume /import 回到已有 session)不级联:record 按 rootSessionId
|
|
580
|
+
// 归属隔离,跨 session 读写由 store 过滤守卫,无需销毁。
|
|
581
|
+
if (event.reason !== "new") return;
|
|
582
|
+
const service = getSubagentService();
|
|
583
|
+
if (service) {
|
|
584
|
+
const count = service.onParentNew();
|
|
585
|
+
if (count > 0) {
|
|
586
|
+
logger.warn(`[subagents] /new 级联关闭 ${count} 个 subagent`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
// ════════════════════════════════════════════════════════════
|
|
592
|
+
// session_shutdown:dispose subagents + terminate workflows + store 收尾 + cleanup
|
|
593
|
+
//
|
|
594
|
+
// store 收尾:每 session 的 JsonlRunStore 在 terminateRunningRuns 之后 dispose(刷
|
|
595
|
+
// pending 去抖批 + await in-flight 链,见 W2C5)。R3 声明:SIGTERM/SIGINT 走下方
|
|
596
|
+
// process handler 不触发本路径,pending 去抖丢失等价崩溃链(重启后 kill-9 恢复
|
|
597
|
+
// 收编 running 残留——终态/创建均冷路径已落盘,丢的只有 ≤saveDebounceMs 的
|
|
598
|
+
// running 尾巴,ES1 已接受);不做 best-effort SIGTERM dispose(需同步 IO 改造,
|
|
599
|
+
// 超出 wave 边界)。
|
|
396
600
|
// ════════════════════════════════════════════════════════════
|
|
397
601
|
pi.on("session_shutdown", async (_event: SessionShutdownEvent, _ctx: ExtensionContext) => {
|
|
398
602
|
// ── subagents 域:dispose SubagentService ──
|
|
399
603
|
getSubagentService()?.dispose();
|
|
400
604
|
|
|
401
|
-
// ── workflow 域:
|
|
605
|
+
// ── workflow 域:terminate 所有 running run + store 收尾 + 清理 temp files ──
|
|
402
606
|
// H-5: 遍历所有 sessionState 条目清理(而不只 lastSessionId——
|
|
403
607
|
// 防御 session 切换但 session_tree 未先触发导致 lastSessionId 指向已删除 session 的情况)。
|
|
404
608
|
for (const [sessionId, state] of sessionState) {
|
|
405
|
-
|
|
406
|
-
await
|
|
407
|
-
|
|
408
|
-
|
|
609
|
+
// 编排顺序(W2C5):terminate(await,failed 落盘——重启后 kill-9 恢复不误判)
|
|
610
|
+
// → store.dispose(await,刷 pending 去抖批 + await in-flight 链,关「shutdown
|
|
611
|
+
// 时刻 pending 去抖写丢失」窗口)→ delete。terminate 的 running 过滤在 helper
|
|
612
|
+
// 内部(单 run 失败不中断其余);外层 try/catch 兜底防单 session 异常中断后续
|
|
613
|
+
// session 条目的 dispose + delete(对齐原 allSettled 的不中断语义)。
|
|
614
|
+
try {
|
|
615
|
+
await terminateRunningRuns(makeDeps(state, _ctx), "Session shutdown: run terminated");
|
|
616
|
+
} catch (err) {
|
|
617
|
+
bestEffort(err, "terminateRunningRuns (session_shutdown handler)");
|
|
618
|
+
}
|
|
619
|
+
// dispose 自身恒 resolve,catch 兜底防御——handler 内抛错会中断后续 session
|
|
620
|
+
// 条目清理。不留静默吞错(错误必须可操作):debug 留痕带 sessionId/sessionDir,
|
|
621
|
+
// 排查「shutdown 后 run 状态不落盘」类问题时有迹可循。
|
|
622
|
+
await state.store.dispose().catch((err: unknown) => {
|
|
623
|
+
logger.debug(
|
|
624
|
+
`[subagent-workflow] session_shutdown store.dispose failed (sessionId=${sessionId}, sessionDir=${state.sessionDir})`,
|
|
625
|
+
{ reason: err instanceof Error ? err.message : String(err) },
|
|
626
|
+
);
|
|
627
|
+
});
|
|
409
628
|
sessionState.delete(sessionId);
|
|
410
629
|
}
|
|
411
630
|
|
|
@@ -422,6 +641,45 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
422
641
|
dialogQueue.rejectAll();
|
|
423
642
|
});
|
|
424
643
|
|
|
644
|
+
// ════════════════════════════════════════════════════════════
|
|
645
|
+
// [V2 决策 7 防线 i] process 级 shutdown hook(显式收割三道防线之一)
|
|
646
|
+
//
|
|
647
|
+
// 上方 session_shutdown(pi async hook)在进程被 SIGTERM/SIGINT 强杀或崩溃时
|
|
648
|
+
// 不触发,此处 process.on 兜底确保 sync 子进程被收割(防线 i:shutdown 时显式
|
|
649
|
+
// SIGTERM 全部 activation)。
|
|
650
|
+
//
|
|
651
|
+
// - SIGTERM:pi 各 mode(rpc/interactive/print)自带 SIGTERM handler 负责退出编排,
|
|
652
|
+
// 本 extension 的 handler 只做收割 + 设 exitCode(不 re-raise、不抢 pi 的退出语义;
|
|
653
|
+
// xyz-agent 桌面 supervisor 用 SIGTERM 杀 pi 走这条路)。
|
|
654
|
+
// - SIGINT:pi 本体不注册常规 SIGINT handler(interactive/print/rpc 均 SIGTERM only),
|
|
655
|
+
// 依赖 Node 默认终止。本 extension 注册 listener 即取消默认终止——若只设 exitCode,
|
|
656
|
+
// 本地 pi CLI 的 Ctrl-C 杀不死进程(TUI/stdin/agent loop 仍在事件循环)。故收割
|
|
657
|
+
// 完成后 re-raise 恢复默认终止,见下方 sigintHandler。
|
|
658
|
+
// - beforeExit 是退出前最后事件,不 exit(自然退出)。
|
|
659
|
+
// - idempotent guard(reapSpawnedChildrenOnShutdown 内)防多信号叠加重复 kill。
|
|
660
|
+
//
|
|
661
|
+
// 防线 iii(activate 互斥)已接入:subagent-service.ts 冷路径 resume 调
|
|
662
|
+
// acquireActivateLock(含 30s 超时兜底,见 lifecycle-manager.ts ACTIVATE_LOCK_TIMEOUT_MS)。
|
|
663
|
+
// 防线 ii(启动 scanOrphanProcesses)骨架就位,启动时接入待实现。
|
|
664
|
+
// ════════════════════════════════════════════════════════════
|
|
665
|
+
process.on("SIGTERM", () => {
|
|
666
|
+
reapSpawnedChildrenOnShutdown();
|
|
667
|
+
// S-3: 改用 process.exitCode 而非 process.exit(0),让子进程 cleanup 完成后再自然退出。
|
|
668
|
+
// process.exit(0) 会立即终止,可能在 reapSpawnedChildrenOnShutdown 完成前截断。
|
|
669
|
+
// 退出编排归 pi 自身的 SIGTERM handler(rpc-mode 会主动退出)。
|
|
670
|
+
process.exitCode = 0;
|
|
671
|
+
});
|
|
672
|
+
// [review 修复] SIGINT re-raise:收割同步完成后,先移除自身 listener 再向自身重发
|
|
673
|
+
// SIGINT,恢复 Node 默认终止。不 removeListener 直接 kill(process.pid) 会再次进入
|
|
674
|
+
// 本 handler 递归;移除后无其他 SIGINT listener(pi 不注册)→ 默认行为终止进程。
|
|
675
|
+
const sigintHandler = (): void => {
|
|
676
|
+
reapSpawnedChildrenOnShutdown();
|
|
677
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
678
|
+
process.kill(process.pid, "SIGINT");
|
|
679
|
+
};
|
|
680
|
+
process.on("SIGINT", sigintHandler);
|
|
681
|
+
process.on("beforeExit", reapSpawnedChildrenOnShutdown);
|
|
682
|
+
|
|
425
683
|
// ════════════════════════════════════════════════════════════
|
|
426
684
|
// pi.__workflowRun(D-8 签名)
|
|
427
685
|
// ════════════════════════════════════════════════════════════
|
|
@@ -51,6 +51,19 @@ const logger = getLogger("injector");
|
|
|
51
51
|
*/
|
|
52
52
|
let agentCache: AgentEntry[] | null = null;
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* 注入块渲染缓存(与 agentCache 同步更新):before_agent_start 每个 turn 都要注入,
|
|
56
|
+
* formatAgentList(escapeXml 多趟正则 × 全部字段)在数据不变时输出完全相同——渲染
|
|
57
|
+
* 一次随缓存复用,turn 热路径零重复计算。
|
|
58
|
+
*/
|
|
59
|
+
let agentInjectionCache: string | null = null;
|
|
60
|
+
|
|
61
|
+
/** agentCache 唯一写点:数据与渲染缓存同步更新(null 清空两者)。 */
|
|
62
|
+
function setAgentCache(entries: AgentEntry[] | null): void {
|
|
63
|
+
agentCache = entries;
|
|
64
|
+
agentInjectionCache = entries !== null ? formatAgentList(entries) : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
54
67
|
/** 从 .md frontmatter 提取的最小 agent 信息(m5:+ when/examples 路由样本;S1:+ path) */
|
|
55
68
|
export interface AgentEntry {
|
|
56
69
|
name: string;
|
|
@@ -189,9 +202,11 @@ export function setupSubagentListInjector(pi: ExtensionAPI): void {
|
|
|
189
202
|
"session_start",
|
|
190
203
|
async (_event: SessionStartEvent, ctx: ExtensionContext): Promise<void> => {
|
|
191
204
|
try {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
205
|
+
setAgentCache(
|
|
206
|
+
await discoverAllAgents(
|
|
207
|
+
findWorkspaceRoot(ctx.cwd),
|
|
208
|
+
getAgentDir(),
|
|
209
|
+
),
|
|
195
210
|
);
|
|
196
211
|
} catch (err) {
|
|
197
212
|
// fail-safe:发现异常不阻断 session,缓存保持 null(before_agent_start 会 fallback)
|
|
@@ -211,12 +226,15 @@ export function setupSubagentListInjector(pi: ExtensionAPI): void {
|
|
|
211
226
|
try {
|
|
212
227
|
// 读缓存;miss(session_start 未触发/缓存被清)则 fallback 重新发现+赋值
|
|
213
228
|
if (agentCache === null) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
229
|
+
setAgentCache(
|
|
230
|
+
await discoverAllAgents(
|
|
231
|
+
findWorkspaceRoot(ctx.cwd),
|
|
232
|
+
getAgentDir(),
|
|
233
|
+
),
|
|
217
234
|
);
|
|
218
235
|
}
|
|
219
|
-
|
|
236
|
+
// agentInjectionCache 与 agentCache 不变量同步(setAgentCache 保证),直接复用
|
|
237
|
+
const injection = agentInjectionCache;
|
|
220
238
|
if (!injection) return;
|
|
221
239
|
return { systemPrompt: event.systemPrompt + injection };
|
|
222
240
|
} catch (err) {
|
|
@@ -230,7 +248,7 @@ export function setupSubagentListInjector(pi: ExtensionAPI): void {
|
|
|
230
248
|
pi.on(
|
|
231
249
|
"session_shutdown",
|
|
232
250
|
(_event: SessionShutdownEvent, _ctx: ExtensionContext): void => {
|
|
233
|
-
|
|
251
|
+
setAgentCache(null);
|
|
234
252
|
},
|
|
235
253
|
);
|
|
236
254
|
}
|
|
@@ -43,6 +43,18 @@ const logger = getLogger("injector");
|
|
|
43
43
|
*/
|
|
44
44
|
let workflowCache: WorkflowEntry[] | null = null;
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* 注入块渲染缓存(与 workflowCache 同步更新):before_agent_start 每个 turn 都要注入,
|
|
48
|
+
* formatWorkflowList 在数据不变时输出完全相同——渲染一次随缓存复用。
|
|
49
|
+
*/
|
|
50
|
+
let workflowInjectionCache: string | null = null;
|
|
51
|
+
|
|
52
|
+
/** workflowCache 唯一写点:数据与渲染缓存同步更新(null 清空两者)。 */
|
|
53
|
+
function setWorkflowCache(entries: WorkflowEntry[] | null): void {
|
|
54
|
+
workflowCache = entries;
|
|
55
|
+
workflowInjectionCache = entries !== null ? formatWorkflowList(entries) : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
/** 注入段中单个 workflow 的最大描述长度(控制每 turn prompt 体积) */
|
|
47
59
|
const MAX_DESC_LEN = 160;
|
|
48
60
|
|
|
@@ -172,9 +184,11 @@ export function setupWorkflowListInjector(pi: ExtensionAPI): void {
|
|
|
172
184
|
"session_start",
|
|
173
185
|
async (_event: SessionStartEvent, ctx: ExtensionContext): Promise<void> => {
|
|
174
186
|
try {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
187
|
+
setWorkflowCache(
|
|
188
|
+
await discoverAllWorkflows(
|
|
189
|
+
findWorkspaceRoot(ctx.cwd),
|
|
190
|
+
getAgentDir(),
|
|
191
|
+
),
|
|
178
192
|
);
|
|
179
193
|
} catch (err) {
|
|
180
194
|
// fail-safe:发现异常不阻断 session,缓存保持 null(before_agent_start 会 fallback)
|
|
@@ -194,12 +208,15 @@ export function setupWorkflowListInjector(pi: ExtensionAPI): void {
|
|
|
194
208
|
try {
|
|
195
209
|
// 读缓存;miss(session_start 未触发/缓存被清)则 fallback 重新发现+赋值
|
|
196
210
|
if (workflowCache === null) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
211
|
+
setWorkflowCache(
|
|
212
|
+
await discoverAllWorkflows(
|
|
213
|
+
findWorkspaceRoot(ctx.cwd),
|
|
214
|
+
getAgentDir(),
|
|
215
|
+
),
|
|
200
216
|
);
|
|
201
217
|
}
|
|
202
|
-
|
|
218
|
+
// workflowInjectionCache 与 workflowCache 不变量同步(setWorkflowCache 保证),直接复用
|
|
219
|
+
const injection = workflowInjectionCache;
|
|
203
220
|
if (!injection) return;
|
|
204
221
|
return { systemPrompt: event.systemPrompt + injection };
|
|
205
222
|
} catch (err) {
|
|
@@ -213,7 +230,7 @@ export function setupWorkflowListInjector(pi: ExtensionAPI): void {
|
|
|
213
230
|
pi.on(
|
|
214
231
|
"session_shutdown",
|
|
215
232
|
(_event: SessionShutdownEvent, _ctx: ExtensionContext): void => {
|
|
216
|
-
|
|
233
|
+
setWorkflowCache(null);
|
|
217
234
|
},
|
|
218
235
|
);
|
|
219
236
|
}
|
|
@@ -31,12 +31,16 @@ function extractDescription(src: string): string {
|
|
|
31
31
|
const DESCRIPTION = extractDescription(SUBAGENT_TOOL_SRC);
|
|
32
32
|
|
|
33
33
|
describe("subagent tool description — 行为约束器(非功能说明书)", () => {
|
|
34
|
-
it("词数 ≤
|
|
34
|
+
it("词数 ≤ 800(高风险 description 密度上限)", () => {
|
|
35
35
|
// 高风险 tool 的 description 应聚焦约束而非功能铺陈;过长会稀释信号。
|
|
36
|
-
//
|
|
37
|
-
//
|
|
36
|
+
// 上限演进:400 → 550(补 start/list/cancel 三 action 完整 JSON 正例)→
|
|
37
|
+
// 650(M2-B3 新增 message/close 两 action + conversation 对话模式 + 例子)→
|
|
38
|
+
// 800(递归指导:树形判据 + 自包含 task + 独立验收 + fork 成本警告)。
|
|
39
|
+
// 800 是断言的真实预算(标题与本值一致);留 ≥60 词余量供后续措辞微调,
|
|
40
|
+
// 逼近上限时优先精简正文而非再放宽断言。
|
|
41
|
+
// 每次 action 扩展必然增加必需描述;正例对弱模型首次用对参数的价值 > 节省 description 预算。
|
|
38
42
|
const words = DESCRIPTION.trim().split(/\s+/).filter(Boolean).length;
|
|
39
|
-
expect(words).toBeLessThanOrEqual(
|
|
43
|
+
expect(words).toBeLessThanOrEqual(800);
|
|
40
44
|
});
|
|
41
45
|
|
|
42
46
|
it("含 'When to delegate' 调用条件段(何时委派 vs 自己做)", () => {
|
|
@@ -78,6 +82,15 @@ describe("subagent tool description — 行为约束器(非功能说明书)"
|
|
|
78
82
|
expect(DESCRIPTION).toMatch(/Depth: N\/10/);
|
|
79
83
|
});
|
|
80
84
|
|
|
85
|
+
it("递归指导含树形判据 + 自包含 task + 独立验收 + 深度定位", () => {
|
|
86
|
+
// 递归不是「能套就套」,必须有明确使用判据,否则 LLM 会滥用深递归。
|
|
87
|
+
const lower = DESCRIPTION.toLowerCase();
|
|
88
|
+
expect(lower).toContain("tree-shaped"); // 树形判据
|
|
89
|
+
expect(lower).toContain("self-contained"); // task 自包含
|
|
90
|
+
expect(lower).toContain("acceptance criteria"); // 独立验收
|
|
91
|
+
expect(lower).toContain("safety rail"); // 深度定位:10 是护栏非预算
|
|
92
|
+
});
|
|
93
|
+
|
|
81
94
|
it("保留 executionMode sequential 的 CRITICAL 说明", () => {
|
|
82
95
|
// sequential 是关键执行语义,删了会导致 LLM 误以为并行可用。
|
|
83
96
|
expect(DESCRIPTION).toMatch(/CRITICAL/i);
|
|
@@ -80,39 +80,41 @@ describe("renderSubagentCall — 拍平形态提取(regression for wave 3 flat
|
|
|
80
80
|
expect(out).toContain("fix-login");
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
-
it("
|
|
83
|
+
it("task 不再渲染为 preview 行(标题块只有单行)", () => {
|
|
84
84
|
const out = renderText(renderSubagentCall(
|
|
85
85
|
{ action: "start", agent: "coder", task: "Analyze the bug in parser", slug: "fix-parser" },
|
|
86
86
|
makeTheme() as never,
|
|
87
87
|
CTX,
|
|
88
88
|
));
|
|
89
|
-
// task
|
|
90
|
-
expect(out).toContain("Analyze the bug in parser");
|
|
89
|
+
// task 预览行已移除——task 内容不进对话流(完整 task 在 /subagents 详情可见)
|
|
90
|
+
expect(out).not.toContain("Analyze the bug in parser");
|
|
91
|
+
expect(out).not.toContain("\n");
|
|
91
92
|
});
|
|
92
93
|
|
|
93
|
-
it("task
|
|
94
|
+
it("task 含换行时不进渲染(无 preview 泄漏)", () => {
|
|
94
95
|
const out = renderText(renderSubagentCall(
|
|
95
96
|
{ action: "start", task: "first line\nsecond line", slug: "x" },
|
|
96
97
|
makeTheme() as never,
|
|
97
98
|
CTX,
|
|
98
99
|
));
|
|
99
|
-
expect(out).toContain("first line");
|
|
100
|
+
expect(out).not.toContain("first line");
|
|
100
101
|
expect(out).not.toContain("second line");
|
|
102
|
+
expect(out).not.toContain("\n");
|
|
101
103
|
});
|
|
102
104
|
|
|
103
105
|
// 关键回归:若有人把提取路径改回 args.startParam,这些顶层调用都会失败
|
|
104
|
-
// (agent/slug
|
|
106
|
+
// (agent/slug 取不到,全用默认值)。此测试用顶层数据形态锁住 flatten。
|
|
107
|
+
// task 不在断言里——task 已不渲染(上方用例锁定)。
|
|
105
108
|
it("REGRESSION: 顶层 args 形态完整提取(防止回退到 startParam envelope)", () => {
|
|
106
109
|
const out = renderText(renderSubagentCall(
|
|
107
110
|
{ action: "start", agent: "researcher", task: "search docs", slug: "search-docs" },
|
|
108
111
|
makeTheme() as never,
|
|
109
112
|
CTX,
|
|
110
113
|
));
|
|
111
|
-
//
|
|
114
|
+
// agent + slug 都应被提取(默认值 fallback 也能过单字段断言,但同时命中的
|
|
112
115
|
// 概率只有联合 fallback 才有——researcher/search-docs 都不是默认值)
|
|
113
116
|
expect(out).toContain("researcher");
|
|
114
117
|
expect(out).toContain("search-docs");
|
|
115
|
-
expect(out).toContain("search docs");
|
|
116
118
|
});
|
|
117
119
|
|
|
118
120
|
it("args 缺所有字段时不崩(最防御)", () => {
|