@zhushanwen/pi-subagent-workflow 8.9.0 → 8.10.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 +6 -4
- package/relay/relay.mjs +3 -2
- package/src/host/__tests__/pi-host.test.ts +22 -11
- package/src/host/pi-host.ts +58 -4
- package/src/index.ts +14 -12
- package/src/interface/subagent-tool-schema.ts +1 -1
- package/src/jsonl-run-store.ts +13 -51
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-subagent-workflow",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.10.0",
|
|
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.",
|
|
@@ -58,15 +58,17 @@
|
|
|
58
58
|
"@xyz-agent/session-delivery": "0.3.1",
|
|
59
59
|
"@zhushanwen/pi-ext-guards": "0.2.1",
|
|
60
60
|
"@zhushanwen/pi-extension-logger": "0.4.1",
|
|
61
|
-
"@zhushanwen/subagent-core": "0.
|
|
61
|
+
"@zhushanwen/subagent-core": "0.7.0",
|
|
62
|
+
"@zhushanwen/pi-subagent-cli": "0.1.1",
|
|
63
|
+
"@zhushanwen/zcode-subagent-cli": "0.1.1"
|
|
62
64
|
},
|
|
63
65
|
"peerDependencies": {
|
|
64
66
|
"@earendil-works/pi-ai": "^0.84.4",
|
|
65
67
|
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
66
68
|
"@earendil-works/pi-tui": "^0.84.4",
|
|
67
69
|
"typebox": "*",
|
|
68
|
-
"@zhushanwen/pi-pending-notifications": "0.
|
|
69
|
-
"@zhushanwen/pi-structured-output": "5.1.
|
|
70
|
+
"@zhushanwen/pi-pending-notifications": "0.6.0",
|
|
71
|
+
"@zhushanwen/pi-structured-output": "5.1.4"
|
|
70
72
|
},
|
|
71
73
|
"peerDependenciesMeta": {
|
|
72
74
|
"@earendil-works/pi-coding-agent": {
|
package/relay/relay.mjs
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* 通道,payload 一律 base64 封装保字节精确。
|
|
9
9
|
*
|
|
10
10
|
* ============================ 镜像常量表 ============================
|
|
11
|
-
* SSOT = packages/subagent-
|
|
12
|
-
*
|
|
11
|
+
* SSOT = packages/subagent-engine-sdk/src/relay-env.ts(零依赖脚本不能 import workspace 包,只能内嵌镜像;
|
|
12
|
+
* core 侧经 subagent-core 的 ./relay-env 子入口 re-export 保 semver 面。改名/改值必须双侧同步,
|
|
13
|
+
* 一致性由 E-3 conformance relay 变体断言锁定——设计 §10-5):
|
|
13
14
|
*
|
|
14
15
|
* | relay-env.ts 常量 | 本文件常量 | 值 |
|
|
15
16
|
* |---------------------------|---------------------------|-----------------------------|
|
|
@@ -197,20 +197,31 @@ describe("createPiHostServices.log(桥接 pi-extension-logger)", () => {
|
|
|
197
197
|
});
|
|
198
198
|
|
|
199
199
|
describe("createPiNotifyDomainPorts.countActiveFromEntries(适配读 .count)", () => {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
200
|
+
it("拆 CountActiveResult.count 为 number(core 端口契约),entries 透传(缺省过滤基准 undefined = 不过滤)", () => {
|
|
201
|
+
const entries: unknown[] = [{ customType: "pending:register" }];
|
|
202
|
+
vi.mocked(countActiveFromEntries).mockReturnValue({ count: 3, ids: ["a", "b", "c"], entries: [] });
|
|
203
203
|
|
|
204
|
-
|
|
204
|
+
const result = createPiNotifyDomainPorts().countActiveFromEntries?.(entries);
|
|
205
205
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
expect(result).toBe(3);
|
|
207
|
+
// [W4 读侧过滤②] 端口适配恒透传过滤基准 opts(缺省 undefined = 不过滤,
|
|
208
|
+
// 既有调用方零改动行为不变)。
|
|
209
|
+
expect(vi.mocked(countActiveFromEntries)).toHaveBeenCalledWith(entries, { currentSessionId: undefined });
|
|
210
|
+
});
|
|
209
211
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
it("[W4 读侧过滤② / F1] 调用时传 currentSessionId(per-call 基准)→ 透传差集过滤基准", () => {
|
|
213
|
+
const entries: unknown[] = [{ customType: "pending:register" }];
|
|
214
|
+
vi.mocked(countActiveFromEntries).mockReturnValue({ count: 1, ids: ["a"], entries: [] });
|
|
215
|
+
|
|
216
|
+
createPiNotifyDomainPorts().countActiveFromEntries?.(entries, { currentSessionId: "sess-child" });
|
|
217
|
+
|
|
218
|
+
expect(vi.mocked(countActiveFromEntries)).toHaveBeenCalledWith(entries, { currentSessionId: "sess-child" });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("零活跃返回 0({count: 0} 形状)", () => {
|
|
222
|
+
vi.mocked(countActiveFromEntries).mockReturnValue({ count: 0, ids: [], entries: [] });
|
|
223
|
+
expect(createPiNotifyDomainPorts().countActiveFromEntries?.([])).toBe(0);
|
|
224
|
+
});
|
|
214
225
|
});
|
|
215
226
|
|
|
216
227
|
describe("createPiNotifyDomainPorts.createDelivery(透传 session-delivery)", () => {
|
package/src/host/pi-host.ts
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
import { createRequire } from "node:module";
|
|
31
31
|
import { dirname, join } from "node:path";
|
|
32
|
+
import { fileURLToPath } from "node:url";
|
|
32
33
|
|
|
33
34
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
34
35
|
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
@@ -117,6 +118,45 @@ function skillRoots(): DiscoveryRoot[] {
|
|
|
117
118
|
];
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
/**
|
|
122
|
+
* 引擎包发现根(W4 L1 第二通道,设计 §3.4)。打包态主通道 = env
|
|
123
|
+
* XYZ_AGENT_ENGINE_ROOTS(W9 注入),此处承载 pi 宿主的常规安装路径:
|
|
124
|
+
* 1. 宿主包自身 node_modules——引擎包(<engine>-subagent-cli)作为本包
|
|
125
|
+
* dependencies 安装位(W9「扩展 package.json 声明引擎包为 dependencies」);
|
|
126
|
+
* 2. <agentDir>/npm/node_modules——pi npm 安装位(org 分组二层布局由扫描器
|
|
127
|
+
* 下钻覆盖);
|
|
128
|
+
* 3. <agentDir>/extensions——dev symlink 位(agents kind 同款 npm-dev 根)。
|
|
129
|
+
* 根目录不存在时由扫描器静默跳过(不在此预检)。 */
|
|
130
|
+
function engineRoots(): DiscoveryRoot[] {
|
|
131
|
+
const agentDir = getAgentDir();
|
|
132
|
+
const roots: DiscoveryRoot[] = [];
|
|
133
|
+
const selfNodeModules = hostPackageNodeModulesRoot();
|
|
134
|
+
if (selfNodeModules !== undefined) {
|
|
135
|
+
roots.push({ dir: selfNodeModules, source: "npm" });
|
|
136
|
+
}
|
|
137
|
+
roots.push({ dir: join(agentDir, "npm", "node_modules"), source: "npm" });
|
|
138
|
+
roots.push({ dir: join(agentDir, "extensions"), source: "npm-dev" });
|
|
139
|
+
return roots;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 宿主包(本扩展)node_modules 根:src/host/pi-host.ts 上溯三级 = 包根。
|
|
143
|
+
* jiti 加载下 import.meta.url 可用(corePackageNpmRoot 同款可靠性先例);解析失败
|
|
144
|
+
* 降级为不注入并 warn——绝不因根定位失败阻断发现主链。 */
|
|
145
|
+
function hostPackageNodeModulesRoot(): string | undefined {
|
|
146
|
+
try {
|
|
147
|
+
const here = fileURLToPath(import.meta.url);
|
|
148
|
+
// src/host/pi-host.ts → src/host → src → 包根
|
|
149
|
+
const packageRoot = dirname(dirname(dirname(here)));
|
|
150
|
+
return join(packageRoot, "node_modules");
|
|
151
|
+
} catch (err) {
|
|
152
|
+
getLogger("pi-host").warn(
|
|
153
|
+
"[pi-host] 宿主包 node_modules 引擎发现根解析失败——dependencies 安装的引擎包可能不可发现",
|
|
154
|
+
{ reason: toErrorMessage(err) },
|
|
155
|
+
);
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
120
160
|
/** pi 宿主 HostServices 实现(扩展初始化最早处经 configureCore 注入)。 */
|
|
121
161
|
export function createPiHostServices(): HostServices {
|
|
122
162
|
return {
|
|
@@ -139,23 +179,37 @@ export function createPiHostServices(): HostServices {
|
|
|
139
179
|
agents?: DiscoveryRoot[];
|
|
140
180
|
skills?: DiscoveryRoot[];
|
|
141
181
|
workflows?: DiscoveryRoot[];
|
|
182
|
+
engines?: DiscoveryRoot[];
|
|
142
183
|
} {
|
|
143
184
|
return {
|
|
144
185
|
agents: agentDirKindRoots("agents"),
|
|
145
186
|
skills: skillRoots(),
|
|
146
187
|
workflows: agentDirKindRoots("workflows"),
|
|
188
|
+
engines: engineRoots(),
|
|
147
189
|
};
|
|
148
190
|
},
|
|
149
191
|
};
|
|
150
192
|
}
|
|
151
193
|
|
|
152
|
-
/**
|
|
153
|
-
*
|
|
194
|
+
/**
|
|
195
|
+
* pi 侧通知域窄端口实现(configureNotifyDomain 注入)。zsw 壳不注入本端口
|
|
196
|
+
* (其完成通知走 HostServices.notify,P2 落地)。
|
|
197
|
+
*
|
|
198
|
+
* [F1 形态升级] 跨 session 残留过滤基准(W4 读侧过滤②)不走 factory 定型——本装配
|
|
199
|
+
* 在扩展模块加载时执行一次,session id 逐 session 变化(session_start 每次更新),
|
|
200
|
+
* factory 参数表达不了 per-call 基准;且生产装配点从未传参(注释声称的防御实际
|
|
201
|
+
* 缺基准)。现形态 = core 端口契约第二参(NotifyDomainPorts.countActiveFromEntries
|
|
202
|
+
* 的 opts)per-call 透传,基准由 core 读侧按「被读 entries 所属 session」提供
|
|
203
|
+
* (session-pending.ts 读 pi session 文件首行 SessionHeader.id)——fork 继承的父级
|
|
204
|
+
* 注册残留不进后代判定差集,防御真实生效。
|
|
205
|
+
*/
|
|
154
206
|
export function createPiNotifyDomainPorts(): NotifyDomainPorts {
|
|
155
207
|
return {
|
|
156
208
|
// pi 真函数返回 CountActiveResult,core 契约只读 .count——壳侧拆数值。
|
|
157
|
-
|
|
158
|
-
|
|
209
|
+
// [W4 读侧过滤② / F1] per-call 基准透传差集口径(缺省 undefined = 不过滤,
|
|
210
|
+
// 向后兼容:无基准的调用方零改动行为不变)。
|
|
211
|
+
countActiveFromEntries(entries: unknown[], opts?: { currentSessionId?: string }): number {
|
|
212
|
+
return countActiveFromEntries(entries, { currentSessionId: opts?.currentSessionId }).count;
|
|
159
213
|
},
|
|
160
214
|
|
|
161
215
|
// 投递内核工厂直传本体(结构兼容论证见文件头);不经包装避免多一层间接面。
|
package/src/index.ts
CHANGED
|
@@ -26,10 +26,11 @@ import { bestEffort } from "@zhushanwen/subagent-core";
|
|
|
26
26
|
// ═══ execution/ 层(subagents 核心 + 运行时) ═══
|
|
27
27
|
// [U7] 引擎列表状态文件(registry → engines.json,GUI 引擎选择器数据源)
|
|
28
28
|
import { syncEnginesFile } from "@zhushanwen/subagent-core";
|
|
29
|
-
// [
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
//
|
|
29
|
+
// [W11/DoD#5] registerPiEngine(inproc 'pi' 注册)已随内建引擎删除:registry 'pi'
|
|
30
|
+
// 由下方 syncEnginesFile 的三级发现装载 cli descriptor(engines/zcode 同理由
|
|
31
|
+
// registerZcodeEngine D8 薄壳承载)。
|
|
32
|
+
// [P3 引擎接线] 组合根登记 'zcode' 引擎(D8 薄壳:vendored 定位 cli descriptor;
|
|
33
|
+
// engineDataDir 默认走 common/data-dir SSOT)
|
|
33
34
|
import { registerZcodeEngine } from "@zhushanwen/subagent-core";
|
|
34
35
|
import { getModelConfigService } from "@zhushanwen/subagent-core";
|
|
35
36
|
import { getBoundNotifyLedger } from "@zhushanwen/subagent-core";
|
|
@@ -132,16 +133,17 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
132
133
|
// 完整;且先于任何可能消费 core 端口的初始化逻辑(引擎登记等)。缺省态若被消费,
|
|
133
134
|
// dataRoot 抛 core_host_not_configured(§3.4),接线后不再可达。
|
|
134
135
|
configureCore(createPiHostServices());
|
|
136
|
+
// [F1] 通知域端口无 factory 基准参数:跨 session 残留过滤基准(W4 读侧过滤②)
|
|
137
|
+
// 由 core 读侧 per-call 提供(session-pending 读「被读 entries 所属 session」)——
|
|
138
|
+
// 本装配在扩展启动时执行一次,session id 逐 session 变化,factory 定型表达不了
|
|
139
|
+
// per-call 基准(生产装配也从未传参,防御曾实际缺基准)。
|
|
135
140
|
configureNotifyDomain(createPiNotifyDomainPorts());
|
|
136
141
|
|
|
137
|
-
// [
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
// [P3 引擎接线] 登记 'zcode'(幂等同上)。惰性工厂:不触发 CLI/凭据探测,引擎被
|
|
144
|
-
// 实际选用(P4 路由或显式 getEngine('zcode'))才解析 deps。
|
|
142
|
+
// [W11/DoD#5] 'pi' 的 inproc 注册(registerPiEngine)已删除——缺省引擎 'pi' 的
|
|
143
|
+
// registry 条目由下方 syncEnginesFile 内的三级发现装载(cli descriptor,幂等),
|
|
144
|
+
// P4 配置路由(agent frontmatter engine 字段 + 三层优先级)在其上消费;chat 域
|
|
145
|
+
// pi 引擎不经 registry(SubagentService 自持 DI,W11 主 agent 裁决的临时豁免面)。
|
|
146
|
+
// [P3 引擎接线] 登记 'zcode'(幂等同上)。D8 薄壳:vendored 定位 cli descriptor。
|
|
145
147
|
registerZcodeEngine();
|
|
146
148
|
|
|
147
149
|
// [U7b] 引擎列表在 extension 模块加载时即同步 engines.json(不等 session_start——
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/interface/subagent-tool-schema.ts
|
|
2
2
|
//
|
|
3
|
-
// `subagent` 工具的参数 schema
|
|
3
|
+
// `subagent` 工具的参数 schema 纯常量叶子(零运行时依赖)。
|
|
4
4
|
//
|
|
5
5
|
// 抽取自 subagent-tool.ts(跨包契约另一半):subagent-tool 依赖树沉重(pi SDK /
|
|
6
6
|
// handler / render 链),structured-output 侧的跨包契约测试若从它 import schema
|
package/src/jsonl-run-store.ts
CHANGED
|
@@ -87,6 +87,7 @@ import {
|
|
|
87
87
|
SNAPSHOT_VERSION,
|
|
88
88
|
fromRunSnapshot,
|
|
89
89
|
toRunSnapshot,
|
|
90
|
+
pruneStateFilesBeyondCap,
|
|
90
91
|
type RunSnapshot,
|
|
91
92
|
} from "@zhushanwen/subagent-core";
|
|
92
93
|
import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
|
|
@@ -217,56 +218,11 @@ async function loadRunFromStateFile(filePath: string): Promise<WorkflowRun | nul
|
|
|
217
218
|
}
|
|
218
219
|
|
|
219
220
|
// ── State file retention (OR-5 ⑥b, default-on) ───────────────
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* 把 workflow-state 目录裁剪到 maxRuns 个最新 state 文件(mtime 升序,删最旧)。
|
|
227
|
-
*
|
|
228
|
-
* 只删本目录内命中 {@link STATE_FILE_GLOB} 的文件;任何失败都不抛(清理是旁路
|
|
229
|
-
* 维护,不能拖垮持久化主链路):readdir/stat 失败静默放弃本轮,单个 unlink 失败
|
|
230
|
-
* (非 ENOENT)logger.warn 留证后继续删其余——ENOENT 视为并发删除竞态下的已达成
|
|
231
|
-
* 目标,不告警。
|
|
232
|
-
*/
|
|
233
|
-
async function pruneStateFilesBeyondCap(stateDir: string, maxRuns: number): Promise<void> {
|
|
234
|
-
let names: string[];
|
|
235
|
-
try {
|
|
236
|
-
names = await fs.promises.readdir(stateDir);
|
|
237
|
-
} catch (err) {
|
|
238
|
-
if (!isEnoentError(err)) {
|
|
239
|
-
const reason = toErrorMessage(err);
|
|
240
|
-
logger.warn(`[subagent-workflow] state retention: readdir ${stateDir} failed: ${reason}`);
|
|
241
|
-
}
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
const stateFiles = names.filter((n) => STATE_FILE_GLOB.test(n)).sort();
|
|
245
|
-
if (stateFiles.length <= maxRuns) return;
|
|
246
|
-
|
|
247
|
-
// stat 全集取 mtime;allSettled 部分降级——单文件 stat 失败(并发删除 ENOENT 等)
|
|
248
|
-
// 静默跳过该文件,不阻断本轮裁剪
|
|
249
|
-
const settled = await Promise.allSettled(
|
|
250
|
-
stateFiles.map(async (name) => {
|
|
251
|
-
const full = path.join(stateDir, name);
|
|
252
|
-
return { full, mtimeMs: (await fs.promises.stat(full)).mtimeMs };
|
|
253
|
-
}),
|
|
254
|
-
);
|
|
255
|
-
const byMtimeAsc = settled
|
|
256
|
-
.flatMap((r) => (r.status === "fulfilled" ? [r.value] : []))
|
|
257
|
-
.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
258
|
-
const victims = byMtimeAsc.slice(0, byMtimeAsc.length - maxRuns);
|
|
259
|
-
for (const victim of victims) {
|
|
260
|
-
try {
|
|
261
|
-
await fs.promises.unlink(victim.full);
|
|
262
|
-
logger.debug(`[subagent-workflow] state retention: pruned ${victim.full}`);
|
|
263
|
-
} catch (err) {
|
|
264
|
-
if (isEnoentError(err)) continue; // 并发删除已达成目标
|
|
265
|
-
const reason = toErrorMessage(err);
|
|
266
|
-
logger.warn(`[subagent-workflow] state retention: failed to delete ${victim.full}: ${reason}`);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
221
|
+
//
|
|
222
|
+
// 裁剪主体单源消费 core pruneStateFilesBeyondCap(S4-A7 收口:本地曾有一份逐段
|
|
223
|
+
// 同构的私有实现,retention 语义双份各自演化是漂移隐患);日志注入本模块
|
|
224
|
+
// logger + [subagent-workflow] tag,错误字符串化用 ext-guards toErrorMessage——
|
|
225
|
+
// 行为与收口前一致(差异仅经 core 单源演化时两宿主同步)。
|
|
270
226
|
|
|
271
227
|
// ── JsonlRunStore ────────────────────────────────────────────
|
|
272
228
|
|
|
@@ -580,7 +536,13 @@ export class JsonlRunStore {
|
|
|
580
536
|
if (rollbackFirstWrite) {
|
|
581
537
|
const maxRuns = getEnvStateMaxRuns();
|
|
582
538
|
if (maxRuns !== undefined) {
|
|
583
|
-
|
|
539
|
+
// 单源裁剪(core pruneStateFilesBeyondCap),注入本扩展 logger tag 与
|
|
540
|
+
// toErrorMessage(行为与本地实现收口前一致)
|
|
541
|
+
await pruneStateFilesBeyondCap(this.stateDir, maxRuns, {
|
|
542
|
+
warn: (msg) => logger.warn(`[subagent-workflow] ${msg}`),
|
|
543
|
+
debug: (msg) => logger.debug(`[subagent-workflow] ${msg}`),
|
|
544
|
+
toMsg: toErrorMessage,
|
|
545
|
+
});
|
|
584
546
|
}
|
|
585
547
|
}
|
|
586
548
|
for (const s of settlers) s.resolve();
|