@zhushanwen/pi-subagent-workflow 0.4.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/execution/__tests__/crash-recovery.test.ts +1 -1
- package/src/execution/__tests__/index-session-start.test.ts +1 -1
- package/src/execution/__tests__/sdk-contract.test.ts +4 -4
- package/src/execution/__tests__/session-start-reaper.test.ts +1 -1
- package/src/execution/dialog-queue.ts +5 -4
- package/src/execution/host-mode.ts +5 -1
- package/src/execution/model-resolver.ts +1 -1
- package/src/execution/session-runner.ts +1 -1
- package/src/execution/subagent-service.ts +1 -1
- package/src/execution/ui-request-observability.ts +1 -1
- package/src/index.ts +5 -7
- package/src/interface/subagent-tool.ts +21 -35
- package/src/interface/views/WorkflowsView.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-subagent-workflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "2.0.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.",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"@earendil-works/pi-coding-agent": "*",
|
|
47
47
|
"@earendil-works/pi-ai": "*",
|
|
48
48
|
"@earendil-works/pi-tui": "*",
|
|
49
|
-
"
|
|
50
|
-
"@zhushanwen/pi-structured-output": "0.
|
|
49
|
+
"typebox": "*",
|
|
50
|
+
"@zhushanwen/pi-structured-output": "2.0.0"
|
|
51
51
|
},
|
|
52
52
|
"peerDependenciesMeta": {
|
|
53
53
|
"@earendil-works/pi-coding-agent": {
|
|
@@ -29,7 +29,7 @@ vi.mock("@earendil-works/pi-ai", () => ({
|
|
|
29
29
|
vi.mock("@earendil-works/pi-ai", () => ({
|
|
30
30
|
StringEnum: (values: string[]) => ({ type: "string", enum: values }),
|
|
31
31
|
}));
|
|
32
|
-
vi.mock("
|
|
32
|
+
vi.mock("typebox", () => ({
|
|
33
33
|
Type: {
|
|
34
34
|
Object: (props: Record<string, unknown>) => ({ type: "object", properties: props }),
|
|
35
35
|
Optional: (schema: unknown) => ({ ...(schema as object), optional: true }),
|
|
@@ -51,7 +51,7 @@ vi.mock("@earendil-works/pi-ai", () => ({
|
|
|
51
51
|
vi.mock("@earendil-works/pi-ai", () => ({
|
|
52
52
|
StringEnum: (values: string[]) => ({ type: "string", enum: values }),
|
|
53
53
|
}));
|
|
54
|
-
vi.mock("
|
|
54
|
+
vi.mock("typebox", () => ({
|
|
55
55
|
Type: {
|
|
56
56
|
Object: (props: Record<string, unknown>) => ({ type: "object", properties: props }),
|
|
57
57
|
Optional: (schema: unknown) => ({ ...(schema as object), optional: true }),
|
|
@@ -12,19 +12,19 @@
|
|
|
12
12
|
// 不导入 index.ts(它经 getAgentDir 值导入触发 alias 解析失败——alias 指向
|
|
13
13
|
// .d.ts-only stub)。改测叶子注册函数。session_start 的
|
|
14
14
|
// (event, ctx) 双参数契约由 tsconfig 的精确 stub 类型在编译期强制(见
|
|
15
|
-
//
|
|
15
|
+
// 注释:modelRegistry/cwd/ui 不在 event 上)。
|
|
16
16
|
|
|
17
17
|
import { describe, expect, it, vi } from "vitest";
|
|
18
18
|
|
|
19
19
|
// registerSubagentTool 经 subagent-tool.ts 值导入 StringEnum(pi-ai)+ Type(typebox)。
|
|
20
|
-
//
|
|
20
|
+
// stub 是 .d.ts(仅类型),pnpm optional-peer-dep 插件拦截值导入 → vi.mock 兜底。
|
|
21
21
|
vi.mock("@earendil-works/pi-ai", () => ({
|
|
22
22
|
StringEnum: (values: string[]) => ({ type: "string", enum: values }),
|
|
23
23
|
}));
|
|
24
24
|
vi.mock("@earendil-works/pi-ai", () => ({
|
|
25
25
|
StringEnum: (values: string[]) => ({ type: "string", enum: values }),
|
|
26
26
|
}));
|
|
27
|
-
vi.mock("
|
|
27
|
+
vi.mock("typebox", () => ({
|
|
28
28
|
Type: {
|
|
29
29
|
Object: (props: Record<string, unknown>) => ({ type: "object", properties: props }),
|
|
30
30
|
Optional: (schema: unknown) => ({ ...schema as object, optional: true }),
|
|
@@ -263,7 +263,7 @@ describe("subagent tool contract [MANDATORY]", () => {
|
|
|
263
263
|
// ============================================================
|
|
264
264
|
describe("session_start handler signature (compile-time guarantee)", () => {
|
|
265
265
|
it("ExtensionHandler<SessionStartEvent> is (event, ctx) two-param — enforced by stub type", () => {
|
|
266
|
-
// 此测试是编译期断言:
|
|
266
|
+
// 此测试是编译期断言:stub 类型声明
|
|
267
267
|
// ExtensionHandler<E, R> = (event: E, ctx: ExtensionContext) => Promise<R|void> | R | void;
|
|
268
268
|
// 且 SessionStartEvent 注释明确 modelRegistry/cwd/ui 不在 event 上(在 ctx)。
|
|
269
269
|
// index.ts:66 `pi.on("session_start", (_event, ctx) => {...})` 通过此类型检查即证明契约。
|
|
@@ -21,7 +21,7 @@ vi.mock("@earendil-works/pi-ai", () => ({
|
|
|
21
21
|
vi.mock("@earendil-works/pi-ai", () => ({
|
|
22
22
|
StringEnum: (values: string[]) => ({ type: "string", enum: values }),
|
|
23
23
|
}));
|
|
24
|
-
vi.mock("
|
|
24
|
+
vi.mock("typebox", () => ({
|
|
25
25
|
Type: {
|
|
26
26
|
Object: (props: Record<string, unknown>) => ({ type: "object", properties: props }),
|
|
27
27
|
Optional: (schema: unknown) => ({ ...schema as object, optional: true }),
|
|
@@ -144,7 +144,7 @@ interface QueueItem {
|
|
|
144
144
|
*
|
|
145
145
|
* 单 session 假设(M-2,与 index.ts lastSessionId 同源):本队列是进程级单例(实例挂在
|
|
146
146
|
* globalThis[Symbol.for("@zhushanwen/pi-subagents.dialogQueue")],见 getOrCreateDialogQueue)。
|
|
147
|
-
* rejectAll()
|
|
147
|
+
* rejectAll() 清空所有 pending dialog——无 per-session 隔离。Pi 当前架构保证单进程
|
|
148
148
|
* 单 session 串行(同进程不会并发多个 session),故 session_shutdown 调 rejectAll() 只会清掉
|
|
149
149
|
* 当前 session 的 pending。若未来 Pi 支持同进程多 session 并发,session A 退出会误清 session B
|
|
150
150
|
* 的 pending dialog——届时需改为 per-session 隔离(入队项 QueueItem 带 sessionId,rejectAll
|
|
@@ -314,9 +314,10 @@ export class DialogGlobalQueue {
|
|
|
314
314
|
this.processing = false;
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
/**
|
|
318
|
-
*
|
|
319
|
-
|
|
317
|
+
/** 清空队列状态(仅在 rejectAll 之后调用)。pending Promise 必须先由 rejectAll settle。
|
|
318
|
+
* 不 settle Promise 的纯状态重置——单独调用会导致 Promise 永挂(footgun),故设为 private。
|
|
319
|
+
* 外部调用方应使用 rejectAll()(它 settle 所有 pending + 重置状态,是原子操作)。 */
|
|
320
|
+
private resetState(): void {
|
|
320
321
|
this.queue = [];
|
|
321
322
|
this.current = undefined;
|
|
322
323
|
this.processing = false;
|
|
@@ -16,7 +16,11 @@
|
|
|
16
16
|
// 集中到单一修改点,未来 Pi 新增 mode 值时只改本文件。
|
|
17
17
|
// - 业务语义命名("gui"/"headless")比原始枚举值更清晰表达意图。
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// ExtensionMode 在 Pi SDK 的 core/extensions/types.d.ts 中定义为
|
|
20
|
+
// `"tui" | "rpc" | "json" | "print"`(ExtensionContext.mode 的类型),但未从包根
|
|
21
|
+
// 导出。这里定义与 SDK 同构的本地别名(ctx.mode 的字面量联合),由同包其他模块复用,
|
|
22
|
+
// 避免依赖未导出的内部类型。
|
|
23
|
+
export type ExtensionMode = "tui" | "rpc" | "json" | "print";
|
|
20
24
|
|
|
21
25
|
/** 主进程运行模式分类。基于 ExtensionMode 聚合为业务语义。
|
|
22
26
|
* - "tui":纯 Pi TUI,ctx.ui.custom 可用,用户在终端交互
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { type ChildProcess,execFileSync, spawn } from "node:child_process";
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
|
|
12
|
-
import type { ExtensionMode } from "
|
|
12
|
+
import type { ExtensionMode } from "./host-mode.ts";
|
|
13
13
|
|
|
14
14
|
import { type MirrorFlags, mirrorMainProcessFlags } from "./argv-mirror.ts";
|
|
15
15
|
import { writeAliveMarker } from "./alive-store.ts";
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
6
|
|
|
7
|
-
import type { ExtensionMode } from "
|
|
7
|
+
import type { ExtensionMode } from "./host-mode.ts";
|
|
8
8
|
|
|
9
9
|
import type { AgentResult as WorkflowAgentResult } from "../orchestration/models/types.ts";
|
|
10
10
|
// D-A10: workflow 侧 AgentResult 映射(executeAndAwait 出口)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// UI 请求可观测性状态(从 subagent-service.ts 提取,降低主文件行数)。
|
|
4
4
|
// 持有 sessionMode + handler 缺失告警去重集合,供 SubagentService 委托调用。
|
|
5
5
|
|
|
6
|
-
import type { ExtensionMode } from "
|
|
6
|
+
import type { ExtensionMode } from "./host-mode.ts";
|
|
7
7
|
|
|
8
8
|
// ── 跨模块桥接(ui-request-queue 无 ctx.service 引用时走这里) ──
|
|
9
9
|
//
|
package/src/index.ts
CHANGED
|
@@ -17,7 +17,7 @@ import * as fs from "node:fs";
|
|
|
17
17
|
import * as os from "node:os";
|
|
18
18
|
import * as path from "node:path";
|
|
19
19
|
|
|
20
|
-
import type { ExtensionAPI, ExtensionContext,
|
|
20
|
+
import type { ExtensionAPI, ExtensionContext, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent } from "@earendil-works/pi-coding-agent";
|
|
21
21
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
22
22
|
|
|
23
23
|
import type { AgentRegistry } from "./execution/agent-registry.ts";
|
|
@@ -90,7 +90,7 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
90
90
|
// resources_discover:不再注入额外 skill 目录(ADR-031 废弃 discovery.json)。
|
|
91
91
|
// pi 核心 auto-discovery 已覆盖 .agents/skills 等标准目录,子 session 的
|
|
92
92
|
// --skill 由 agent({skill}) 调用方显式传入,无需 extension 额外补充。
|
|
93
|
-
pi.on("resources_discover", (_event
|
|
93
|
+
pi.on("resources_discover", (_event, _ctx: ExtensionContext) => {
|
|
94
94
|
return {};
|
|
95
95
|
});
|
|
96
96
|
|
|
@@ -342,7 +342,7 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
342
342
|
// ════════════════════════════════════════════════════════════
|
|
343
343
|
// model_select:用户切换 model 时刷新缓存
|
|
344
344
|
// ════════════════════════════════════════════════════════════
|
|
345
|
-
pi.on("model_select", (event
|
|
345
|
+
pi.on("model_select", (event, ctx: ExtensionContext) => {
|
|
346
346
|
const service = getModelConfigService();
|
|
347
347
|
if (service && typeof service.setCtxModel === "function") {
|
|
348
348
|
service.setCtxModel(event.model);
|
|
@@ -396,10 +396,9 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
396
396
|
}
|
|
397
397
|
|
|
398
398
|
// M2: 清理 dialog queue 运行时状态(queue/current/processing)。
|
|
399
|
-
// [#10]
|
|
399
|
+
// [#10] rejectAll() settle 所有 pending dialog Promise(防闭包泄漏:未 settle 的
|
|
400
400
|
// Promise 持有 resolve/reject 闭包及 handler 上下文,session 退出后仍挂在全球队列上),
|
|
401
|
-
//
|
|
402
|
-
// rejectAll() 由 dialog-queue.ts 提供(Group B 新增);若其内部已 reset 状态,此处 clear() 为幂等兜底。
|
|
401
|
+
// 并内部重置 queue/current/processing(原子操作,无 footgun)。
|
|
403
402
|
// 单 session 假设(M-2,同 lastSessionId):rejectAll() 清空进程级单例的所有 pending,
|
|
404
403
|
// 依赖 Pi 单进程单 session 串行保证——不会误清其他 session。多 session 并发的迁移策略
|
|
405
404
|
// 见 DialogGlobalQueue 类注释(rejectAllForSession)。
|
|
@@ -407,7 +406,6 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
407
406
|
// 在 /new /resume /fork 时不丢失注册)。
|
|
408
407
|
const dialogQueue = getOrCreateDialogQueue();
|
|
409
408
|
dialogQueue.rejectAll();
|
|
410
|
-
dialogQueue.clear();
|
|
411
409
|
});
|
|
412
410
|
|
|
413
411
|
// ════════════════════════════════════════════════════════════
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import type { Component } from "@earendil-works/pi-tui";
|
|
13
13
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
14
14
|
import type { AgentToolResult, ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import { Type } from "
|
|
15
|
+
import { type Static, Type } from "typebox";
|
|
16
16
|
|
|
17
17
|
import { SLUG_MAX_LENGTH } from "../execution/execute-options-mapper.ts";
|
|
18
18
|
import { getSubagentService } from "../execution/subagent-service.ts";
|
|
@@ -27,44 +27,15 @@ import { type RenderContext,renderSubagentCall, renderSubagentResult } from "./t
|
|
|
27
27
|
// ============================================================
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* execute 回调的 params
|
|
31
|
-
* 无法从 SubagentParams schema 反向推断参数类型)。
|
|
30
|
+
* execute 回调的 params 类型由 SubagentParams schema 经 Static 投影得出(见下方 cb 签名)。
|
|
32
31
|
* action 与对应 param 不匹配时 handler 内 throw。
|
|
33
32
|
*/
|
|
34
|
-
interface ListParam {
|
|
35
|
-
includeFinished?: boolean;
|
|
36
|
-
limit?: number;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface CancelParam {
|
|
40
|
-
subagentId: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
interface SubagentExecuteParams {
|
|
44
|
-
action: "start" | "list" | "cancel";
|
|
45
|
-
// action:"start" 的 13 字段拍平到顶层(弱模型常省略 startParam 嵌套层导致调用失败)。
|
|
46
|
-
// 拍平后这些字段直接在顶层(全部 optional——schema flat 无法表达「action 条件必填」,
|
|
47
|
-
// 由 startHandler runtime 校验 task/slug 必填)。
|
|
48
|
-
task?: string;
|
|
49
|
-
slug?: string;
|
|
50
|
-
agent?: string;
|
|
51
|
-
model?: string;
|
|
52
|
-
thinkingLevel?: string;
|
|
53
|
-
skillPath?: string;
|
|
54
|
-
appendSystemPrompt?: string[];
|
|
55
|
-
schema?: Record<string, unknown>;
|
|
56
|
-
maxTurns?: number;
|
|
57
|
-
graceTurns?: number;
|
|
58
|
-
fork?: boolean;
|
|
59
|
-
worktree?: boolean;
|
|
60
|
-
cwd?: string;
|
|
61
|
-
listParam?: ListParam;
|
|
62
|
-
cancelParam?: CancelParam;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
33
|
type SubagentExecuteCb = (
|
|
66
34
|
toolCallId: string,
|
|
67
|
-
params
|
|
35
|
+
// Pi SDK 从 parameters schema 反向推断 params 类型。typebox v1 的 StringEnum
|
|
36
|
+
// 在 Static 投影下退化为 string(非字面量联合),因此 action 在 cb 入参里是 string,
|
|
37
|
+
// 由下方 isSubagentAction 类型守卫收窄到字面量联合后再 switch。
|
|
38
|
+
params: Static<typeof SubagentParams>,
|
|
68
39
|
signal: AbortSignal | undefined,
|
|
69
40
|
onUpdate?: (partialResult: AgentToolResult<SubagentToolResult>) => void,
|
|
70
41
|
// ctx 在 SDK 契约里必填;此处保持 optional 以兼容 onUpdate? 在前(TS 参数顺序约束),
|
|
@@ -166,6 +137,16 @@ function assertNever(value: never): string {
|
|
|
166
137
|
return String(value);
|
|
167
138
|
}
|
|
168
139
|
|
|
140
|
+
/** Subagent action 字面量联合(与 parameters schema 的 StringEnum 取值一致)。 */
|
|
141
|
+
type SubagentAction = "start" | "list" | "cancel";
|
|
142
|
+
|
|
143
|
+
/** 类型守卫:把 schema 投影出的 string 形式 action 收窄回字面量联合。
|
|
144
|
+
* typebox v1 的 StringEnum Static 退化为 string,需运行时校验 + 类型收窄
|
|
145
|
+
* 才能恢复 switch 的 exhaustiveness 约束。 */
|
|
146
|
+
function isSubagentAction(value: string): value is SubagentAction {
|
|
147
|
+
return value === "start" || value === "list" || value === "cancel";
|
|
148
|
+
}
|
|
149
|
+
|
|
169
150
|
/** unknown 是否为含 model/thinkingLevel 的对象(类型守卫,替代全可选结构 `as`)。 */
|
|
170
151
|
function isModelOverrideObj(a: unknown): a is { model?: unknown; thinkingLevel?: unknown } {
|
|
171
152
|
return typeof a === "object" && a !== null;
|
|
@@ -324,6 +305,11 @@ const executeSubagent: SubagentExecuteCb = async (
|
|
|
324
305
|
const service = getSubagentService();
|
|
325
306
|
if (!service) throw new Error("subagents runtime not initialized");
|
|
326
307
|
|
|
308
|
+
// typebox v1 的 StringEnum 在 Static 投影下退化为 string,此处类型守卫收窄回
|
|
309
|
+
// 字面量联合,恢复 switch 的 exhaustiveness 约束(default 分支 = never)。
|
|
310
|
+
if (!isSubagentAction(params.action)) {
|
|
311
|
+
throw new Error(`Unknown subagent action: ${params.action}`);
|
|
312
|
+
}
|
|
327
313
|
switch (params.action) {
|
|
328
314
|
case "start":
|
|
329
315
|
// 拍平后直接传顶层 params(StartHandlerInput 是 SubagentExecuteParams 子集,
|
|
@@ -109,7 +109,7 @@ function walled(theme: ThemeLike, content: string, contentWidth: number): string
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// ── Minimal TUI duck-types(避免直接 import TUI/KeybindingsManager 类型 ──
|
|
112
|
-
// 共享类型 fallback
|
|
112
|
+
// 共享类型 fallback 不导出 TUI 类,
|
|
113
113
|
// workspace 跨包 typecheck 会报 "no exported member 'TUI'"。
|
|
114
114
|
// 此处用结构化接口替代——只声明 view 实际用到的成员(requestRender + terminal)。
|
|
115
115
|
|