@zhushanwen/pi-subagent-workflow 8.14.0 → 8.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/package.json +7 -7
- package/relay/relay.mjs +9 -9
- package/skills/subagent-ext-config/SKILL.md +5 -5
- package/src/host/__tests__/inflight-reporter.test.ts +1 -1
- package/src/host/__tests__/pi-host.test.ts +2 -2
- package/src/host/inflight-reporter.ts +3 -3
- package/src/host/pi-host.ts +5 -5
- package/src/index.ts +2 -2
- package/src/injectors/resource-list-injector.ts +1 -1
- package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +12 -9
- package/src/interface/command-actions.ts +1 -1
- package/src/interface/commands.ts +2 -2
- package/src/interface/format.ts +1 -1
- package/src/interface/gui-mappers.ts +3 -3
- package/src/interface/helpers.ts +1 -1
- package/src/interface/subagent-actions.ts +1 -1
- package/src/interface/subagent-tool-schema.ts +2 -2
- package/src/interface/subagent-tool.ts +3 -3
- package/src/interface/subagents.ts +5 -5
- package/src/interface/tool-workflow-script.ts +1 -1
- package/src/interface/tool-workflow.ts +9 -3
- package/src/jsonl-run-store.ts +4 -4
- package/src/session-lifecycle.ts +1 -1
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ Pi 的 subagent + workflow 合并包:任务委派 + 多 agent 编排(chain /
|
|
|
33
33
|
|
|
34
34
|
orchestrator 是纯协调器角色:拆解任务 → 委派 subagent → 汇总结果,自身不做执行类工作。orchestrator agent 自身也可递归委派子 orchestrator,实现分层任务拆解(深度受 `Depth: N/10` 护栏保护)。
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
**工具约束(C1/D-5)**:内置模板不携带 `tools:` frontmatter 白名单,subagent 不以 `--tools` 白名单启动——工具约束回归宿主默认工具面,orchestrator 靠角色职责(职责边界段)约束自身只做协调。想要白名单的用户在 `<workspace>/.agents/agents/` 放同名 `.md` 覆写(project 级源稳定遮蔽内置,是唯一逃生门),或沿用 pi CLI 的 `--tools` 白名单(临时验证):
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
39
|
pi --tools todo,goal_control,workflow,subagent,ask_user
|
|
@@ -57,14 +57,14 @@ Workflow run 是一次性执行,状态机两态:`running → done`(`done`
|
|
|
57
57
|
|
|
58
58
|
Runs are one-shot: there is no pause/resume — to stop a run early use abort; for a fresh result start a new run.
|
|
59
59
|
|
|
60
|
-
- **abort 是唯一的提前停止方式**:`{"action":"abort","runId":"<id>"}`(可选 `"error":"<reason>"
|
|
60
|
+
- **abort 是唯一的提前停止方式**:`{"action":"abort","runId":"<id>"}`(可选 `"error":"<reason>"`)。不存在 pause/resume action,调用会被 pi schema 校验拒绝(`Validation failed for tool "workflow"`);`/workflows pause|resume <id>` 返回 removed 提示
|
|
61
61
|
- **session 切换/关闭时**,所有 running run 当即作废转 `done,failed`(state.error 为 `Session switched: run terminated` / `Session shutdown: run terminated`),已投入的 token 作废;需要结果就重新 run
|
|
62
62
|
- **快照格式 `wf-run-v2`**(status 两态、无 `pausedAt`);旧 `wf-run-v1` 文件加载时静默跳过
|
|
63
63
|
- **worker 崩溃自动重建重试**(默认 3 次):重建时在飞 agent 调用被清除重跑,已完成的调用保留 replay 缓存,不重复消耗 token
|
|
64
64
|
|
|
65
65
|
## 性能:sessions-index.json 持久化索引
|
|
66
66
|
|
|
67
|
-
冷启动首扫的 identity 探测结论持久化为 `<enc>/sessions-index.json`(stat 戳自校验、tmp(pid)+rename 原子写、60s 节流、损坏/版本不符静默回退全量探测),真实目录(1744 jsonl / 671MB)实测冷扫描中位数 972.8ms → 80.6ms(12.1x,预算 ≤300ms)。可复现验收脚本:`bench/cold-scan.bench.ts`(冷扫描计时 + 输出等价断言)、`bench/concurrent-scan.bench.ts`(3 实例并发 +
|
|
67
|
+
冷启动首扫的 identity 探测结论持久化为 `<enc>/sessions-index.json`(stat 戳自校验、tmp(pid)+rename 原子写、60s 节流、损坏/版本不符静默回退全量探测),真实目录(1744 jsonl / 671MB)实测冷扫描中位数 972.8ms → 80.6ms(12.1x,预算 ≤300ms)。可复现验收脚本:`bench/cold-scan.bench.ts`(冷扫描计时 + 输出等价断言)、`bench/concurrent-scan.bench.ts`(3 实例并发 + 随机变异四判定)。
|
|
68
68
|
|
|
69
69
|
## 安装
|
|
70
70
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-subagent-workflow",
|
|
3
|
-
"version": "8.14.
|
|
3
|
+
"version": "8.14.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"description": "Unified subagent execution and multi-agent workflow orchestration for Pi — spawned-process agent runtime with sync/background modes, stateful workflow management with persistence, state machine, and execution tracing.",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"src/injectors/",
|
|
30
30
|
"src/interface/"
|
|
31
31
|
],
|
|
32
|
-
"
|
|
32
|
+
"taiji": {
|
|
33
33
|
"role": "universal",
|
|
34
34
|
"subagentEngines": [
|
|
35
35
|
"pi",
|
|
@@ -54,13 +54,13 @@
|
|
|
54
54
|
]
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@
|
|
58
|
-
"@xyz-agent/session-delivery": "0.4.1",
|
|
57
|
+
"@zhushanwen/extension-protocol": "0.11.0",
|
|
59
58
|
"@zhushanwen/pi-ext-guards": "0.4.0",
|
|
60
|
-
"@zhushanwen/
|
|
59
|
+
"@zhushanwen/session-delivery": "0.10.0",
|
|
60
|
+
"@zhushanwen/pi-extension-logger": "0.6.0",
|
|
61
|
+
"@zhushanwen/subagent-core": "0.10.2",
|
|
61
62
|
"@zhushanwen/pi-subagent-cli": "0.4.0",
|
|
62
|
-
"@zhushanwen/zcode-subagent-cli": "0.3.1"
|
|
63
|
-
"@zhushanwen/pi-extension-logger": "0.6.0"
|
|
63
|
+
"@zhushanwen/zcode-subagent-cli": "0.3.1"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
66
|
"@earendil-works/pi-ai": "^0.84.4",
|
package/relay/relay.mjs
CHANGED
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
*
|
|
15
15
|
* | relay-env.ts 常量 | 本文件常量 | 值 |
|
|
16
16
|
* |---------------------------|---------------------------|-----------------------------|
|
|
17
|
-
* | RELAY_ENV_SOCKET | RELAY_ENV_SOCKET | '
|
|
18
|
-
* | RELAY_ENV_SESSION_ID | RELAY_ENV_SESSION_ID | '
|
|
19
|
-
* | RELAY_ENV_RECORD_ID | RELAY_ENV_RECORD_ID | '
|
|
17
|
+
* | RELAY_ENV_SOCKET | RELAY_ENV_SOCKET | 'TAIJI_SUBAGENT_RELAY_SOCKET' |
|
|
18
|
+
* | RELAY_ENV_SESSION_ID | RELAY_ENV_SESSION_ID | 'TAIJI_SUBAGENT_RELAY_SESSION_ID' |
|
|
19
|
+
* | RELAY_ENV_RECORD_ID | RELAY_ENV_RECORD_ID | 'TAIJI_SUBAGENT_RELAY_RECORD_ID' |
|
|
20
20
|
* | RELAY_PROTOCOL_VERSION | RELAY_PROTOCOL_VERSION | 1 |
|
|
21
21
|
* | RELAY_EXIT_CODES.* | RELAY_EXIT_CODES.* | 10/11/12/13 |
|
|
22
22
|
*
|
|
@@ -46,9 +46,9 @@ import net from "node:net";
|
|
|
46
46
|
import os from "node:os";
|
|
47
47
|
|
|
48
48
|
// ---- 镜像常量(与 relay-env.ts 逐字一致,见头部对照表)----
|
|
49
|
-
const RELAY_ENV_SOCKET = "
|
|
50
|
-
const RELAY_ENV_SESSION_ID = "
|
|
51
|
-
const RELAY_ENV_RECORD_ID = "
|
|
49
|
+
const RELAY_ENV_SOCKET = "TAIJI_SUBAGENT_RELAY_SOCKET";
|
|
50
|
+
const RELAY_ENV_SESSION_ID = "TAIJI_SUBAGENT_RELAY_SESSION_ID";
|
|
51
|
+
const RELAY_ENV_RECORD_ID = "TAIJI_SUBAGENT_RELAY_RECORD_ID";
|
|
52
52
|
|
|
53
53
|
const RELAY_PROTOCOL_VERSION = 1;
|
|
54
54
|
|
|
@@ -137,7 +137,7 @@ function connectWithRetry(socketPath) {
|
|
|
137
137
|
fail(
|
|
138
138
|
RELAY_EXIT_CODES.SOCKET_UNREACHABLE,
|
|
139
139
|
`relay socket unreachable after retry: ${socketPath} (${err.message}). ` +
|
|
140
|
-
`Recovery: 重试任务;持续失败请重启
|
|
140
|
+
`Recovery: 重试任务;持续失败请重启 taiji(runtime 未运行或已重启)`,
|
|
141
141
|
);
|
|
142
142
|
}
|
|
143
143
|
});
|
|
@@ -146,7 +146,7 @@ function connectWithRetry(socketPath) {
|
|
|
146
146
|
fail(
|
|
147
147
|
RELAY_EXIT_CODES.SOCKET_CLOSED,
|
|
148
148
|
"relay socket closed by runtime(生命线断,可能是 runtime 崩溃/重启). " +
|
|
149
|
-
"Recovery: 重启
|
|
149
|
+
"Recovery: 重启 taiji 后重试任务",
|
|
150
150
|
);
|
|
151
151
|
}
|
|
152
152
|
});
|
|
@@ -205,7 +205,7 @@ function handleLine(line) {
|
|
|
205
205
|
RELAY_EXIT_CODES.VERSION_MISMATCH,
|
|
206
206
|
`relay protocol version mismatch: agent v${RELAY_PROTOCOL_VERSION}, ` +
|
|
207
207
|
`runtime supports ${JSON.stringify(frame.supported)}. ` +
|
|
208
|
-
"Recovery: 升级
|
|
208
|
+
"Recovery: 升级 taiji(runtime 与代理资产同包分发,版本不一致意味着安装损坏,重装应用)",
|
|
209
209
|
);
|
|
210
210
|
return;
|
|
211
211
|
}
|
|
@@ -16,10 +16,10 @@ config.json 位于 pi agent 目录下的 `subagents/config.json`,随环境不
|
|
|
16
16
|
| 环境 | 路径 |
|
|
17
17
|
|------|------|
|
|
18
18
|
| 独立 pi CLI | `~/.pi/agent/subagents/config.json` |
|
|
19
|
-
|
|
|
20
|
-
|
|
|
19
|
+
| taiji dev | `~/.taiji-dev/pi/agent/subagents/config.json` |
|
|
20
|
+
| taiji prod | `~/.taiji/pi/agent/subagents/config.json` |
|
|
21
21
|
|
|
22
|
-
**动态推导(推荐)**:agentDir 由 pi 核心 `getAgentDir()` 决定(读 `PI_CODING_AGENT_DIR`,默认 `~/.pi/agent`);
|
|
22
|
+
**动态推导(推荐)**:agentDir 由 pi 核心 `getAgentDir()` 决定(读 `PI_CODING_AGENT_DIR`,默认 `~/.pi/agent`);taiji 通过 `TAIJI_AGENT_DATA_DIR` 隔离数据目录。排查时先查这两个 env 变量组合出实际路径(`<agentDir>/subagents/config.json`),不要假设单一环境——写错环境的配置文件改了也不生效。
|
|
23
23
|
|
|
24
24
|
文件不存在 / JSON 解析失败 / 字段缺失时全部回默认配置,不报错。旧版 `categories` / `fallback` / `yoloByDefault` 等字段读取时忽略(模型解析已退化为「主 agent model 优先」)。
|
|
25
25
|
|
|
@@ -86,8 +86,8 @@ config.json 位于 pi agent 目录下的 `subagents/config.json`,随环境不
|
|
|
86
86
|
|
|
87
87
|
1. **新建 session**(必须——当前 session 不重读配置)。
|
|
88
88
|
2. 让主 agent 派一个 subagent(例:用 `subagents` 工具发个简单任务)。
|
|
89
|
-
3.
|
|
90
|
-
4. journal 落点 `~/.
|
|
89
|
+
3. taiji 侧边栏 **Agents tab** 看该项最左的引擎 icon(pi / zcode)——这是统一验证面。
|
|
90
|
+
4. journal 落点 `~/.taiji-dev/engines/<engineId>/` **仅适用非 pi 引擎**(zcode 分支建 journal);pi 分支不建 journal,pi 任务以 icon 为验证面。
|
|
91
91
|
|
|
92
92
|
## 常见错误排查
|
|
93
93
|
|
|
@@ -31,7 +31,7 @@ vi.mock("@zhushanwen/pi-extension-logger", () => extensionLoggerMock);
|
|
|
31
31
|
vi.mock("@zhushanwen/subagent-core", () => ({ getInFlightSnapshot: mockSnapshot }));
|
|
32
32
|
|
|
33
33
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
34
|
-
import { INFLIGHT_REPORT_ACK, SUBAGENT_INFLIGHT_MARKER } from "@
|
|
34
|
+
import { INFLIGHT_REPORT_ACK, SUBAGENT_INFLIGHT_MARKER } from "@zhushanwen/extension-protocol";
|
|
35
35
|
|
|
36
36
|
import { createInFlightReporter } from "../inflight-reporter.ts";
|
|
37
37
|
|
|
@@ -41,12 +41,12 @@ const sessionDeliveryMock = vi.hoisted(() => ({
|
|
|
41
41
|
vi.mock("@earendil-works/pi-coding-agent", () => piCodingAgentMock);
|
|
42
42
|
vi.mock("@zhushanwen/pi-extension-logger", () => extensionLoggerMock);
|
|
43
43
|
vi.mock("@zhushanwen/pi-pending-notifications", () => pendingNotificationsMock);
|
|
44
|
-
vi.mock("@
|
|
44
|
+
vi.mock("@zhushanwen/session-delivery", () => sessionDeliveryMock);
|
|
45
45
|
|
|
46
46
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
47
47
|
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
48
48
|
import { countActiveFromEntries } from "@zhushanwen/pi-pending-notifications";
|
|
49
|
-
import { createDelivery } from "@
|
|
49
|
+
import { createDelivery } from "@zhushanwen/session-delivery";
|
|
50
50
|
|
|
51
51
|
import { createPiHostServices, createPiNotifyDomainPorts } from "../pi-host.ts";
|
|
52
52
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// ② 事件产生点在 core、上报出口在壳层——本文件即出口(core 零 pi SDK);
|
|
16
16
|
// ③ marker 路由不广播前端是 u7b(runtime 侧)的约束,本文件不涉及。
|
|
17
17
|
//
|
|
18
|
-
// 环境门控(2026-09-13 oe-audit + 裸 TUI 闪框事故):上报的消费方是
|
|
18
|
+
// 环境门控(2026-09-13 oe-audit + 裸 TUI 闪框事故):上报的消费方是 taiji
|
|
19
19
|
// runtime 的 event-adapter marker 路由,而 runtime spawn pi 恒为 --mode rpc——
|
|
20
20
|
// ctx.mode !== 'rpc' 即无拦截方(裸 pi TUI 下 marker select 会弹真框超时,2026-09-12
|
|
21
21
|
// 实测无限闪框),此时不启动上报(ask-user channel-handler 的 mode 二值判定同款
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
// (30min 有界),不丢 errs-safe 兜底。
|
|
38
38
|
|
|
39
39
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
40
|
-
import { SUBAGENT_INFLIGHT_MARKER, callMarkerRpc, isInFlightReportAck } from "@
|
|
40
|
+
import { SUBAGENT_INFLIGHT_MARKER, callMarkerRpc, isInFlightReportAck } from "@zhushanwen/extension-protocol";
|
|
41
41
|
import { getInFlightSnapshot } from "@zhushanwen/subagent-core";
|
|
42
42
|
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
43
43
|
import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
|
|
@@ -51,7 +51,7 @@ const SELECT_TIMEOUT_MS = 2_000;
|
|
|
51
51
|
const RETRY_DELAY_MS = 2_000;
|
|
52
52
|
|
|
53
53
|
/** 累计失败放弃上限(对齐 plugin-bridge MAX_SYNC_ATTEMPTS:60s 窗口覆盖 attach 竞态,
|
|
54
|
-
* 有界防 rpc-but-非-
|
|
54
|
+
* 有界防 rpc-but-非-taiji orchestrator 场景的永久空转)。 */
|
|
55
55
|
const MAX_REPORT_ATTEMPTS = 30;
|
|
56
56
|
|
|
57
57
|
/** 在途上报器(组合根 index.ts 持有;per-factory 实例,session_start/shutdown 驱动)。 */
|
package/src/host/pi-host.ts
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
//
|
|
6
6
|
// 本文件属壳侧(shell),不进 core 切面——对 pi SDK 与 pi 宿主协作件
|
|
7
7
|
// (@earendil-works/pi-coding-agent / @zhushanwen/pi-extension-logger /
|
|
8
|
-
// @zhushanwen/pi-pending-notifications / @
|
|
8
|
+
// @zhushanwen/pi-pending-notifications / @zhushanwen/session-delivery)的运行时
|
|
9
9
|
// import 收敛在此层,core 闭包(D9 守卫对象)不得出现这些包。
|
|
10
10
|
//
|
|
11
11
|
// 端口语义:
|
|
12
12
|
// - dataRoot / discoveryRoots 每次调用现取 getAgentDir(),禁止模块级缓存:
|
|
13
|
-
// getAgentDir 尊重 PI_CODING_AGENT_DIR 实例隔离(
|
|
13
|
+
// getAgentDir 尊重 PI_CODING_AGENT_DIR 实例隔离(taiji 按 session dir
|
|
14
14
|
// 隔离 pi 实例),缓存会把后续切换实例的进程钉死在首个 agentDir。
|
|
15
15
|
// - discoveryRoots 的根清单/顺序/source 标签与 shared/resource-discovery.ts
|
|
16
16
|
// buildScanTargets、orchestration/skill-discovery.ts resolveSkillPath 的现推导
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// - countActiveFromEntries 适配:pi 侧真函数返回 CountActiveResult 对象,core
|
|
23
23
|
// 端口契约是 number(core 消费面只读 .count,notify-ports.ts 契约注释)——
|
|
24
24
|
// foundation 单元登记给本单元的适配责任。
|
|
25
|
-
// - createDelivery 透传:@
|
|
25
|
+
// - createDelivery 透传:@zhushanwen/session-delivery 的 createDelivery 与 core
|
|
26
26
|
// 的 Delivery* 结构化类型逐字段结构兼容(DeliveryHandle 的 sendChecked/depth
|
|
27
27
|
// 是结构超集成员,多不碍兼容)——结构兼容由本注入点 typecheck 守护,上游签名
|
|
28
28
|
// 漂移即红(notify-ports.ts「闭包红线」段)。
|
|
@@ -34,7 +34,7 @@ import { fileURLToPath } from "node:url";
|
|
|
34
34
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
35
35
|
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
36
36
|
import { countActiveFromEntries } from "@zhushanwen/pi-pending-notifications";
|
|
37
|
-
import { createDelivery } from "@
|
|
37
|
+
import { createDelivery } from "@zhushanwen/session-delivery";
|
|
38
38
|
|
|
39
39
|
import type { DiscoveryRoot, HostServices } from "@zhushanwen/subagent-core";
|
|
40
40
|
import type { LogLevel } from "@zhushanwen/subagent-core";
|
|
@@ -120,7 +120,7 @@ function skillRoots(): DiscoveryRoot[] {
|
|
|
120
120
|
|
|
121
121
|
/**
|
|
122
122
|
* 引擎包发现根(W4 L1 第二通道,设计 §3.4)。打包态主通道 = env
|
|
123
|
-
*
|
|
123
|
+
* TAIJI_AGENT_ENGINE_ROOTS(W9 注入),此处承载 pi 宿主的常规安装路径:
|
|
124
124
|
* 1. 宿主包自身 node_modules——引擎包(<engine>-subagent-cli)作为本包
|
|
125
125
|
* dependencies 安装位(W9「扩展 package.json 声明引擎包为 dependencies」);
|
|
126
126
|
* 2. <agentDir>/npm/node_modules——pi npm 安装位(org 分组二层布局由扫描器
|
package/src/index.ts
CHANGED
|
@@ -163,7 +163,7 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
163
163
|
setInFlightListener(inflightReporter.onInFlightChanged);
|
|
164
164
|
|
|
165
165
|
// [U7b] 引擎列表在 extension 模块加载时即同步 engines.json(不等 session_start——
|
|
166
|
-
// 用户体验拍板 2026-08-25:
|
|
166
|
+
// 用户体验拍板 2026-08-25:taiji 打开后激活任意 session 的第一时间(含 TUI 等价
|
|
167
167
|
// 场景)GUI 引擎选择器就该有数据;session_start 处保留幂等重写兜底 jiti 双路径/
|
|
168
168
|
// 模块重载场景的刷新)。
|
|
169
169
|
syncEnginesFile(getAgentDir());
|
|
@@ -495,7 +495,7 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
495
495
|
//
|
|
496
496
|
// - SIGTERM:pi 各 mode(rpc/interactive/print)自带 SIGTERM handler 负责退出编排,
|
|
497
497
|
// 本 extension 的 handler 只做收割 + 设 exitCode(不 re-raise、不抢 pi 的退出语义;
|
|
498
|
-
//
|
|
498
|
+
// taiji 桌面 supervisor 用 SIGTERM 杀 pi 走这条路)。
|
|
499
499
|
// - SIGINT:pi 本体不注册常规 SIGINT handler(interactive/print/rpc 均 SIGTERM only),
|
|
500
500
|
// 依赖 Node 默认终止。本 extension 注册 listener 即取消默认终止——若只设 exitCode,
|
|
501
501
|
// 本地 pi CLI 的 Ctrl-C 杀不死进程(TUI/stdin/agent loop 仍在事件循环)。故收割
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* subagent / workflow 两个资源清单 injector 的同构骨架单实现(原两文件逐字同构的
|
|
5
5
|
* 缓存对 / 唯一写点 / 发现函数 / 三 handler 收敛于此,改 fallback 策略只改一处):
|
|
6
|
-
* - 缓存对:entries + 渲染快照。per-process = per-session——
|
|
6
|
+
* - 缓存对:entries + 渲染快照。per-process = per-session——taiji session-pool
|
|
7
7
|
* 模型下每 pi 子进程 = 一 session = 独立扩展实例,闭包级缓存天然 per-session 隔离
|
|
8
8
|
* (split mode 多 session 各自独立进程)。渲染快照与数据缓存同步更新:before_agent_start
|
|
9
9
|
* 每个 turn 都要注入,format(escapeXml 多趟正则 × 全部字段)在数据不变时输出完全
|
|
@@ -135,21 +135,24 @@ describe("C5③ run 内置名(fake registry)", () => {
|
|
|
135
135
|
expect(vi.mocked(runWorkflow)).not.toHaveBeenCalled();
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
-
it("两者都 miss →
|
|
138
|
+
it("两者都 miss → 报错含建议清单且逐条附绝对路径 location(按名失败后的自救指引)", async () => {
|
|
139
139
|
const registry = {
|
|
140
140
|
get: vi.fn().mockResolvedValue(undefined),
|
|
141
141
|
getPath: vi.fn().mockResolvedValue(undefined),
|
|
142
142
|
loadAll: vi.fn().mockResolvedValue([makeScript("chain", "/builtin/workflows/chain.js")]),
|
|
143
143
|
};
|
|
144
|
-
await
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
144
|
+
const err = await actionRun(
|
|
145
|
+
{ action: "run", name: "no-such" } as never,
|
|
146
|
+
makeDeps(registry) as never,
|
|
147
|
+
undefined,
|
|
148
|
+
).catch((e: unknown) => e as Error);
|
|
149
|
+
// 逐条 location:模型按清单里的名字重试注定再 throw(8.6.0 getPath-only 时代
|
|
150
|
+
// 实测失败形态)——摘要必须直接给出可用的绝对路径,失败一次即自救。
|
|
151
|
+
expect(err.message).toContain(
|
|
152
|
+
"Workflow 'no-such' not found. Available (name — use the absolute location path as 'name' when the bare name is rejected):",
|
|
152
153
|
);
|
|
154
|
+
expect(err.message).toContain(" - chain: chain workflow");
|
|
155
|
+
expect(err.message).toContain(" location: /builtin/workflows/chain.js");
|
|
153
156
|
});
|
|
154
157
|
});
|
|
155
158
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* command-actions — RPC 模式 slash command action 解析纯函数。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* taiji GUI 通过 `client.prompt("/subagents cancel <id>")` 等触发生命周期操作,
|
|
5
5
|
* 不经 LLM(pi 的 _tryExecuteExtensionCommand 在 agent loop 前短路)。command handler
|
|
6
6
|
* 在 RPC 模式下用这两个函数解析 action 字符串,分发到对应 service/lifecycle 调用。
|
|
7
7
|
*
|
|
@@ -96,7 +96,7 @@ export function registerWorkflowsCommand(
|
|
|
96
96
|
return null;
|
|
97
97
|
},
|
|
98
98
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
99
|
-
// ── RPC 模式(
|
|
99
|
+
// ── RPC 模式(taiji GUI):解析 lifecycle action 直接执行,不打开 TUI ──
|
|
100
100
|
// hasUI 在 TUI 和 RPC 都为 true,不能用于区分;用 ctx.mode === "rpc" 判定 GUI 通道。
|
|
101
101
|
if (ctx.mode === "rpc") {
|
|
102
102
|
await handleRpcMode(parseWorkflowRpcCommand(args), ctx, deps);
|
|
@@ -125,7 +125,7 @@ export function registerWorkflowsCommand(
|
|
|
125
125
|
// ── Helpers ──────────────────────────────────────────────────
|
|
126
126
|
|
|
127
127
|
/**
|
|
128
|
-
* RPC 模式分支(
|
|
128
|
+
* RPC 模式分支(taiji GUI):解析后的 lifecycle action 直接执行,不打开 TUI。
|
|
129
129
|
*
|
|
130
130
|
* 各 action 语义:
|
|
131
131
|
* - abort → 调 lifecycle abortRun,成功/失败均 notify(不向上抛)
|
package/src/interface/format.ts
CHANGED
|
@@ -13,7 +13,7 @@ import os from "node:os";
|
|
|
13
13
|
|
|
14
14
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
15
15
|
|
|
16
|
-
import { firstContentText } from "@
|
|
16
|
+
import { firstContentText } from "@zhushanwen/extension-protocol";
|
|
17
17
|
|
|
18
18
|
import type { AgentEventLogEntry, DisplayItem, ExecutionStatus } from "@zhushanwen/subagent-core";
|
|
19
19
|
import { DEFAULT_AGENT_NAME } from "@zhushanwen/subagent-core";
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* GUI 协议映射辅助函数 —— run/subagent 状态字符串 → 协议 TreeItem 状态 + 图标。
|
|
3
3
|
*
|
|
4
|
-
* 协议包 @
|
|
4
|
+
* 协议包 @zhushanwen/extension-protocol 的 list-tree 组件用 TreeItem.status(三态)
|
|
5
5
|
* + TreeItem.icon 表达运行态。本模块把 workflow/subagent 领域的丰富状态字符串收口
|
|
6
6
|
* 到这两个枚举,供 helpers.ts / tool-workflow.ts / subagent-actions.ts 复用。
|
|
7
7
|
*
|
|
8
|
-
* 参考:@
|
|
8
|
+
* 参考:@zhushanwen/extension-protocol GuiComponentProps['list-tree']。
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type { GuiContext, TreeItem, TreeItemIcon } from "@
|
|
11
|
+
import type { GuiContext, TreeItem, TreeItemIcon } from "@zhushanwen/extension-protocol";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* 从 Pi ExtensionContext 构造协议 GuiContext 的最小子集。
|
package/src/interface/helpers.ts
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
type GuiRenderResult,
|
|
27
27
|
guiResult,
|
|
28
28
|
isGuiCapable,
|
|
29
|
-
} from "@
|
|
29
|
+
} from "@zhushanwen/extension-protocol";
|
|
30
30
|
import { mapRunIcon, mapRunStatus } from "./gui-mappers.ts";
|
|
31
31
|
|
|
32
32
|
// ── 常量 ─────────────────────────────────────────────────────
|
|
@@ -95,7 +95,7 @@ export const SubagentParams = Type.Object({
|
|
|
95
95
|
"Idle-recycle cadence for ALL subagents (modeless: every subagent stays continuable — this is NOT a mode switch). Controls how long an idle subagent (between rounds, no activity) stays before being automatically archived. " +
|
|
96
96
|
"Default: 300000 (5min). Raise it for long-interval collaboration where your next message may arrive more than 5min after a round ends. " +
|
|
97
97
|
"Pass 0 or a negative value to DISABLE idle recycling entirely (subagent stays available until you close it). " +
|
|
98
|
-
"Priority: this param > env
|
|
98
|
+
"Priority: this param > env TAIJI_SUBAGENT_IDLE_TIMEOUT_MS > default.",
|
|
99
99
|
})),
|
|
100
100
|
engine: Type.Optional(StringEnum(["pi", "zcode"], {
|
|
101
101
|
description:
|
|
@@ -173,7 +173,7 @@ export const SubagentParams = Type.Object({
|
|
|
173
173
|
// 源文件只读不续写);旧记录/状态机不动。pi 引擎限定(非 pi 在 execute 层拒绝)。
|
|
174
174
|
forkFromParam: Type.Optional(Type.Object({
|
|
175
175
|
sourceSubagentId: Type.String({
|
|
176
|
-
description: "REQUIRED for action:'fork-from'. The OLD subagentId whose conversation history becomes the inherited context of the new subagent. Works for any idle record — disconnected by a session restart, already finished, or previously closed/cancelled. Rejections: still-running sources (message them instead), sources held by another live process, worktree-bound sources, and unknown ids; an unparseable history anchor is guided to action:'message' (same-id reopen) instead.",
|
|
176
|
+
description: "REQUIRED for action:'fork-from'. The OLD subagentId whose conversation history becomes the inherited context of the new subagent. Works for any idle record — disconnected by a session restart, already finished, or previously closed/cancelled. Rejections: still-running sources (message them instead; sync-collect members cannot be messaged — close them first, then fork-from), sources held by another live process, worktree-bound sources, and unknown ids; an unparseable history anchor is guided to action:'message' (same-id reopen) instead.",
|
|
177
177
|
}),
|
|
178
178
|
prompt: Type.Optional(Type.String({
|
|
179
179
|
description: "Continuation instruction for the new subagent (what to do next on top of the inherited history). When omitted, a standard handover frame is injected: reconstruct done/decided/remaining from the inherited history, then continue to completion. Whitespace-only treated as omitted.",
|
|
@@ -207,7 +207,7 @@ Items over budget are truncated with a pointer: session_read {"action":"result",
|
|
|
207
207
|
## Continuing a subagent (modeless)
|
|
208
208
|
|
|
209
209
|
Every subagent stays continuable — no mode switch: action:"message" revives an idle record in place (or joins a running one's round), action:"fork-from" branches a new subagent from old history, action:"close" archives it.
|
|
210
|
-
idleTimeoutMs: idle-recycle cadence for ALL subagents — idle records auto-archive on expiry (default 300000 / 5min; 0/negative disables; env
|
|
210
|
+
idleTimeoutMs: idle-recycle cadence for ALL subagents — idle records auto-archive on expiry (default 300000 / 5min; 0/negative disables; env TAIJI_SUBAGENT_IDLE_TIMEOUT_MS: global default, per-call wins).
|
|
211
211
|
|
|
212
212
|
## You cannot
|
|
213
213
|
|
|
@@ -240,7 +240,7 @@ Do NOT recurse when: the work is linear/flat; the child needs your context; or y
|
|
|
240
240
|
|
|
241
241
|
// ponytail: renderCall 每次 TUI invalidate 都触发。streaming 中 args 是 partial JSON
|
|
242
242
|
// 解析结果(如 model="deep" 来自未流完的 "deepseek-router/ds-pro"),解析失败是预期。
|
|
243
|
-
// 不走 appendEntry(非真实错误),只走 logger.debug(默认 no-op,
|
|
243
|
+
// 不走 appendEntry(非真实错误),只走 logger.debug(默认 no-op,TAIJI_AGENT_DEBUG=1 写文件)。
|
|
244
244
|
const renderCallLogger = getLogger("subagents");
|
|
245
245
|
|
|
246
246
|
const subagentRenderCall: SubagentRenderCallCb = (args, theme, ctx) => {
|
|
@@ -260,7 +260,7 @@ const subagentRenderCall: SubagentRenderCallCb = (args, theme, ctx) => {
|
|
|
260
260
|
if (r) resolved = { model: `${r.model.provider}/${r.model.id}`, thinkingLevel: r.thinkingLevel };
|
|
261
261
|
} catch (err) {
|
|
262
262
|
// streaming 中间态(partial JSON)或 service 未就绪 → 降级不显示 model(renderCall 不应崩)。
|
|
263
|
-
// 不阻断渲染,不污染 TUI。开发期开
|
|
263
|
+
// 不阻断渲染,不污染 TUI。开发期开 TAIJI_AGENT_DEBUG=1 可写文件日志排查。
|
|
264
264
|
renderCallLogger.debug("renderCall model resolution failed, degrading", {
|
|
265
265
|
reason: toErrorMessage(err),
|
|
266
266
|
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// /subagents 命令。薄壳——打开 list overlay(等同原 /subagents list [<id>])。
|
|
4
4
|
//
|
|
5
5
|
// 解析:args[0] 直接作可选 <id>(聚焦该 record)。
|
|
6
|
-
// RPC 模式(
|
|
6
|
+
// RPC 模式(taiji GUI):解析 cancel/message/start action 直接执行,不打开 TUI。
|
|
7
7
|
// message/start 为 GUI 定向消息通道(设计 §3.3.3):GUI 经 client.prompt 短路
|
|
8
8
|
// extension 命令(不经主 agent LLM),TUI 分支不消费这两个 verb(行为零变化)。
|
|
9
9
|
|
|
@@ -129,14 +129,14 @@ async function rpcStart(
|
|
|
129
129
|
task: string,
|
|
130
130
|
ctx: ExtensionCommandContext,
|
|
131
131
|
): Promise<void> {
|
|
132
|
-
// GUI 定向新建(设计 §3.3.3
|
|
132
|
+
// GUI 定向新建(设计 §3.3.3):万物可续(modeless),start 无续聊参数——
|
|
133
|
+
// 续聊资格由引擎 conversation 能力轴在 message 面把关。
|
|
133
134
|
try {
|
|
134
135
|
const result = await startHandler(
|
|
135
136
|
service,
|
|
136
137
|
{
|
|
137
138
|
slug,
|
|
138
139
|
task,
|
|
139
|
-
conversation: true,
|
|
140
140
|
},
|
|
141
141
|
// RPC 命令无外层 AbortSignal(GUI 请求生命周期不映射到 subagent 取消——
|
|
142
142
|
// start 是 detached 后台语义,取消走 /subagents cancel)
|
|
@@ -156,7 +156,7 @@ async function rpcStart(
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
/**
|
|
159
|
-
* RPC 模式(
|
|
159
|
+
* RPC 模式(taiji GUI):解析后的 action 分发执行,不打开 TUI。
|
|
160
160
|
* 行为等价拆分自 handler(fallow 圈复杂度 21 > 15):三个执行体
|
|
161
161
|
* (cancel/message/start)各自成函数,本函数只做 switch 分发 +
|
|
162
162
|
* usage notify + exhaustiveness 断言。
|
|
@@ -252,7 +252,7 @@ export function registerSubagentsCommand(pi: ExtensionAPI): void {
|
|
|
252
252
|
return;
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
// ── RPC 模式(
|
|
255
|
+
// ── RPC 模式(taiji GUI):解析 action 直接执行,不打开 TUI ──
|
|
256
256
|
// hasUI 在 TUI 和 RPC 都为 true,不能用于区分;用 ctx.mode === "rpc" 判定 GUI 通道。
|
|
257
257
|
if (ctx.mode === "rpc") {
|
|
258
258
|
await executeRpcAction(pi, service, parseSubagentRpcCommand(argsStr), ctx);
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
type GuiRenderResult,
|
|
33
33
|
guiResult,
|
|
34
34
|
isGuiCapable,
|
|
35
|
-
} from "@
|
|
35
|
+
} from "@zhushanwen/extension-protocol";
|
|
36
36
|
// C5②/C5⑦:创作闭环统一走 core barrel(generateWorkflowScript/saveWorkflow/
|
|
37
37
|
// deleteWorkflow/lintScript 均为 barrel 导出面;深路径在 npm/vendored 形态不可达)
|
|
38
38
|
import {
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
type GuiRenderResult,
|
|
29
29
|
guiResult,
|
|
30
30
|
isGuiCapable,
|
|
31
|
-
} from "@
|
|
31
|
+
} from "@zhushanwen/extension-protocol";
|
|
32
32
|
import { type Static, Type } from "typebox";
|
|
33
33
|
|
|
34
34
|
import { SLUG_MAX_LENGTH } from "@zhushanwen/subagent-core";
|
|
@@ -393,10 +393,16 @@ export async function actionRun(
|
|
|
393
393
|
const all = await deps.registry.loadAll();
|
|
394
394
|
const available = all.filter((wf) => wf.available);
|
|
395
395
|
const suggestions = available
|
|
396
|
-
.map((wf) => ` - ${wf.name}: ${wf.meta.description || "(no description)"}`)
|
|
396
|
+
.map((wf) => ` - ${wf.name}: ${wf.meta.description || "(no description)"}\n location: ${wf.path}`)
|
|
397
397
|
.join("\n");
|
|
398
|
+
// [按名解析自救指引] 摘要逐条附绝对路径 location:run 的 name 形参最贴近的
|
|
399
|
+
// 读取面就是本清单(<available_workflows> 注入面在 start 时已过时/可能不在
|
|
400
|
+
// 上下文)——模型按清单里的名字重试(8.6.0 实装 getPath-only 时代的实测失败
|
|
401
|
+
// 形态,2026-09-14 嵌套 subagent 现场)还是注定 throw;带上 location 后失败
|
|
402
|
+
// 一次即可按绝对路径自救。main 的 C5③ 按名解析(registry.get 内置/已保存名
|
|
403
|
+
// 优先)是主修复,本行是清单侧恢复指引闭环。
|
|
398
404
|
throw new Error(
|
|
399
|
-
`Workflow '${name}' not found. Available:\n${suggestions || " (none)"}
|
|
405
|
+
`Workflow '${name}' not found. Available (name — use the absolute location path as 'name' when the bare name is rejected):\n${suggestions || " (none)"}`,
|
|
400
406
|
);
|
|
401
407
|
}
|
|
402
408
|
|
package/src/jsonl-run-store.ts
CHANGED
|
@@ -248,11 +248,11 @@ const DEFAULT_SAVE_DEBOUNCE_MS = 200;
|
|
|
248
248
|
* - 非法值(非有限数/≤0)→ 不清理(显式 opt-out 通道:用户意图不明时不动
|
|
249
249
|
* 磁盘,对齐 prune 内部「任何失败都不抛」的保守哲学)。
|
|
250
250
|
*
|
|
251
|
-
* 用
|
|
252
|
-
* ENV_WHITELIST_PREFIXES(只有
|
|
253
|
-
* 同
|
|
251
|
+
* 用 TAIJI_ 前缀而非 PI_:本 env 是 pi 进程内读的配置 env,taiji 桌面 spawn 链按
|
|
252
|
+
* ENV_WHITELIST_PREFIXES(只有 TAIJI_ 等)过滤,PI_ 前缀在桌面场景被静默丢弃——
|
|
253
|
+
* 同 TAIJI_SUBAGENT_IDLE_TIMEOUT_MS 的改名教训(lifecycle-manager.ts)。
|
|
254
254
|
*/
|
|
255
|
-
export const STATE_MAX_RUNS_ENV = "
|
|
255
|
+
export const STATE_MAX_RUNS_ENV = "TAIJI_SUBAGENT_STATE_MAX_RUNS";
|
|
256
256
|
|
|
257
257
|
/** 解析保留上限;env 未设/空 → 默认上限,显式非法/≤0 → undefined(不清理)。 */
|
|
258
258
|
function getEnvStateMaxRuns(): number | undefined {
|
package/src/session-lifecycle.ts
CHANGED
|
@@ -230,7 +230,7 @@ function createOrReuseServices(pi: ExtensionAPI, ctx: ExtensionContext): Service
|
|
|
230
230
|
// 注入 ctx.ui.setWidget 作为 streaming sink(只绑方法,不持有整个 ctx)。
|
|
231
231
|
// background subagent 执行期间,text_delta 经 SubagentStream 合并后由此通道转发。
|
|
232
232
|
// [W1 修复] ctx.mode === 'rpc' 守卫:TUI/json/print 下 streamSink = undefined(无 widget 噪音),
|
|
233
|
-
// rpc mode(GUI/
|
|
233
|
+
// rpc mode(GUI/taiji)下保持原行为(ctx.ui.setWidget → sidecar → chatStore)。
|
|
234
234
|
// streamSink API 不变(SubagentStream.onDelta 仍可调,只是 TUI 下 stream 不会被创建)。
|
|
235
235
|
streamSink: ctx.mode === "rpc"
|
|
236
236
|
? { setWidget: (key, lines) => ctx.ui.setWidget(key, lines) }
|