@zhushanwen/pi-subagent-workflow 8.5.0 → 8.6.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/package.json +7 -6
- package/src/execution/__tests__/bg-notify-render.test.ts +73 -0
- package/src/execution/__tests__/chat-engine-routing.test.ts +6 -2
- package/src/execution/__tests__/delivery-methods.test.ts +38 -1
- package/src/execution/__tests__/execute-options-mapper.test.ts +11 -0
- package/src/execution/__tests__/execution-record.test.ts +110 -0
- package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +171 -0
- package/src/execution/__tests__/format-schema-instruction.test.ts +63 -32
- package/src/execution/__tests__/helpers/spawn-mock.ts +4 -0
- package/src/execution/__tests__/index-session-start.test.ts +86 -7
- package/src/execution/__tests__/lifecycle-manager.test.ts +46 -0
- package/src/execution/__tests__/list-fields.test.ts +45 -14
- package/src/execution/__tests__/model-resolver.test.ts +57 -5
- package/src/execution/__tests__/notifier-flush.test.ts +64 -26
- package/src/execution/__tests__/notify-ledger.test.ts +826 -0
- package/src/execution/__tests__/output-collector.test.ts +299 -2
- package/src/execution/__tests__/rpc-mode.test.ts +1 -1
- package/src/execution/__tests__/run-spawn-edges.test.ts +44 -1
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +199 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +39 -0
- package/src/execution/__tests__/spawn-args.test.ts +37 -26
- package/src/execution/__tests__/start-sync-model-guard.test.ts +150 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +94 -1
- package/src/execution/__tests__/timeout-integration.test.ts +220 -2
- package/src/execution/__tests__/tool-action.test.ts +92 -1
- package/src/execution/agent-registry.ts +6 -0
- package/src/execution/argv-mirror.ts +5 -1
- package/src/execution/concurrency-pool.ts +1 -1
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +13 -0
- package/src/execution/engine/engines/zcode/zcode-engine.ts +11 -1
- package/src/execution/engine/types.ts +6 -1
- package/src/execution/execute-options-mapper.ts +8 -7
- package/src/execution/execution-record.ts +60 -1
- package/src/execution/lifecycle-manager.ts +23 -1
- package/src/execution/model-config-service.ts +16 -1
- package/src/execution/model-resolver.ts +31 -59
- package/src/execution/notifier.ts +105 -35
- package/src/execution/notify-ledger.ts +580 -0
- package/src/execution/output-collector.ts +143 -3
- package/src/execution/session-runner.ts +304 -71
- package/src/execution/subagent-service.ts +24 -2
- package/src/execution/subprocess-agent-runner.ts +14 -0
- package/src/execution/types.ts +68 -5
- package/src/execution/ui-request-queue.ts +14 -4
- package/src/index.ts +54 -1
- package/src/interface/__tests__/subagent-tool-path-guard.test.ts +157 -0
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +12 -0
- package/src/interface/bg-notify-render.ts +33 -12
- package/src/interface/helpers.ts +2 -2
- package/src/interface/subagent-actions.ts +26 -9
- package/src/interface/subagent-tool-schema.ts +156 -0
- package/src/interface/subagent-tool.ts +56 -125
- package/src/interface/subagents.ts +2 -2
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +16 -3
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +0 -6
- package/src/orchestration/__tests__/agent-call-stream.test.ts +0 -5
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +89 -4
- package/src/orchestration/__tests__/execute-agent-call.test.ts +137 -0
- package/src/orchestration/__tests__/jsonl-run-store-corrupt-entry.test.ts +150 -0
- package/src/orchestration/__tests__/jsonl-run-store-retention.test.ts +202 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +326 -3
- package/src/orchestration/__tests__/lifecycle.test.ts +41 -7
- package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +95 -0
- package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +57 -3
- package/src/orchestration/__tests__/skill-discovery.test.ts +44 -0
- package/src/orchestration/__tests__/worker-exit-without-result.test.ts +368 -0
- package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +43 -0
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +21 -2
- package/src/orchestration/agent-opts-resolver.ts +104 -23
- package/src/orchestration/error-recovery.ts +189 -33
- package/src/orchestration/execute-agent-call.ts +39 -0
- package/src/orchestration/jsonl-run-store.ts +121 -7
- package/src/orchestration/launcher.ts +60 -15
- package/src/orchestration/lifecycle.ts +10 -7
- package/src/orchestration/models/__tests__/budget.test.ts +1 -61
- package/src/orchestration/models/budget.ts +5 -35
- package/src/orchestration/models/run-runtime.ts +24 -9
- package/src/orchestration/models/types.ts +9 -0
- package/src/orchestration/script-lint.ts +1 -1
- package/src/orchestration/skill-discovery.ts +31 -8
- package/src/orchestration/worker-script-builder.ts +16 -3
- package/src/shared/__tests__/model-ref.test.ts +306 -0
- package/src/shared/__tests__/schema-jsonify.test.ts +1 -1
- package/src/shared/__tests__/timer-delay.test.ts +61 -0
- package/src/shared/model-ref.ts +286 -0
- package/src/shared/schema-env.ts +44 -0
- package/src/shared/schema-jsonify.ts +6 -4
- package/src/shared/timer-delay.ts +54 -0
- package/workflows/review-fix-loop-utils.cjs +9 -7
- package/workflows/review-fix-loop.js +20 -12
- package/src/orchestration/__tests__/concurrency-gate.test.ts +0 -125
- package/src/orchestration/concurrency-gate.ts +0 -69
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
} from "@xyz-agent/extension-protocol";
|
|
18
18
|
|
|
19
19
|
import { SLUG_MAX_LENGTH } from "../execution/execute-options-mapper.ts";
|
|
20
|
-
import { computeElapsedSeconds } from "../execution/execution-record.ts";
|
|
20
|
+
import { computeElapsedSeconds, projectOutcome } from "../execution/execution-record.ts";
|
|
21
21
|
import { isResumable } from "../execution/lifecycle-predicates.ts";
|
|
22
22
|
import type { ExecutionRecord } from "../execution/types.ts";
|
|
23
23
|
import type { ModelInfo } from "../execution/model-resolver.ts";
|
|
@@ -49,6 +49,9 @@ const MAX_LIST_LIMIT = 100;
|
|
|
49
49
|
/** background 启动提示文案(spec FR-3 bgResponse.message)。 */
|
|
50
50
|
const BG_MESSAGE = "detached, will notify on completion (auto-injected message, do not poll)";
|
|
51
51
|
|
|
52
|
+
/** 通知投递契约回显恒值(U1 预置,U2 账本兑现,见 BgResponse.notifyContract)。 */
|
|
53
|
+
const NOTIFY_CONTRACT = "ledger+at-least-once" as const;
|
|
54
|
+
|
|
52
55
|
/** subagentId(UUID)在 GUI header 的截断显示长度。 */
|
|
53
56
|
const SUBAGENT_ID_PREVIEW = 8;
|
|
54
57
|
|
|
@@ -79,7 +82,10 @@ export interface StartHandlerInput {
|
|
|
79
82
|
cwd?: string;
|
|
80
83
|
/** 可持续对话模式(true = chatMode,轮次完成进 idle 等续聊)。 */
|
|
81
84
|
conversation?: boolean;
|
|
82
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* 空闲超时毫秒数(仅 conversation 模式有意义,覆盖默认 5min)。
|
|
87
|
+
* 显式传 0/负数 = 禁用 idle GC(不挂 timer);不传走 env/默认优先级。
|
|
88
|
+
*/
|
|
83
89
|
idleTimeoutMs?: number;
|
|
84
90
|
/** 执行引擎(D4 三层路由第一层:本参数 > agent frontmatter engine > config defaultEngine)。 */
|
|
85
91
|
engine?: string;
|
|
@@ -92,6 +98,11 @@ export type StartHandlerResult = {
|
|
|
92
98
|
sessionFile: string | undefined;
|
|
93
99
|
/** 短标签,来自 record(handle.details.slug)。用于 result 行展示。 */
|
|
94
100
|
slug: string;
|
|
101
|
+
/**
|
|
102
|
+
* registry 全等回显(U1):handle.details.model = record.model = `${provider}/${id}`,
|
|
103
|
+
* 源头是 resolveModel 裁决放行的条目——通过校验 = 子进程必然按此名执行。
|
|
104
|
+
*/
|
|
105
|
+
model: string;
|
|
95
106
|
response: BgResponse;
|
|
96
107
|
};
|
|
97
108
|
|
|
@@ -154,9 +165,11 @@ export function mapExternalState(status: ExecutionStatus): ExternalState {
|
|
|
154
165
|
}
|
|
155
166
|
|
|
156
167
|
/** SubagentRecord → SubagentListItem(state 四态主字段 + status 调试字段,duration 实时计算)。
|
|
157
|
-
* [v4 A-6] 新增 parent/resumable
|
|
158
|
-
*
|
|
159
|
-
*
|
|
168
|
+
* [v4 A-6] 新增 parent/resumable:parent 从 record.parentRecordId 派生(配合 A-5 直接父
|
|
169
|
+
* 守卫),resumable 从 isResumable 派生(B-1「可续聊」对外表达)。
|
|
170
|
+
* [U3 C-outcome] 新增 outcome 一等终态语义(projectOutcome 唯一出口);closedReason
|
|
171
|
+
* 退出对外 JSON(保留为 record 内部诊断字段),对外成败判读收口到 outcome,消费方
|
|
172
|
+
* 零手写推导(三处同构 switch 已收敛删除)。
|
|
160
173
|
* agent 是 GUI/TUI list 共用的显示名——取 basename 短名(displayAgentName),
|
|
161
174
|
* 完整路径保留在 record.agent(数据层)。 */
|
|
162
175
|
export function recordToListItem(r: SubagentRecord): SubagentListItem {
|
|
@@ -173,7 +186,7 @@ export function recordToListItem(r: SubagentRecord): SubagentListItem {
|
|
|
173
186
|
sessionFile: r.sessionFile,
|
|
174
187
|
parent: r.parentRecordId,
|
|
175
188
|
resumable: isResumable(r),
|
|
176
|
-
|
|
189
|
+
outcome: projectOutcome(r),
|
|
177
190
|
};
|
|
178
191
|
}
|
|
179
192
|
|
|
@@ -232,10 +245,13 @@ export async function startHandler(
|
|
|
232
245
|
subagentId: handle.subagentId,
|
|
233
246
|
sessionFile: handle.sessionFile,
|
|
234
247
|
slug: handle.details.slug,
|
|
248
|
+
// [U1] registry 全等回显:record.model 由 resolved(裁决放行条目)拼接,原样透出。
|
|
249
|
+
model: handle.details.model,
|
|
235
250
|
response: {
|
|
236
251
|
status: "running",
|
|
237
252
|
mode: "background",
|
|
238
253
|
message: BG_MESSAGE,
|
|
254
|
+
notifyContract: NOTIFY_CONTRACT,
|
|
239
255
|
},
|
|
240
256
|
};
|
|
241
257
|
}
|
|
@@ -471,7 +487,8 @@ export function adapter(
|
|
|
471
487
|
const d = input.domain;
|
|
472
488
|
// MF-3(决策 10 细则 4):LLM content (text) 用 null 瘦身,防诱导 agent 用 read 绕过工具
|
|
473
489
|
// 直接读 session 文件。真实 sessionFile 仅 details 保留(供 GUI/程序化消费)。
|
|
474
|
-
|
|
490
|
+
// [U1] model 为 registry 全等回显(放行即全等)。
|
|
491
|
+
result = { action, subagentId: d.subagentId, sessionFile: null, slug: d.slug, model: d.model, bgResponse: d.response };
|
|
475
492
|
} else if (action === "list") {
|
|
476
493
|
result = { action, subagentId: null, sessionFile: null, listResponse: input.domain.response };
|
|
477
494
|
} else if (action === "cancel") {
|
|
@@ -491,7 +508,7 @@ export function adapter(
|
|
|
491
508
|
let detailsBase: SubagentToolResult = result;
|
|
492
509
|
if (action === "start") {
|
|
493
510
|
const d = input.domain;
|
|
494
|
-
detailsBase = { action: "start", subagentId: d.subagentId, sessionFile: d.sessionFile ?? null, slug: d.slug, bgResponse: d.response };
|
|
511
|
+
detailsBase = { action: "start", subagentId: d.subagentId, sessionFile: d.sessionFile ?? null, slug: d.slug, model: d.model, bgResponse: d.response };
|
|
495
512
|
}
|
|
496
513
|
|
|
497
514
|
// GUI 协议:RPC 模式下附加结构化渲染数据(union 各成员已声明 __gui__?,无需强转)
|
|
@@ -503,7 +520,7 @@ export function adapter(
|
|
|
503
520
|
// reminder 作为第二个 text block(独立追加,不污染 details/JSON schema)。
|
|
504
521
|
// 只有 list 触发——start 的 reminder 已在 BG_MESSAGE 里;cancel 无需。
|
|
505
522
|
const reminder = action === "list"
|
|
506
|
-
? "\n\nReminder: Subagent completion is auto-notified via injected message (
|
|
523
|
+
? "\n\nReminder: Subagent completion is auto-notified via auto-injected message (turn-triggering on idle). Do NOT poll in a loop — there is no poll action. Use action:'list' only when you concretely need state, then continue working or stop." // g4-allow: 契约文案——reminder 字符串描述自动注入通道(triggerTurn 单通道,U2/D5 无 deliverAs),非实际投递调用
|
|
507
524
|
: "";
|
|
508
525
|
|
|
509
526
|
return {
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// src/interface/subagent-tool-schema.ts
|
|
2
|
+
//
|
|
3
|
+
// `subagent` 工具的参数 schema 纯常量叶子(零运行时依赖,先例 shared/schema-env.ts)。
|
|
4
|
+
//
|
|
5
|
+
// 抽取自 subagent-tool.ts(跨包契约另一半):subagent-tool 依赖树沉重(pi SDK /
|
|
6
|
+
// handler / render 链),structured-output 侧的跨包契约测试若从它 import schema
|
|
7
|
+
// 会把整条依赖树拖进测试进程。本模块只含 schema 常量,运行时 import 仅
|
|
8
|
+
// typebox(Type 构造)与 pi-ai(StringEnum helper),为 structured-output 侧
|
|
9
|
+
//(及任何消费者)的跨包契约测试提供稳定 import 点。
|
|
10
|
+
//
|
|
11
|
+
// [跨包契约] structured-output 的 cross-package-contract.test.ts 经真实 typebox
|
|
12
|
+
// 编译本 schema 并断言 required/description/enum/pattern 存活——SW 自身测试环境
|
|
13
|
+
// 把 typebox alias 到 mock(丢 options),SO 侧测试以真实构造为对照基准。
|
|
14
|
+
//
|
|
15
|
+
// 层归属:Interface(工具 schema 的家)。SLUG_MAX_LENGTH 随 schema 迁入:
|
|
16
|
+
// 它的唯一语义就是 tool schema 的 maxLength(见原 execute-options-mapper 注释),
|
|
17
|
+
// execution 侧经 re-export 保持既有 import 路径不变。
|
|
18
|
+
|
|
19
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
20
|
+
import { type Static, Type } from "typebox";
|
|
21
|
+
|
|
22
|
+
import { THINKING_ORDER } from "../shared/model-ref.ts";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* slug 最大长度(字符)。subagent/workflow 创建时 slug 超过此值会被截断。
|
|
26
|
+
* subagent/workflow tool schema 的 maxLength 引用此常量(单一真相,勿再硬编码)。
|
|
27
|
+
* 历史值 20 偏紧——描述性 slug 如 "audit-structured-output"(23)/"fix-subagent-wf-tools"(21)
|
|
28
|
+
* 会撞上限,放宽到 35 兼顾「短到能塞进 TUI 标题行」与「容纳合理描述性 kebab-case 名」。
|
|
29
|
+
*/
|
|
30
|
+
export const SLUG_MAX_LENGTH = 35;
|
|
31
|
+
|
|
32
|
+
// Params schema(跨包契约测试的真实 typebox 校验入口)。
|
|
33
|
+
//
|
|
34
|
+
// action:"start" 的 13 字段(task/slug/agent/model/...)拍平在顶层,不再用 startParam
|
|
35
|
+
// 嵌套容器包。原因:弱模型(GLM/DeepSeek)信任 schema 结构信号 > 文本信号,经常省略
|
|
36
|
+
// startParam 嵌套层把 task/slug 直接平铺到顶层导致调用失败。拍平后 schema 结构与模型
|
|
37
|
+
// 的自然倾向一致,消除这层误用。task/slug 必填性由 startHandler runtime 校验(flat
|
|
38
|
+
// JSON Schema 无法表达「action 条件必填」)。
|
|
39
|
+
//
|
|
40
|
+
// TODO(long-term, option-A): listParam/cancelParam 仍标 Optional 也是 flat JSON Schema
|
|
41
|
+
// 表达「action 分发条件必填」的妥协——长期方案是拆成 3 个独立 tool
|
|
42
|
+
// (subagent_start / subagent_list / subagent_cancel),让每个 tool 的 schema 真实
|
|
43
|
+
// 反映必填性。勿在此基础上继续堆 action 条件逻辑——要加就拆 tool。
|
|
44
|
+
export const SubagentParams = Type.Object({
|
|
45
|
+
action: StringEnum(["start", "list", "cancel", "message", "close"], {
|
|
46
|
+
description: "Operation: 'start' runs a subagent, 'list' shows subagents, 'cancel' stops a background subagent, 'message' sends a follow-up to a running subagent (one-shot subagents are auto-upgraded to conversation mode on first message), 'close' ends a running subagent (conversation-mode or one-shot).",
|
|
47
|
+
}),
|
|
48
|
+
// ── action:"start" fields (flattened to top level). task/slug REQUIRED for start. ──
|
|
49
|
+
// Missing/empty task or slug throws at runtime (startHandler).
|
|
50
|
+
// (flat JSON Schema can't express conditional requirement — see file-level TODO.)
|
|
51
|
+
task: Type.Optional(Type.String({
|
|
52
|
+
description: "REQUIRED for action:'start'. The task for the subagent to execute. Throws if missing or whitespace-only.",
|
|
53
|
+
})),
|
|
54
|
+
slug: Type.Optional(Type.String({
|
|
55
|
+
description:
|
|
56
|
+
"REQUIRED for action:'start'. Short label (≤35 chars) for this subagent, e.g. 'fix-login', 'extract-urls'. " +
|
|
57
|
+
"Shown in TUI to distinguish concurrent subagents.",
|
|
58
|
+
maxLength: SLUG_MAX_LENGTH,
|
|
59
|
+
})),
|
|
60
|
+
agent: Type.Optional(Type.String({
|
|
61
|
+
description: 'Agent ref: absolute path to the agent .md file (use <location> from <available_subagents>). If omitted, defaults to "general-purpose" — a generic agent that inherits the main agent\'s model and project context. Do not invent names — only use paths from the injected list.',
|
|
62
|
+
})),
|
|
63
|
+
model: Type.Optional(Type.String({
|
|
64
|
+
description: 'Model override in "provider/modelId" format. CASE-SENSITIVE: the string must equal a registry entry exactly, including letter case (e.g. "zai-coding-cn/GLM-5.3-Flash", NOT "zai-coding-cn/glm-5.3-flash"). A non-exact match is rejected immediately with "Did you mean" suggestions — retry with the exact suggested string; the system never auto-corrects your input. Resolution order (top wins): (1) this param, (2) agent .md frontmatter model, (3) the main agent\'s current model (zero-config default). An explicit model (param or frontmatter) that is missing or unauthorized THROWS — there is no silent fallback to the main model. Omit this param to inherit the main model.',
|
|
65
|
+
})),
|
|
66
|
+
thinkingLevel: Type.Optional(StringEnum(THINKING_ORDER, {
|
|
67
|
+
description: "Thinking depth override (derived from THINKING_ORDER SSOT, includes 'max'). Omit to default to the model's highest available level (not the main agent's level).",
|
|
68
|
+
})),
|
|
69
|
+
skillPath: Type.Optional(Type.String({
|
|
70
|
+
description:
|
|
71
|
+
"Absolute path to a skill directory, injected into the subagent's pi process via --skill " +
|
|
72
|
+
"(e.g. a path under .agents/skills/ already resolved for the caller). Must be an absolute path; " +
|
|
73
|
+
"'..' traversal segments are rejected.",
|
|
74
|
+
pattern: "^/",
|
|
75
|
+
})),
|
|
76
|
+
appendSystemPrompt: Type.Optional(Type.Array(Type.String())),
|
|
77
|
+
schema: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
78
|
+
maxTurns: Type.Optional(Type.Number({
|
|
79
|
+
description: "Turn limit. The subagent is terminated via SIGTERM after maxTurns turn_end events + graceTurns of slack. There is no graceful wrap-up message — the process is killed. 0 or omitted = unlimited.",
|
|
80
|
+
})),
|
|
81
|
+
graceTurns: Type.Optional(Type.Number({
|
|
82
|
+
description: "Extra turns allowed after maxTurns is reached before SIGTERM (default 2). Only meaningful when maxTurns is set.",
|
|
83
|
+
})),
|
|
84
|
+
fork: Type.Optional(Type.Boolean({
|
|
85
|
+
description: "Fork mode: inherit the parent's conversation context. When true, the subagent receives the parent's session file via --fork and builds a branched conversation (it sees prior turns/messages). The subagent still runs in a separate spawned child process (process isolation) — fork is about context inheritance, not process sharing; independent of worktree (file-system isolation, see worktree param). When to use: only when the task extends from the parent and genuinely needs key information from the parent's conversation history that a self-contained task prompt cannot carry — most tasks a plain prompt can describe do NOT need fork, so keep false by default and enable only when the user explicitly asks or the task truly depends on seeing prior turns. Caveat: fork drags in the parent's dispatch records and unrelated task context, polluting the subagent (it cannot tell 'context meant for me' from 'parent dispatching me'); when state lives in an external store the subagent can query (e.g., cw handoff), prefer that over fork.",
|
|
86
|
+
})),
|
|
87
|
+
worktree: Type.Optional(Type.Boolean({
|
|
88
|
+
description: "Worktree isolation: run the subagent in a dedicated git worktree, providing file-system level isolation from the parent session (prevents concurrent file-write conflicts). Independent of fork — worktree may be combined with fork:false (file isolation does not require context inheritance). When to use: parallel development scenarios where multiple agents write files concurrently and need isolated working directories (each gets its own checkout; merge later); leave false for single-agent or read-only tasks.",
|
|
89
|
+
})),
|
|
90
|
+
cwd: Type.Optional(Type.String({
|
|
91
|
+
description: 'Override the working directory for the subagent execution. Must be an absolute path (no "~" shorthand, no relative paths); ".." segments are rejected. Defaults to the parent session\'s cwd.',
|
|
92
|
+
pattern: "^/",
|
|
93
|
+
})),
|
|
94
|
+
conversation: Type.Optional(Type.Boolean({
|
|
95
|
+
description:
|
|
96
|
+
"Enable continuous chat with this subagent. When true, the subagent stays available after each reply — you can send follow-up messages (action:'message') and it keeps the full conversation context across rounds, with no need to re-spawn or re-explain. " +
|
|
97
|
+
"\nUse conversation:true for: multi-round collaboration (iterative review-fix loops, back-and-forth refinement), any task where you expect to send follow-up messages after the initial result. " +
|
|
98
|
+
"\nOmit (or false) for: one-shot tasks — single exploration, lookup, file read, code generation that needs no follow-up. The subagent runs once, notifies on completion, and is cleaned up automatically (default). " +
|
|
99
|
+
"\nFor long-interval collaboration (each round spaced >5min apart), set conversation:true AND increase idleTimeoutMs to avoid premature timeout. " +
|
|
100
|
+
"Cost: a conversation-mode subagent holds resources (memory, and a worktree if enabled) until you explicitly end it with action:'close'. Always close when done.",
|
|
101
|
+
})),
|
|
102
|
+
idleTimeoutMs: Type.Optional(Type.Number({
|
|
103
|
+
description:
|
|
104
|
+
"Idle timeout in milliseconds for conversation-mode subagents. Controls how long an idle subagent (between rounds) stays alive before automatic cleanup. " +
|
|
105
|
+
"Default: 300000 (5min). Override for long-interval collaboration where each round is spaced >5min apart. " +
|
|
106
|
+
"Pass 0 or a negative value to DISABLE idle cleanup entirely (subagent stays alive until explicitly closed). " +
|
|
107
|
+
"Only meaningful with conversation:true; ignored for one-shot subagents.",
|
|
108
|
+
})),
|
|
109
|
+
engine: Type.Optional(StringEnum(["pi", "zcode"], {
|
|
110
|
+
description:
|
|
111
|
+
"Execution engine for this subagent. Omit to inherit the global config. " +
|
|
112
|
+
"Three-layer priority: this parameter > agent .md frontmatter engine > config.json defaultEngine. " +
|
|
113
|
+
"Non-pi engines do not support conversation/fork/worktree (rejected before the subagent is created).",
|
|
114
|
+
})),
|
|
115
|
+
// action:"list" → listParam OPTIONAL (all fields optional, defaults apply). Ignored by other actions.
|
|
116
|
+
listParam: Type.Optional(Type.Object({
|
|
117
|
+
includeFinished: Type.Optional(Type.Boolean({
|
|
118
|
+
description: "Include finished (done/failed/cancelled) records. Default false (running only).",
|
|
119
|
+
})),
|
|
120
|
+
limit: Type.Optional(Type.Number({
|
|
121
|
+
description: "Max items to return. Default 20, clamped to [1, 100].",
|
|
122
|
+
})),
|
|
123
|
+
})),
|
|
124
|
+
// action:"cancel" → cancelParam.subagentId REQUIRED. Throws if missing. Ignored by other actions.
|
|
125
|
+
cancelParam: Type.Optional(Type.Object({
|
|
126
|
+
subagentId: Type.String({
|
|
127
|
+
description: "REQUIRED for action:'cancel'. The subagentId to cancel. Throws if missing. Only background subagents can be cancelled.",
|
|
128
|
+
}),
|
|
129
|
+
})),
|
|
130
|
+
// action:"message" → messageParam.subagentId + text REQUIRED. Any RUNNING subagent works —
|
|
131
|
+
// one-shot subagents are auto-upgraded to conversation mode on first message (SP-5); ended ones throw.
|
|
132
|
+
messageParam: Type.Optional(Type.Object({
|
|
133
|
+
subagentId: Type.String({
|
|
134
|
+
description: "REQUIRED for action:'message'. The subagentId to message (any running subagent; a one-shot subagent is auto-upgraded to conversation mode on first message, so you may also message one-shot subagents that are still running).",
|
|
135
|
+
}),
|
|
136
|
+
text: Type.String({
|
|
137
|
+
description: "REQUIRED for action:'message'. The message to send. Whitespace-only throws.",
|
|
138
|
+
}),
|
|
139
|
+
interrupt: Type.Optional(Type.Boolean({
|
|
140
|
+
description: "If true, interrupt the subagent's current work immediately (in-progress output stops, it switches to your new message). If false (default), the message is queued and processed after the current round completes. When the subagent is idle (between rounds), interrupt has no effect — the message always starts a new round.",
|
|
141
|
+
})),
|
|
142
|
+
})),
|
|
143
|
+
// action:"close" → closeParam.subagentId REQUIRED. Ends a running subagent (conversation-mode
|
|
144
|
+
// or one-shot — closeSubagent behavior split covers both).
|
|
145
|
+
closeParam: Type.Optional(Type.Object({
|
|
146
|
+
subagentId: Type.String({
|
|
147
|
+
description: "REQUIRED for action:'close'. The subagentId to close (any running subagent, conversation-mode or one-shot).",
|
|
148
|
+
}),
|
|
149
|
+
force: Type.Optional(Type.Boolean({
|
|
150
|
+
description: "If true, terminate immediately even if mid-round (in-progress work is lost). If false (default), let the current round finish, then close. When idle, the subagent closes immediately regardless.",
|
|
151
|
+
})),
|
|
152
|
+
})),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
/** Params schema 的 Static 投影(消费方经 `Static<typeof SubagentParams>` 使用,见 subagent-tool.ts)。 */
|
|
156
|
+
export type SubagentParamsStatic = Static<typeof SubagentParams>;
|
|
@@ -9,19 +9,19 @@
|
|
|
9
9
|
// Theme、ExtensionContext)会触发 TS2307 误报(probe5d/5f 验证)。
|
|
10
10
|
// 抽到顶层后参数类型由 alias 提供,绕过该 quirk。
|
|
11
11
|
|
|
12
|
+
import { isAbsolute } from "node:path";
|
|
13
|
+
|
|
12
14
|
import type { Component } from "@earendil-works/pi-tui";
|
|
13
|
-
import { StringEnum } from "@earendil-works/pi-ai";
|
|
14
15
|
import type { AgentToolResult, ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
15
16
|
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
16
|
-
import {
|
|
17
|
+
import type { Static } from "typebox";
|
|
17
18
|
|
|
18
|
-
import { SLUG_MAX_LENGTH } from "../execution/execute-options-mapper.ts";
|
|
19
|
-
import { THINKING_ORDER } from "../execution/model-resolver.ts";
|
|
20
19
|
import { getSubagentService } from "../execution/subagent-service.ts";
|
|
21
20
|
import type { SubagentToolResult } from "../execution/types.ts";
|
|
22
21
|
import { extractAgentName } from "./format.ts";
|
|
23
22
|
import { toGuiCtx } from "./gui-mappers.ts";
|
|
24
23
|
import { adapter, cancelHandler, closeHandler, listHandler, messageHandler, startHandler } from "./subagent-actions.ts";
|
|
24
|
+
import { SubagentParams } from "./subagent-tool-schema.ts";
|
|
25
25
|
import { type RenderContext,renderSubagentCall, renderSubagentResult } from "./tool-render.ts";
|
|
26
26
|
|
|
27
27
|
// ============================================================
|
|
@@ -54,128 +54,14 @@ type SubagentRenderResultCb = (
|
|
|
54
54
|
ctx: RenderContext,
|
|
55
55
|
) => Component;
|
|
56
56
|
|
|
57
|
-
// ============================================================
|
|
58
|
-
// Params schema
|
|
59
|
-
// ============================================================
|
|
60
|
-
|
|
61
|
-
// Params schema(模块内消费,未导出)。
|
|
62
|
-
//
|
|
63
|
-
// action:"start" 的 13 字段(task/slug/agent/model/...)拍平在顶层,不再用 startParam
|
|
64
|
-
// 嵌套容器包。原因:弱模型(GLM/DeepSeek)信任 schema 结构信号 > 文本信号,经常省略
|
|
65
|
-
// startParam 嵌套层把 task/slug 直接平铺到顶层导致调用失败。拍平后 schema 结构与模型
|
|
66
|
-
// 的自然倾向一致,消除这层误用。task/slug 必填性由 startHandler runtime 校验(flat
|
|
67
|
-
// JSON Schema 无法表达「action 条件必填」)。
|
|
68
|
-
//
|
|
69
|
-
// TODO(long-term, option-A): listParam/cancelParam 仍标 Optional 也是 flat JSON Schema
|
|
70
|
-
// 表达「action 分发条件必填」的妥协——长期方案是拆成 3 个独立 tool
|
|
71
|
-
// (subagent_start / subagent_list / subagent_cancel),让每个 tool 的 schema 真实
|
|
72
|
-
// 反映必填性。勿在此基础上继续堆 action 条件逻辑——要加就拆 tool。
|
|
73
|
-
const SubagentParams = Type.Object({
|
|
74
|
-
action: StringEnum(["start", "list", "cancel", "message", "close"], {
|
|
75
|
-
description: "Operation: 'start' runs a subagent, 'list' shows subagents, 'cancel' stops a background subagent, 'message' sends a follow-up to a running subagent (one-shot subagents are auto-upgraded to conversation mode on first message), 'close' ends a running subagent (conversation-mode or one-shot).",
|
|
76
|
-
}),
|
|
77
|
-
// ── action:"start" fields (flattened to top level). task/slug REQUIRED for start. ──
|
|
78
|
-
// Missing/empty task or slug throws at runtime (startHandler).
|
|
79
|
-
// (flat JSON Schema can't express conditional requirement — see file-level TODO.)
|
|
80
|
-
task: Type.Optional(Type.String({
|
|
81
|
-
description: "REQUIRED for action:'start'. The task for the subagent to execute. Throws if missing or whitespace-only.",
|
|
82
|
-
})),
|
|
83
|
-
slug: Type.Optional(Type.String({
|
|
84
|
-
description:
|
|
85
|
-
"REQUIRED for action:'start'. Short label (≤35 chars) for this subagent, e.g. 'fix-login', 'extract-urls'. " +
|
|
86
|
-
"Shown in TUI to distinguish concurrent subagents.",
|
|
87
|
-
maxLength: SLUG_MAX_LENGTH,
|
|
88
|
-
})),
|
|
89
|
-
agent: Type.Optional(Type.String({
|
|
90
|
-
description: 'Agent ref: absolute path to the agent .md file (use <location> from <available_subagents>). If omitted, defaults to "general-purpose" — a generic agent that inherits the main agent\'s model and project context. Do not invent names — only use paths from the injected list.',
|
|
91
|
-
})),
|
|
92
|
-
model: Type.Optional(Type.String({
|
|
93
|
-
description: 'Model override in "provider/modelId" format. Resolution order (top wins): (1) this param, (2) agent .md frontmatter model, (3) the main agent\'s current model (zero-config default). An explicit model (param or frontmatter) that is missing or unauthorized THROWS — there is no silent fallback to the main model. Omit this param to inherit the main model.',
|
|
94
|
-
})),
|
|
95
|
-
thinkingLevel: Type.Optional(StringEnum(THINKING_ORDER, {
|
|
96
|
-
description: "Thinking depth override (derived from THINKING_ORDER SSOT, includes 'max'). Omit to default to the model's highest available level (not the main agent's level).",
|
|
97
|
-
})),
|
|
98
|
-
skillPath: Type.Optional(Type.String()),
|
|
99
|
-
appendSystemPrompt: Type.Optional(Type.Array(Type.String())),
|
|
100
|
-
schema: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
101
|
-
maxTurns: Type.Optional(Type.Number({
|
|
102
|
-
description: "Turn limit. The subagent is terminated via SIGTERM after maxTurns turn_end events + graceTurns of slack. There is no graceful wrap-up message — the process is killed. 0 or omitted = unlimited.",
|
|
103
|
-
})),
|
|
104
|
-
graceTurns: Type.Optional(Type.Number({
|
|
105
|
-
description: "Extra turns allowed after maxTurns is reached before SIGTERM (default 2). Only meaningful when maxTurns is set.",
|
|
106
|
-
})),
|
|
107
|
-
fork: Type.Optional(Type.Boolean({
|
|
108
|
-
description: "Fork mode: inherit the parent's conversation context. When true, the subagent receives the parent's session file via --fork and builds a branched conversation (it sees prior turns/messages). The subagent still runs in a separate spawned child process (process isolation) — fork is about context inheritance, not process sharing; independent of worktree (file-system isolation, see worktree param). When to use: only when the task extends from the parent and genuinely needs key information from the parent's conversation history that a self-contained task prompt cannot carry — most tasks a plain prompt can describe do NOT need fork, so keep false by default and enable only when the user explicitly asks or the task truly depends on seeing prior turns. Caveat: fork drags in the parent's dispatch records and unrelated task context, polluting the subagent (it cannot tell 'context meant for me' from 'parent dispatching me'); when state lives in an external store the subagent can query (e.g., cw handoff), prefer that over fork.",
|
|
109
|
-
})),
|
|
110
|
-
worktree: Type.Optional(Type.Boolean({
|
|
111
|
-
description: "Worktree isolation: run the subagent in a dedicated git worktree, providing file-system level isolation from the parent session (prevents concurrent file-write conflicts). Independent of fork — worktree may be combined with fork:false (file isolation does not require context inheritance). When to use: parallel development scenarios where multiple agents write files concurrently and need isolated working directories (each gets its own checkout; merge later); leave false for single-agent or read-only tasks.",
|
|
112
|
-
})),
|
|
113
|
-
cwd: Type.Optional(Type.String({
|
|
114
|
-
description: 'Override the working directory for the subagent execution. Must be an absolute path. Defaults to the parent session\'s cwd.',
|
|
115
|
-
})),
|
|
116
|
-
conversation: Type.Optional(Type.Boolean({
|
|
117
|
-
description:
|
|
118
|
-
"Enable continuous chat with this subagent. When true, the subagent stays available after each reply — you can send follow-up messages (action:'message') and it keeps the full conversation context across rounds, with no need to re-spawn or re-explain. " +
|
|
119
|
-
"\nUse conversation:true for: multi-round collaboration (iterative review-fix loops, back-and-forth refinement), any task where you expect to send follow-up messages after the initial result. " +
|
|
120
|
-
"\nOmit (or false) for: one-shot tasks — single exploration, lookup, file read, code generation that needs no follow-up. The subagent runs once, notifies on completion, and is cleaned up automatically (default). " +
|
|
121
|
-
"\nFor long-interval collaboration (each round spaced >5min apart), set conversation:true AND increase idleTimeoutMs to avoid premature timeout. " +
|
|
122
|
-
"Cost: a conversation-mode subagent holds resources (memory, and a worktree if enabled) until you explicitly end it with action:'close'. Always close when done.",
|
|
123
|
-
})),
|
|
124
|
-
idleTimeoutMs: Type.Optional(Type.Number({
|
|
125
|
-
description:
|
|
126
|
-
"Idle timeout in milliseconds for conversation-mode subagents. Controls how long an idle subagent (between rounds) stays alive before automatic cleanup. " +
|
|
127
|
-
"Default: 300000 (5min). Override for long-interval collaboration where each round is spaced >5min apart. " +
|
|
128
|
-
"Only meaningful with conversation:true; ignored for one-shot subagents.",
|
|
129
|
-
})),
|
|
130
|
-
engine: Type.Optional(StringEnum(["pi", "zcode"], {
|
|
131
|
-
description:
|
|
132
|
-
"Execution engine for this subagent. Omit to inherit the global config. " +
|
|
133
|
-
"Three-layer priority: this parameter > agent .md frontmatter engine > config.json defaultEngine. " +
|
|
134
|
-
"Non-pi engines do not support conversation/fork/worktree (rejected before the subagent is created).",
|
|
135
|
-
})),
|
|
136
|
-
// action:"list" → listParam OPTIONAL (all fields optional, defaults apply). Ignored by other actions.
|
|
137
|
-
listParam: Type.Optional(Type.Object({
|
|
138
|
-
includeFinished: Type.Optional(Type.Boolean({
|
|
139
|
-
description: "Include finished (done/failed/cancelled) records. Default false (running only).",
|
|
140
|
-
})),
|
|
141
|
-
limit: Type.Optional(Type.Number({
|
|
142
|
-
description: "Max items to return. Default 20, clamped to [1, 100].",
|
|
143
|
-
})),
|
|
144
|
-
})),
|
|
145
|
-
// action:"cancel" → cancelParam.subagentId REQUIRED. Throws if missing. Ignored by other actions.
|
|
146
|
-
cancelParam: Type.Optional(Type.Object({
|
|
147
|
-
subagentId: Type.String({
|
|
148
|
-
description: "REQUIRED for action:'cancel'. The subagentId to cancel. Throws if missing. Only background subagents can be cancelled.",
|
|
149
|
-
}),
|
|
150
|
-
})),
|
|
151
|
-
// action:"message" → messageParam.subagentId + text REQUIRED. Any RUNNING subagent works —
|
|
152
|
-
// one-shot subagents are auto-upgraded to conversation mode on first message (SP-5); ended ones throw.
|
|
153
|
-
messageParam: Type.Optional(Type.Object({
|
|
154
|
-
subagentId: Type.String({
|
|
155
|
-
description: "REQUIRED for action:'message'. The subagentId to message (any running subagent; a one-shot subagent is auto-upgraded to conversation mode on first message, so you may also message one-shot subagents that are still running).",
|
|
156
|
-
}),
|
|
157
|
-
text: Type.String({
|
|
158
|
-
description: "REQUIRED for action:'message'. The message to send. Whitespace-only throws.",
|
|
159
|
-
}),
|
|
160
|
-
interrupt: Type.Optional(Type.Boolean({
|
|
161
|
-
description: "If true, interrupt the subagent's current work immediately (in-progress output stops, it switches to your new message). If false (default), the message is queued and processed after the current round completes. When the subagent is idle (between rounds), interrupt has no effect — the message always starts a new round.",
|
|
162
|
-
})),
|
|
163
|
-
})),
|
|
164
|
-
// action:"close" → closeParam.subagentId REQUIRED. Ends a running subagent (conversation-mode
|
|
165
|
-
// or one-shot — closeSubagent behavior split covers both).
|
|
166
|
-
closeParam: Type.Optional(Type.Object({
|
|
167
|
-
subagentId: Type.String({
|
|
168
|
-
description: "REQUIRED for action:'close'. The subagentId to close (any running subagent, conversation-mode or one-shot).",
|
|
169
|
-
}),
|
|
170
|
-
force: Type.Optional(Type.Boolean({
|
|
171
|
-
description: "If true, terminate immediately even if mid-round (in-progress work is lost). If false (default), let the current round finish, then close. When idle, the subagent closes immediately regardless.",
|
|
172
|
-
})),
|
|
173
|
-
})),
|
|
174
|
-
});
|
|
175
|
-
|
|
176
57
|
// ============================================================
|
|
177
58
|
// renderCall 预解析 helper
|
|
178
59
|
// ============================================================
|
|
60
|
+
//
|
|
61
|
+
// Params schema(SubagentParams)定义在 ./subagent-tool-schema.ts 纯常量叶子:
|
|
62
|
+
// subagent-tool 依赖树沉重,structured-output 侧跨包契约测试需要零依赖 import
|
|
63
|
+
// schema 常量经真实 typebox 编译校验(SW 自身 vitest 把 typebox alias 到 mock,
|
|
64
|
+
// 丢 options——required/description 断言必须以真实构造为基准)。
|
|
179
65
|
|
|
180
66
|
// extractAgentName 已上移到 ../tui/format.ts 共享(tool-render / subagent-tool 复用)。
|
|
181
67
|
|
|
@@ -199,6 +85,47 @@ function isModelOverrideObj(a: unknown): a is { model?: unknown; thinkingLevel?:
|
|
|
199
85
|
return typeof a === "object" && a !== null;
|
|
200
86
|
}
|
|
201
87
|
|
|
88
|
+
/**
|
|
89
|
+
* start 路径类参数(skillPath / cwd)运行时守卫:绝对路径 + 禁 `..` 穿越。
|
|
90
|
+
*
|
|
91
|
+
* 校验链事实(pi 0.84.1 实装,登记 PS-20):pi agent-loop 对注册 typebox schema
|
|
92
|
+
* 有运行时强校验——agent-loop.js:403-404 在 beforeToolCall / execute 之前调
|
|
93
|
+
* validateToolArguments(pi-ai validation.js:247:Value.Convert :249 + Compile :210
|
|
94
|
+
* + Check :265,失败 throw `Validation failed for tool` :272-273)→ catch 走
|
|
95
|
+
* immediate error(agent-loop.js:445-451),execute 不被调用。schema 的
|
|
96
|
+
* pattern(skillPath/cwd `^/`)/ maxLength(slug 35)是运行时强制而非仅模型可见
|
|
97
|
+
* 契约;tool-definition-wrapper.js:11 只原样透传 params,校验发生在上游 agent-loop 层。
|
|
98
|
+
*
|
|
99
|
+
* 工具层守卫定位 = defense-in-depth + schema 表达力缺口,非「pi 无校验」:
|
|
100
|
+
* - action 条件必填(task/slug 仅 action=start 必填)flat JSON Schema 表达不了,
|
|
101
|
+
* 只能在 startHandler 运行时校验
|
|
102
|
+
* - `..` 穿越段拒绝超出 pattern 能力(`^/` 放行 "/a/../b"),穿越语义只能在
|
|
103
|
+
* 工具层判——与 slug maxLength 双闸同理(schema 强制之上再叠 handler 兜底)
|
|
104
|
+
*
|
|
105
|
+
* 规则:
|
|
106
|
+
* - 绝对路径(isAbsolute;`~` 缩写不是绝对路径,拒绝并指引展开后重试——
|
|
107
|
+
* 下游 session-runner 把该值原样拼进 `--skill <path>` / spawn cwd,不展开 `~`)
|
|
108
|
+
* - 任意 `..` 路径段拒绝(按 /[\\/] 分段判断而非子串——"a..b" 不是穿越):
|
|
109
|
+
* 相对穿越让子进程读到意图外的目录
|
|
110
|
+
*
|
|
111
|
+
* 校验失败 immediate throw(与 action 枚举守卫同风格):pi 只对 execute throw 置
|
|
112
|
+
* isError:true,错误文案原样进 toolResult。
|
|
113
|
+
*/
|
|
114
|
+
function assertSafeStartPath(value: string, param: "skillPath" | "cwd"): void {
|
|
115
|
+
if (value.split(/[\\/]/).includes("..")) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`${param} must not contain '..' path segments (got "${value}"). ` +
|
|
118
|
+
`Pass a normalized absolute path — traversal segments are rejected.`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
if (!isAbsolute(value)) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`${param} must be an absolute path (got "${value}"). ` +
|
|
124
|
+
`Expand '~' yourself and pass the full path, e.g. "/Users/me/project".`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
202
129
|
/** 从 unknown args 安全提取 model/thinkingLevel override(传给 resolveModel)。
|
|
203
130
|
* 拍平后 args 已是顶层平铺结构(model/thinkingLevel 直接在 args 上)。 */
|
|
204
131
|
function extractModelOverride(args: unknown): { model?: string; thinkingLevel?: string } | undefined {
|
|
@@ -243,7 +170,7 @@ action:"list" before action:"start" — a reusable running subagent may exist; c
|
|
|
243
170
|
|
|
244
171
|
\`\`\`
|
|
245
172
|
{"action":"start","task":"<your task>","slug":"<kebab-case>"}
|
|
246
|
-
{"action":"start","task":"...","slug":"fix-login","agent":"coder","model":"anthropic/claude-3.5-sonnet","fork":true}
|
|
173
|
+
{"action":"start","task":"...","slug":"fix-login","agent":"/abs/path/coder.md","model":"anthropic/claude-3.5-sonnet","fork":true}
|
|
247
174
|
{"action":"start","task":"review iteratively","slug":"review","conversation":true}
|
|
248
175
|
{"action":"message","messageParam":{"subagentId":"sa-550e8400","text":"now also handle the empty-list case"}}
|
|
249
176
|
{"action":"message","messageParam":{"subagentId":"sa-550e8400","text":"stop, switch direction to X","interrupt":true}}
|
|
@@ -277,7 +204,7 @@ When to use:
|
|
|
277
204
|
- ✅ Long-interval rounds (>5min apart) → conversation:true + idleTimeoutMs increased
|
|
278
205
|
- ❌ Single exploration/lookup → default (one-shot)
|
|
279
206
|
|
|
280
|
-
idleTimeoutMs: per-subagent idle timeout (default 300000 / 5min). Env XYZ_SUBAGENT_IDLE_TIMEOUT_MS sets the global default; per-call param takes precedence.
|
|
207
|
+
idleTimeoutMs: per-subagent idle timeout (default 300000 / 5min). Env XYZ_SUBAGENT_IDLE_TIMEOUT_MS sets the global default; per-call param takes precedence. Pass 0 or a negative value to disable idle cleanup entirely.
|
|
281
208
|
|
|
282
209
|
## You cannot
|
|
283
210
|
|
|
@@ -381,6 +308,10 @@ const executeSubagent: SubagentExecuteCb = async (
|
|
|
381
308
|
}
|
|
382
309
|
switch (params.action) {
|
|
383
310
|
case "start":
|
|
311
|
+
// 路径类参数守卫(三通道对称审查 + MF-13):skillPath/cwd 在进入 handler 前
|
|
312
|
+
// immediate throw,不产生半启动 record(与 action 枚举守卫同风格)。
|
|
313
|
+
if (params.skillPath !== undefined) assertSafeStartPath(params.skillPath, "skillPath");
|
|
314
|
+
if (params.cwd !== undefined) assertSafeStartPath(params.cwd, "cwd");
|
|
384
315
|
// 拍平后直接传顶层 params(StartHandlerInput 是 SubagentExecuteParams 子集,
|
|
385
316
|
// action/listParam/cancelParam 被忽略;task/slug 必填性由 startHandler 校验)。
|
|
386
317
|
return adapter({ action: "start", domain: await startHandler(service, params, signal, _ctx?.model) }, toGuiCtx(_ctx));
|
|
@@ -45,7 +45,7 @@ export interface SubagentDirectiveDetails {
|
|
|
45
45
|
* 定向消息注入正在运行的主 agent LLM turn,违反「不经主 agent LLM 直达 subagent」。
|
|
46
46
|
* 故按调用时刻的权威 streaming 状态(ctx.isIdle(),与 sendCustomMessage 内部
|
|
47
47
|
* isStreaming 判据精确互补,含 agent_end 后 retry/continuation 窗口)分流:
|
|
48
|
-
* - streaming(isMainAgentIdle=false):传 { deliverAs: "nextTurn" }
|
|
48
|
+
* - streaming(isMainAgentIdle=false):传 { deliverAs: "nextTurn" }——消息入(g4-allow: 交互注入——GUI 定向消息留痕分流,非结果语义通知)
|
|
49
49
|
* pi 内存 _pendingNextTurnMessages 队列,下个 turn 注入主 agent 上下文;不打断、
|
|
50
50
|
* 不 steer 当前 turn。注意:该队列不落 entry,留痕延迟到下个 turn
|
|
51
51
|
* - 非 streaming(isMainAgentIdle=true):不传 options——立即 append entry 留痕
|
|
@@ -66,7 +66,7 @@ function emitSubagentDirective(
|
|
|
66
66
|
display: false,
|
|
67
67
|
details,
|
|
68
68
|
},
|
|
69
|
-
isMainAgentIdle ? undefined : { deliverAs: "nextTurn" },
|
|
69
|
+
isMainAgentIdle ? undefined : { deliverAs: "nextTurn" }, // g4-allow: 交互注入——/subagents GUI 定向消息留痕,非结果语义(C-ext-19 禁令边界,见 emitSubagentDirective JSDoc)
|
|
70
70
|
);
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -12,7 +12,7 @@ const { parentPort: _parentPort, workerData: _workerData } = require("node:worke
|
|
|
12
12
|
const _workerLogs = [];
|
|
13
13
|
// IF6(#12): known agent() fields — hoisted to module scope, built once per worker
|
|
14
14
|
// (was rebuilt inside agent() on every call; field set is call-invariant).
|
|
15
|
-
const _KNOWN_FIELDS = new Set(["prompt", "description", "schema", "model", "scene", "label", "task", "agent", "phase", "skill", "timeoutMs", "cwd", "fork", "worktree", "returnMeta", "thinkingLevel", "engine"]);
|
|
15
|
+
const _KNOWN_FIELDS = new Set(["prompt", "description", "schema", "model", "scene", "label", "task", "agent", "phase", "skill", "timeoutMs", "maxTurns", "cwd", "fork", "worktree", "returnMeta", "thinkingLevel", "engine"]);
|
|
16
16
|
function _pushWorkerLog(level, args) {
|
|
17
17
|
try { _workerLogs.push({ level, message: args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ") }); } catch (e) { /* swallow */ }
|
|
18
18
|
}
|
|
@@ -98,6 +98,8 @@ function _safePost(msg, context) {
|
|
|
98
98
|
// 让 parallel() 下的脚本容错循环(parseResult → null → skip)自然接管。
|
|
99
99
|
// 错误原因已由主线程 executeAgentCall → trace.update(result.error) 保留在 trace/TUI,
|
|
100
100
|
// 不丢失。失败 resolve 为空字符串是既定容错策略。
|
|
101
|
+
// [MF-4] schema 模式下失败时 agent() 仍 resolve(content 回退、不 throw)——
|
|
102
|
+
// 需要检查错误时请用 returnMeta:true(resolve 值含 error 字段)。
|
|
101
103
|
// parsedOutput: validated data object from structured-output execute().
|
|
102
104
|
// Fallback to content (raw text) when no schema was requested or on error.
|
|
103
105
|
// W2 改动 9(b):returnMeta===true 时 resolve {value,sessionFile,worktreePath,error,usage,durationMs,sessionId}
|
|
@@ -153,6 +155,9 @@ function _safePost(msg, context) {
|
|
|
153
155
|
scene: (secondArg && typeof secondArg === "object" && secondArg.scene) || undefined,
|
|
154
156
|
phase: (secondArg && typeof secondArg === "object" && secondArg.phase) || undefined,
|
|
155
157
|
thinkingLevel: (secondArg && typeof secondArg === "object" && secondArg.thinkingLevel) || $THINKING_LEVEL,
|
|
158
|
+
// step 级 turn 上限(turn limiter;显式 0/负 = 显式不限,压过 spawn watchdog env 兑底,SP-6)
|
|
159
|
+
// ?? 语义保真:仅 null/undefined 归 undefined(走 env 兑底),显式 0 保留(U5 参数 > env)
|
|
160
|
+
maxTurns: (secondArg && typeof secondArg === "object" ? secondArg.maxTurns : undefined) ?? undefined,
|
|
156
161
|
// P4 D9③:step 级 engine 显式指定(仅限必须某引擎独有能力的场景)
|
|
157
162
|
engine: (secondArg && typeof secondArg === "object" && secondArg.engine) || undefined,
|
|
158
163
|
};
|
|
@@ -171,6 +176,7 @@ function _safePost(msg, context) {
|
|
|
171
176
|
scene: firstArg.scene,
|
|
172
177
|
skill: firstArg.skill,
|
|
173
178
|
timeoutMs: firstArg.timeoutMs,
|
|
179
|
+
maxTurns: firstArg.maxTurns,
|
|
174
180
|
cwd: firstArg.cwd,
|
|
175
181
|
fork: firstArg.fork,
|
|
176
182
|
worktree: firstArg.worktree,
|
|
@@ -193,7 +199,7 @@ function _safePost(msg, context) {
|
|
|
193
199
|
// Validate known agent() fields to catch API misuse early (_KNOWN_FIELDS at module scope)
|
|
194
200
|
const _unknownFields = Object.keys(opts).filter((k) => !_KNOWN_FIELDS.has(k));
|
|
195
201
|
if (_unknownFields.length > 0) {
|
|
196
|
-
_pushWorkerLog("warn", ["[workflow] agent() received unknown fields: " + _unknownFields.join(", ") + ". Known fields: prompt, description, schema, model, scene, label, task, agent, phase, skill, timeoutMs, cwd, fork, worktree, returnMeta, thinkingLevel, engine"]);
|
|
202
|
+
_pushWorkerLog("warn", ["[workflow] agent() received unknown fields: " + _unknownFields.join(", ") + ". Known fields: prompt, description, schema, model, scene, label, task, agent, phase, skill, timeoutMs, maxTurns, cwd, fork, worktree, returnMeta, thinkingLevel, engine"]);
|
|
197
203
|
}
|
|
198
204
|
|
|
199
205
|
const callId = _callIdCounter;
|
|
@@ -329,7 +335,14 @@ module.exports = { execute: async (ctx) => ctx.agent("finalize") };
|
|
|
329
335
|
}
|
|
330
336
|
})().then((result) => {
|
|
331
337
|
const runId = (_workerData.args && typeof _workerData.args === "object" && _workerData.args._runId) || "";
|
|
332
|
-
_safePost({ type: "return", runId, result, workerLogs: _workerLogs }, "return")
|
|
338
|
+
if (!_safePost({ type: "return", runId, result, workerLogs: _workerLogs }, "return")) {
|
|
339
|
+
// [F1] return 值不可克隆(含 function/Symbol/循环引用 → DataCloneError)时 _safePost
|
|
340
|
+
// 只能记日志返回 false——若不补救,worker 将静默 exit(0),主线程收不到任何终态消息,
|
|
341
|
+
// run 永久 running、runAndWait 悬挂。回发可克隆的 error 消息(DataCloneError 详情
|
|
342
|
+
// 已由 _safePost 记入 _workerLogs 随消息带回),让主线程 handleScriptError 接管,
|
|
343
|
+
// run 经既有重试矩阵收敛到终态 failed。
|
|
344
|
+
_safePost({ type: "error", runId, error: "Workflow return value could not be delivered (structured-clone failed) — see workerLogs for the postMessage error", workerLogs: _workerLogs }, "error");
|
|
345
|
+
}
|
|
333
346
|
}).catch((err) => {
|
|
334
347
|
const runId = (_workerData.args && typeof _workerData.args === "object" && _workerData.args._runId) || "";
|
|
335
348
|
_safePost({ type: "error", runId, error: err.message || String(err), workerLogs: _workerLogs }, "error");
|
|
@@ -48,12 +48,6 @@ function makeRunningRun(runId: string): WorkflowRun {
|
|
|
48
48
|
runtime: {
|
|
49
49
|
controller,
|
|
50
50
|
worker: { postMessage: vi.fn() },
|
|
51
|
-
gate: {
|
|
52
|
-
// 直接 await fn():executeAgentCall 内 runner.run reject 会沿 withSlot → 外层 .catch
|
|
53
|
-
withSlot: vi.fn(async (fn: () => Promise<void>, _signal: AbortSignal) => {
|
|
54
|
-
await fn();
|
|
55
|
-
}),
|
|
56
|
-
},
|
|
57
51
|
},
|
|
58
52
|
transition: vi.fn(),
|
|
59
53
|
replaceRuntime: vi.fn(),
|
|
@@ -46,11 +46,6 @@ function makeRunningRun(runId: string): WorkflowRun {
|
|
|
46
46
|
runtime: {
|
|
47
47
|
controller,
|
|
48
48
|
worker: { postMessage: vi.fn() },
|
|
49
|
-
gate: {
|
|
50
|
-
withSlot: vi.fn(async (fn: () => Promise<void>, _signal: AbortSignal) => {
|
|
51
|
-
await fn();
|
|
52
|
-
}),
|
|
53
|
-
},
|
|
54
49
|
},
|
|
55
50
|
transition: vi.fn(),
|
|
56
51
|
replaceRuntime: vi.fn(),
|