@zhushanwen/pi-subagent-workflow 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -0
- package/agents/context-builder.md +1 -3
- package/agents/explorer.md +27 -0
- package/agents/oracle.md +2 -2
- package/agents/orchestrator.md +48 -0
- package/agents/planner.md +1 -3
- package/agents/researcher.md +0 -2
- package/agents/reviewer.md +2 -2
- package/agents/worker.md +0 -2
- package/package.json +5 -3
- package/skills/workflow-script-format/SKILL.md +6 -6
- package/src/execution/__tests__/agent-registry.test.ts +3 -3
- package/src/execution/__tests__/agent-result-mapper.test.ts +24 -2
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +484 -0
- package/src/execution/__tests__/channel-registry-handshake.test.ts +233 -0
- package/src/execution/__tests__/concurrency-pool.test.ts +33 -0
- package/src/execution/__tests__/crash-recovery.test.ts +5 -1
- package/src/execution/__tests__/dialog-queue.test.ts +299 -0
- package/src/execution/__tests__/execute-nesting.test.ts +1 -1
- package/src/execution/__tests__/execute-options-mapper.test.ts +41 -9
- package/src/execution/__tests__/finalize-record.test.ts +173 -0
- package/src/execution/__tests__/gui-mode-dispatch.test.ts +59 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +209 -0
- package/src/execution/__tests__/host-mode.test.ts +87 -0
- package/src/execution/__tests__/index-session-start.test.ts +342 -0
- package/src/execution/__tests__/list-component.test.ts +1 -1
- package/src/execution/__tests__/notifier-flush.test.ts +78 -0
- package/src/execution/__tests__/path-encoding.test.ts +30 -1
- package/src/execution/__tests__/record-store.test.ts +86 -2
- package/src/execution/__tests__/records-cwd-isolation.test.ts +91 -0
- package/src/execution/__tests__/rpc-mode.test.ts +89 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +157 -153
- package/src/execution/__tests__/run-spawn-integration.test.ts +85 -151
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +193 -0
- package/src/execution/__tests__/sdk-contract.test.ts +5 -2
- package/src/execution/__tests__/session-file-gc.test.ts +46 -0
- package/src/execution/__tests__/session-reconstructor.test.ts +20 -0
- package/src/execution/__tests__/session-start-reaper.test.ts +7 -1
- package/src/execution/__tests__/spawn-args.test.ts +14 -19
- package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +189 -0
- package/src/execution/__tests__/stdin-writer.test.ts +353 -0
- package/src/execution/__tests__/subagent-service-abort.test.ts +60 -0
- package/src/execution/__tests__/subagent-service.test.ts +73 -3
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +72 -3
- package/src/execution/__tests__/tool-action.test.ts +27 -5
- package/src/execution/__tests__/ui-channels.test.ts +187 -0
- package/src/execution/__tests__/ui-interaction-model.test.ts +67 -0
- package/src/execution/__tests__/ui-request-handler-factory.test.ts +166 -0
- package/src/execution/__tests__/ui-request-handler.test.ts +204 -0
- package/src/execution/__tests__/ui-request-observability.test.ts +101 -0
- package/src/execution/__tests__/ui-request-queue.test.ts +133 -0
- package/src/execution/__tests__/worktree-manager.test.ts +1 -1
- package/src/execution/agent-registry.ts +1 -1
- package/src/execution/agent-result-mapper.ts +4 -1
- package/src/execution/channel-registry-access.ts +138 -0
- package/src/execution/concurrency-pool.ts +38 -6
- package/src/execution/dialog-queue.ts +329 -0
- package/src/execution/execute-options-mapper.ts +21 -4
- package/src/execution/execution-record.ts +5 -0
- package/src/execution/finalize-record.ts +160 -0
- package/src/execution/get-state-handshake.ts +104 -0
- package/src/execution/host-mode.ts +52 -0
- package/src/execution/manifest-store.ts +206 -0
- package/src/execution/notifier.ts +5 -1
- package/src/execution/path-encoding.ts +18 -0
- package/src/execution/pi-invocation.ts +1 -1
- package/src/execution/record-store.ts +110 -2
- package/src/execution/session-file-gc.ts +25 -3
- package/src/execution/session-reconstructor.ts +11 -0
- package/src/execution/session-runner.ts +228 -32
- package/src/execution/spawn-event-adapter.ts +219 -6
- package/src/execution/stdin-writer.ts +106 -0
- package/src/execution/stream-sink.ts +83 -0
- package/src/execution/subagent-service.ts +230 -235
- package/src/execution/subprocess-agent-runner.ts +16 -4
- package/src/execution/types.ts +23 -3
- package/src/execution/ui-channels.ts +216 -0
- package/src/execution/ui-interaction-model.ts +48 -0
- package/src/execution/ui-request-handler-factory.ts +175 -0
- package/src/execution/ui-request-observability.ts +77 -0
- package/src/execution/ui-request-queue.ts +168 -0
- package/src/index.ts +101 -4
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +84 -0
- package/src/interface/__tests__/workflow-state-file-exposure.test.ts +38 -0
- package/src/interface/__tests__/workflow-tool-prompt.test.ts +50 -0
- package/src/interface/command-actions.ts +77 -0
- package/src/interface/commands.ts +40 -4
- package/src/interface/format.ts +2 -0
- package/src/interface/gui-mappers.ts +83 -0
- package/src/interface/helpers.ts +52 -9
- package/src/interface/list-component.ts +3 -1
- package/src/interface/subagent-actions.ts +44 -24
- package/src/interface/subagent-tool.ts +56 -24
- package/src/interface/subagents.ts +45 -5
- package/src/interface/tool-render.ts +16 -5
- package/src/interface/tool-workflow-script.ts +113 -15
- package/src/interface/tool-workflow.ts +92 -34
- package/src/interface/views/WorkflowsView.ts +13 -4
- package/src/interface/views/__tests__/detail-content-session-file.test.ts +70 -0
- package/src/interface/views/detail-content.ts +20 -0
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +208 -0
- package/src/orchestration/__tests__/agent-call-stream.test.ts +157 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +2 -0
- package/src/orchestration/__tests__/execute-agent-call.test.ts +171 -0
- package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +177 -0
- package/src/orchestration/__tests__/worker-script-builder.test.ts +15 -0
- package/src/orchestration/agent-opts-resolver.ts +11 -2
- package/src/orchestration/error-recovery.ts +131 -23
- package/src/orchestration/execute-agent-call.ts +12 -3
- package/src/orchestration/jsonl-run-store.ts +10 -0
- package/src/orchestration/lifecycle.ts +1 -1
- package/src/orchestration/models/agent-call.ts +7 -0
- package/src/orchestration/models/ports.ts +15 -2
- package/src/orchestration/models/run-spec.ts +6 -0
- package/src/orchestration/models/trace.ts +1 -0
- package/src/orchestration/models/types.ts +19 -0
- package/src/orchestration/node-ops.ts +2 -0
- package/src/orchestration/worker-script-builder.ts +1 -0
- package/workflows/README.md +58 -0
- package/workflows/chain.js +107 -0
- package/workflows/map-reduce.js +142 -0
- package/workflows/parallel.js +131 -0
- package/workflows/scatter-gather.js +146 -0
- package/agents/scout.md +0 -17
- package/examples/README.md +0 -43
- package/examples/chain.example.js +0 -92
- package/examples/map-reduce.example.js +0 -99
- package/examples/parallel.example.js +0 -82
- package/examples/scatter-gather.example.js +0 -106
- package/src/interface/gui-adapter.ts +0 -136
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// src/execution/channel-registry-access.ts
|
|
2
|
+
//
|
|
3
|
+
// Channel registry 的访问入口(本扩展内部用 + 契约文档化导出)。
|
|
4
|
+
//
|
|
5
|
+
// ask-user 等扩展通过本模块注册 channel handler,让 subagent 子进程的 UI 请求
|
|
6
|
+
// 能透传到主进程渲染。所有扩展各自直接读写
|
|
7
|
+
// globalThis[Symbol.for("@zhushanwen/pi-subagents.channelHandshake")],
|
|
8
|
+
// 拿到结构兼容 ChannelRegistryHandshake 的同一握手对象。
|
|
9
|
+
//
|
|
10
|
+
// ── 升级说明(决策 D 修复 M4)─────────────────────────────────
|
|
11
|
+
// 历史问题:原协议槽位直接存 UiChannelRegistry 实例,谁先 session_start 谁创建。
|
|
12
|
+
// ask-user 先到时会自建简化 registry 占位,劫持 canonical 槽位,subagent-workflow
|
|
13
|
+
// 后到时拿到的是 ask-user 的非 canonical 实例,行为不一致。
|
|
14
|
+
//
|
|
15
|
+
// 修复:槽位从「存 registry 实例」升级为「存握手对象 {version, registry?, pending[]}」。
|
|
16
|
+
// canonical registry 实例永远由 subagent-workflow 单一创建点(本模块的
|
|
17
|
+
// getOrCreateChannelRegistry)实例化。ask-user 先到时只往 pending 推 handler,
|
|
18
|
+
// 等 subagent-workflow 来 flush;subagent-workflow 先到时直接创建 registry 并
|
|
19
|
+
// 接收后续 ask-user 推入的 pending。
|
|
20
|
+
//
|
|
21
|
+
// ── 跨扩展协议契约(必须与 ask-user 侧严格一致)──────────────
|
|
22
|
+
// key 字面量: "@zhushanwen/pi-subagents.channelHandshake"
|
|
23
|
+
// ↑ 必须与 ask-user/src/channel-registry-register.ts 完全一致
|
|
24
|
+
// handshake 形状: ChannelRegistryHandshake(version=1)
|
|
25
|
+
// version 守卫: slot.version !== 1 时 console.warn + 丢弃重建(向前兼容未来升级)
|
|
26
|
+
|
|
27
|
+
import { createUiChannelRegistry, type UiChannelRegistry, type ChannelHandler } from "./ui-channels.ts";
|
|
28
|
+
|
|
29
|
+
/** 进程级 channel 握手的 globalThis key(Symbol.for 跨模块共享)。
|
|
30
|
+
*
|
|
31
|
+
* **协议契约**:字面量 `"@zhushanwen/pi-subagents.channelHandshake"` 必须与
|
|
32
|
+
* ask-user 扩展的 `ask-user/src/channel-registry-register.ts` 完全一致——
|
|
33
|
+
* 两边读写同一个 Symbol.for key 才能拿到同一握手对象。
|
|
34
|
+
*
|
|
35
|
+
* 改名历史:原 `CHANNEL_REGISTRY_KEY`(字面量 `...channelRegistry`)在决策 D
|
|
36
|
+
* 中升级为 `CHANNEL_HANDSHAKE_KEY`(字面量 `...channelHandshake`),槽位形状
|
|
37
|
+
* 从 registry 实例改为握手对象。 */
|
|
38
|
+
export const CHANNEL_HANDSHAKE_KEY = Symbol.for("@zhushanwen/pi-subagents.channelHandshake");
|
|
39
|
+
|
|
40
|
+
/** 握手版本。未来若形状不兼容升级,递增此常量并在 getOrCreateChannelRegistry
|
|
41
|
+
* 的 version 守卫里加迁移逻辑。 */
|
|
42
|
+
const HANDSHAKE_VERSION = 1 as const;
|
|
43
|
+
|
|
44
|
+
/** pending channel handler 条目。ask-user 先到、registry 未就绪时临时存放。 */
|
|
45
|
+
export interface ChannelRegistryHandshakePendingEntry {
|
|
46
|
+
channel: string;
|
|
47
|
+
handler: ChannelHandler;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 跨扩展 channel registry 握手对象(globalThis 槽位形状)。
|
|
51
|
+
*
|
|
52
|
+
* - `version`:协议版本,当前固定 1。读取时若 ≠1 视为不兼容,丢弃重建。
|
|
53
|
+
* - `registry`:canonical UiChannelRegistry 实例。仅由 subagent-workflow 的
|
|
54
|
+
* getOrCreateChannelRegistry 创建并填充。未就绪时为 undefined。
|
|
55
|
+
* - `pending`:ask-user 在 registry 就绪前推入的 handler 条目。
|
|
56
|
+
* subagent-workflow 创建 registry 后逐条 flush(注册),然后清空。 */
|
|
57
|
+
export interface ChannelRegistryHandshake {
|
|
58
|
+
version: typeof HANDSHAKE_VERSION;
|
|
59
|
+
registry?: UiChannelRegistry;
|
|
60
|
+
pending: ChannelRegistryHandshakePendingEntry[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 读取握手槽位;若 version 不匹配则视为缺失(warn + 返回 undefined)。
|
|
64
|
+
* 类型不安全的 globalThis 反射访问集中在本函数,外层逻辑保持类型严谨。 */
|
|
65
|
+
function readHandshakeSlot(): ChannelRegistryHandshake | undefined {
|
|
66
|
+
const slot = Reflect.get(globalThis, CHANNEL_HANDSHAKE_KEY) as unknown;
|
|
67
|
+
if (slot === undefined) return undefined;
|
|
68
|
+
// 形状校验:必须是对象且 version===1,否则视为不兼容
|
|
69
|
+
if (typeof slot !== "object" || slot === null) {
|
|
70
|
+
console.warn(
|
|
71
|
+
"[pi-subagent-workflow] channel handshake slot is not an object; discarding and recreating.",
|
|
72
|
+
);
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const version = (slot as { version?: unknown }).version;
|
|
76
|
+
if (version !== HANDSHAKE_VERSION) {
|
|
77
|
+
console.warn(
|
|
78
|
+
`[pi-subagent-workflow] channel handshake version mismatch (got ${String(
|
|
79
|
+
version,
|
|
80
|
+
)}, expected ${HANDSHAKE_VERSION}); discarding and recreating.`,
|
|
81
|
+
);
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
// version 正确,但 pending 可能被恶意/错误地塞了非数组;防御性处理
|
|
85
|
+
const candidate = slot as ChannelRegistryHandshake;
|
|
86
|
+
if (!Array.isArray(candidate.pending)) {
|
|
87
|
+
console.warn(
|
|
88
|
+
"[pi-subagent-workflow] channel handshake pending is not an array; discarding and recreating.",
|
|
89
|
+
);
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
return candidate;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 将 pending 条目逐个注册进 registry,注册完清空 pending。
|
|
96
|
+
* pending 内部 entry 的形状由 ask-user 侧保证,此处不再二次校验。 */
|
|
97
|
+
function flushPending(registry: UiChannelRegistry, pending: ChannelRegistryHandshakePendingEntry[]): void {
|
|
98
|
+
for (const entry of pending) {
|
|
99
|
+
registry.register(entry.channel, entry.handler);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 获取或创建进程级 channel registry(canonical 单例)。
|
|
104
|
+
*
|
|
105
|
+
* **唯一创建点**:canonical UiChannelRegistry 实例仅由本函数创建。
|
|
106
|
+
* ask-user 扩展绝不自建 registry——它在 registry 未就绪时只往 slot.pending 推条目。
|
|
107
|
+
*
|
|
108
|
+
* 行为分支:
|
|
109
|
+
* 1. 槽位不存在或 version ≠ 1 → warn(version 不匹配时)+ 建新 slot
|
|
110
|
+
* `{version:1, pending:[], registry:<newly created>}`
|
|
111
|
+
* 2. 槽位存在但 registry 未就绪(ask-user 先到过,塞过 pending)→
|
|
112
|
+
* 创建 canonical registry + flush pending + 清空 pending
|
|
113
|
+
* 3. 槽位存在且 registry 已就绪(本函数已被调用过)→
|
|
114
|
+
* 直接返回同一实例引用(===),不重建、不重复 flush
|
|
115
|
+
*
|
|
116
|
+
* @returns 进程级 canonical UiChannelRegistry 单例(永不返回 undefined) */
|
|
117
|
+
export function getOrCreateChannelRegistry(): UiChannelRegistry {
|
|
118
|
+
let slot = readHandshakeSlot();
|
|
119
|
+
if (slot === undefined) {
|
|
120
|
+
// 分支 1:无合规槽位,新建并立即创建 canonical registry
|
|
121
|
+
const registry = createUiChannelRegistry();
|
|
122
|
+
slot = { version: HANDSHAKE_VERSION, registry, pending: [] };
|
|
123
|
+
Reflect.set(globalThis, CHANNEL_HANDSHAKE_KEY, slot);
|
|
124
|
+
return registry;
|
|
125
|
+
}
|
|
126
|
+
if (slot.registry === undefined) {
|
|
127
|
+
// 分支 2:ask-user 先到过,flush pending 进 canonical registry
|
|
128
|
+
const registry = createUiChannelRegistry();
|
|
129
|
+
flushPending(registry, slot.pending);
|
|
130
|
+
slot.pending = [];
|
|
131
|
+
slot.registry = registry;
|
|
132
|
+
return registry;
|
|
133
|
+
}
|
|
134
|
+
// 分支 3:registry 已就绪,返回同一实例
|
|
135
|
+
return slot.registry;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export type { UiChannelRegistry, ChannelHandler };
|
|
@@ -2,17 +2,22 @@
|
|
|
2
2
|
//
|
|
3
3
|
// 并发控制 + 优先级排队。background=1000(单一优先级,保留 priority 机制供未来扩展)。
|
|
4
4
|
|
|
5
|
-
/** 队列条目:优先级 + resolver + 入队序号(同优先级 FIFO)。 */
|
|
5
|
+
/** 队列条目:优先级 + resolver + rejecter + 入队序号(同优先级 FIFO)。 */
|
|
6
6
|
interface QueueEntry {
|
|
7
7
|
priority: number;
|
|
8
8
|
resolve: () => void;
|
|
9
|
+
reject: (err: Error) => void;
|
|
9
10
|
seq: number;
|
|
11
|
+
/** abort listener(用于 resolve/abort 时清理)。 */
|
|
12
|
+
onAbort?: () => void;
|
|
13
|
+
/** 关联的 signal(用于 resolve 时 removeEventListener)。 */
|
|
14
|
+
signal?: AbortSignal;
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
/** 并发池接口(可注入,便于测试 mock)。 */
|
|
13
18
|
export interface ConcurrencyPool {
|
|
14
|
-
/** 按优先级排队获取槽位(0=最高)。可选 effectiveMaxConcurrent
|
|
15
|
-
acquire(priority: number, effectiveMaxConcurrent?: number): Promise<void>;
|
|
19
|
+
/** 按优先级排队获取槽位(0=最高)。可选 effectiveMaxConcurrent 覆盖实例级默认配额。可选 AbortSignal 在 abort 时 reject 排队条目。 */
|
|
20
|
+
acquire(priority: number, effectiveMaxConcurrent?: number, signal?: AbortSignal): Promise<void>;
|
|
16
21
|
/** 归还槽位。必须无条件执行(finally)。 */
|
|
17
22
|
release(): void;
|
|
18
23
|
/** 当前已占用槽位数(诊断/widget 用)。 */
|
|
@@ -45,7 +50,7 @@ export class DefaultConcurrencyPool implements ConcurrencyPool {
|
|
|
45
50
|
this.maxConcurrent = Math.max(1, maxConcurrent);
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
acquire(priority: number, effectiveMaxConcurrent?: number): Promise<void> {
|
|
53
|
+
acquire(priority: number, effectiveMaxConcurrent?: number, signal?: AbortSignal): Promise<void> {
|
|
49
54
|
// effectiveMaxConcurrent 覆盖实例级默认配额(分层配额:调用方传 max(1, maxConcurrent - depth))。
|
|
50
55
|
// 不修改实例级 maxConcurrent——实例配额是全局共享上限,分层配额是本次 acquire 的局部上限。
|
|
51
56
|
const effective = effectiveMaxConcurrent ?? this.maxConcurrent;
|
|
@@ -53,8 +58,31 @@ export class DefaultConcurrencyPool implements ConcurrencyPool {
|
|
|
53
58
|
this._active += 1;
|
|
54
59
|
return Promise.resolve();
|
|
55
60
|
}
|
|
56
|
-
return new Promise<void>((resolve) => {
|
|
57
|
-
|
|
61
|
+
return new Promise<void>((resolve, reject) => {
|
|
62
|
+
const entry: QueueEntry = { priority, resolve, reject, seq: this.seqCounter++ };
|
|
63
|
+
// H2: abort 时 reject 排队条目并从 queue 移除,防止永久挂起
|
|
64
|
+
if (signal) {
|
|
65
|
+
if (signal.aborted) {
|
|
66
|
+
// S1: abort reject 需带 name="AbortError",对齐 concurrency-gate.ts 的 AbortError 语义
|
|
67
|
+
const err = new Error("acquire aborted");
|
|
68
|
+
err.name = "AbortError";
|
|
69
|
+
reject(err);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
entry.signal = signal;
|
|
73
|
+
entry.onAbort = (): void => {
|
|
74
|
+
const idx = this.queue.indexOf(entry);
|
|
75
|
+
if (idx >= 0) {
|
|
76
|
+
this.queue.splice(idx, 1);
|
|
77
|
+
// S1: abort reject 需带 name="AbortError",与 pre-aborted 分支一致
|
|
78
|
+
const err = new Error("acquire aborted");
|
|
79
|
+
err.name = "AbortError";
|
|
80
|
+
reject(err);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
signal.addEventListener("abort", entry.onAbort, { once: true });
|
|
84
|
+
}
|
|
85
|
+
this.queue.push(entry);
|
|
58
86
|
});
|
|
59
87
|
}
|
|
60
88
|
|
|
@@ -70,6 +98,10 @@ export class DefaultConcurrencyPool implements ConcurrencyPool {
|
|
|
70
98
|
}
|
|
71
99
|
}
|
|
72
100
|
const next = this.queue.splice(bestIdx, 1)[0];
|
|
101
|
+
// H2: 条目已获槽位——移除 abort listener(防 listener 泄漏到长生命周期 signal)
|
|
102
|
+
if (next.onAbort && next.signal) {
|
|
103
|
+
next.signal.removeEventListener("abort", next.onAbort);
|
|
104
|
+
}
|
|
73
105
|
next.resolve();
|
|
74
106
|
// active 不变(一个离开队列立即进入活跃)
|
|
75
107
|
} else if (this._active > 0) {
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// src/execution/dialog-queue.ts
|
|
2
|
+
//
|
|
3
|
+
// L2 跨子进程全局 dialog 串行队列。
|
|
4
|
+
//
|
|
5
|
+
// 进程单例语义:跨所有子进程共享,串行所有 dialog 类 UI 请求
|
|
6
|
+
//(isDialogMethod(method)===true,即 select/confirm/input/editor)。
|
|
7
|
+
//
|
|
8
|
+
// 设计动机(.fix-plans/00-master-summary.md §一 冲突 3):
|
|
9
|
+
// - L1 per-child 队列(session-runner.createUiRequestQueue)只解决同一子进程内的串行,
|
|
10
|
+
// 多个并行子进程仍可同时把 dialog 请求涌向父 UI(争输入焦点)。
|
|
11
|
+
// - L2 全局队列在主 agent handler 入口前再串行一次,保证同一时刻主 agent 只呈现一个 dialog。
|
|
12
|
+
// - 排队绑定 method 交互模型(dialog 才排队),不绑定 channel——排队是 Pi 协议固有属性。
|
|
13
|
+
//
|
|
14
|
+
// SR-4(child close reject):入队项带 child 引用,child close 时把该 child 的 pending dialog
|
|
15
|
+
// 全部 resolve 为 {cancelled:true},防 Promise 永挂 + 内存泄漏。
|
|
16
|
+
//
|
|
17
|
+
// handler 抛错兜底:catch → 回 {cancelled:true} → 继续处理下一个。不能让一个失败卡死队列。
|
|
18
|
+
//
|
|
19
|
+
// 调用方约定:只对 dialog 类(isDialogMethod===true)调 enqueue;fire-and-forget 由调用方
|
|
20
|
+
// 直接调 handler(见 ui-request-handler-factory.ts),不经过本队列。enqueue 内仍防御性兼容
|
|
21
|
+
// fire-and-forget(万一调用方未判):直接调 handler 返回,不入队串行。调用方不应依赖此防御。
|
|
22
|
+
|
|
23
|
+
import { isDialogMethod } from "./ui-interaction-model.ts";
|
|
24
|
+
|
|
25
|
+
// ── 类型定义(本模块是 UiRequest/UiResponse/UiRequestHandler 的规范来源) ──
|
|
26
|
+
// session-runner.ts 和 ui-channels.ts 复用这些类型(session-runner 再导出供测试 import)。
|
|
27
|
+
|
|
28
|
+
/** Pi extension_ui_request 的方法枚举(dialog + fire-and-forget 两类)。
|
|
29
|
+
* dialog 类:select/confirm/input/editor(占输入焦点,等响应)。
|
|
30
|
+
* fire-and-forget 类:notify/setStatus/setWidget/setTitle/set_editor_text(纯展示/写入)。
|
|
31
|
+
* (string & {}) 兜底:Pi 未来新增 method 或未知 method 走字符串字面量类型。 */
|
|
32
|
+
export type UiMethod =
|
|
33
|
+
| "select"
|
|
34
|
+
| "confirm"
|
|
35
|
+
| "input"
|
|
36
|
+
| "editor"
|
|
37
|
+
| "notify"
|
|
38
|
+
| "setStatus"
|
|
39
|
+
| "setWidget"
|
|
40
|
+
| "setTitle"
|
|
41
|
+
| "set_editor_text"
|
|
42
|
+
| (string & {});
|
|
43
|
+
|
|
44
|
+
/** UI 请求(session-runner 构造后传给 handler)。
|
|
45
|
+
*
|
|
46
|
+
* method 是判别字段,决定排队策略(dialog 排队)和业务路由(channel 分发)。
|
|
47
|
+
* method 特定字段按 method 可选出现(与 ExtensionUiRequest 1:1,由 session-runner 从
|
|
48
|
+
* ExtensionUiRequest 平铺构造)。channel/channelPayload 由 parseChannel 填充。
|
|
49
|
+
*
|
|
50
|
+
* 契约来源:.fix-plans/00-master-summary.md §二 2.2。 */
|
|
51
|
+
export interface UiRequest {
|
|
52
|
+
/** Pi rpc-types.ts 的 method(select/confirm/input/editor 为 dialog 类)。 */
|
|
53
|
+
method: UiMethod;
|
|
54
|
+
/** 请求 id(从 extension_ui_request envelope 顶层提取,用于 response 关联)。 */
|
|
55
|
+
id: string;
|
|
56
|
+
// method 特定字段(按 method 可选,与 ExtensionUiRequest 1:1)
|
|
57
|
+
title?: string;
|
|
58
|
+
options?: string[];
|
|
59
|
+
message?: string;
|
|
60
|
+
placeholder?: string;
|
|
61
|
+
prefill?: string;
|
|
62
|
+
notifyType?: string;
|
|
63
|
+
statusKey?: string;
|
|
64
|
+
statusText?: string | undefined;
|
|
65
|
+
widgetKey?: string;
|
|
66
|
+
widgetLines?: string[] | undefined;
|
|
67
|
+
widgetPlacement?: "aboveEditor" | "belowEditor";
|
|
68
|
+
text?: string;
|
|
69
|
+
timeout?: number;
|
|
70
|
+
/** channel 名(从 method 对应字段的 NUL 前缀解析)。
|
|
71
|
+
* select → 从 title 解析;setWidget → 从 widgetLines[0] 解析;其他 → undefined。
|
|
72
|
+
* 已知值:"ask_user"(select)、"gui_widget"(setWidget)。handler 按 channel 分发。 */
|
|
73
|
+
channel?: string;
|
|
74
|
+
/** channel 解析后的结构化 payload(已 JSON.parse)。
|
|
75
|
+
* ask_user: {questions, allowCancel};gui_widget: {component};无 channel: undefined。 */
|
|
76
|
+
channelPayload?: unknown;
|
|
77
|
+
/** 内部元数据字段:发起该 UI 请求的子进程 pid(由 session-runner.handleUiRequest 从
|
|
78
|
+
* child.pid 填入)。L2 队列据此关联 rejectChildDialogs(child close 时批量 reject)。
|
|
79
|
+
* 下划线前缀表示内部字段,非 Pi 协议字段,不参与 stdin 回写。 */
|
|
80
|
+
_childPid?: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** UI 响应(handler 返回,session-runner 按 shape 回写 stdin)。
|
|
84
|
+
* - {value}: select/input/editor 的答案
|
|
85
|
+
* - {confirmed}: confirm 的答案
|
|
86
|
+
* - {cancelled}: 取消(child close / handler 抛错 / 用户取消)
|
|
87
|
+
* - {ack}: fire-and-forget(当前不透传到 TUI,留作协议完整) */
|
|
88
|
+
export type UiResponse =
|
|
89
|
+
| { value: string }
|
|
90
|
+
| { confirmed: boolean }
|
|
91
|
+
| { cancelled: true }
|
|
92
|
+
| { ack: true };
|
|
93
|
+
|
|
94
|
+
/** UI 请求 handler 签名(单函数,按 req.method 内部路由)。
|
|
95
|
+
* 实现方负责:channel 业务路由(ask_user → AskUserComponent)+ 默认转发(ctx.ui.*)。
|
|
96
|
+
* 抛错由调用方(DialogGlobalQueue / session-runner)兜底为 {cancelled:true}。 */
|
|
97
|
+
export type UiRequestHandler = (req: UiRequest) => Promise<UiResponse>;
|
|
98
|
+
|
|
99
|
+
// ── DialogGlobalQueue 实现 ──
|
|
100
|
+
|
|
101
|
+
/** 入队项的 child 引用形状(只取 pid 用于 rejectChildDialogs 匹配)。 */
|
|
102
|
+
export interface DialogChildRef {
|
|
103
|
+
pid: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** enqueue 的可选项。child 用于 rejectChildDialogs 关联(child close 时批量 reject)。 */
|
|
107
|
+
export interface EnqueueOptions {
|
|
108
|
+
child?: DialogChildRef;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 队列内一项:请求 + handler + resolve + 所属 child。
|
|
112
|
+
* pending 状态持有 resolve,handler 完成 / rejectChildDialogs 时调它 settle Promise。
|
|
113
|
+
* settled 标志保证只 settle 一次(rejectChildDialogs 与 handler 完成可能竞争)。 */
|
|
114
|
+
interface QueueItem {
|
|
115
|
+
req: UiRequest;
|
|
116
|
+
handler: UiRequestHandler;
|
|
117
|
+
resolve: (resp: UiResponse) => void;
|
|
118
|
+
childPid: number | undefined;
|
|
119
|
+
/** 是否已 settle(防 handler 完成 / rejectChildDialogs 重复 resolve)。 */
|
|
120
|
+
settled: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* L2 跨子进程全局 dialog 串行队列(进程单例)。
|
|
125
|
+
*
|
|
126
|
+
* 用法(createUiRequestHandlerForMode 返回的总 handler 内):
|
|
127
|
+
* ```ts
|
|
128
|
+
* const dialogQueue = new DialogGlobalQueue();
|
|
129
|
+
* return async (req: UiRequest) => {
|
|
130
|
+
* // 调用方负责判断:dialog 入队,fire-and-forget 直接调 realHandler
|
|
131
|
+
* if (isDialogMethod(req.method)) return dialogQueue.enqueue(req, realHandler);
|
|
132
|
+
* return realHandler(req);
|
|
133
|
+
* };
|
|
134
|
+
* ```
|
|
135
|
+
*
|
|
136
|
+
* 语义保证:
|
|
137
|
+
* - FIFO 串行:前一个 handler settle 后才处理下一个
|
|
138
|
+
* - SR-4:rejectChildDialogs(child) 把该 child 的 pending 全部 resolve 为 {cancelled:true}
|
|
139
|
+
* - handler 抛错兜底:catch → {cancelled:true} → 继续下一个(队列不卡死)
|
|
140
|
+
* - 调用方约定只对 dialog 类调 enqueue;fire-and-forget 由调用方直接调 handler 不入队
|
|
141
|
+
*(enqueue 内仍防御性兼容 fire-and-forget,但不保证行为)
|
|
142
|
+
*
|
|
143
|
+
* 线程模型:纯 Promise + 微任务驱动,无锁。Node 单线程 event loop 保证队列状态一致。
|
|
144
|
+
*
|
|
145
|
+
* 单 session 假设(M-2,与 index.ts lastSessionId 同源):本队列是进程级单例(实例挂在
|
|
146
|
+
* globalThis[Symbol.for("@zhushanwen/pi-subagents.dialogQueue")],见 getOrCreateDialogQueue)。
|
|
147
|
+
* rejectAll()/clear() 清空所有 pending dialog——无 per-session 隔离。Pi 当前架构保证单进程
|
|
148
|
+
* 单 session 串行(同进程不会并发多个 session),故 session_shutdown 调 rejectAll() 只会清掉
|
|
149
|
+
* 当前 session 的 pending。若未来 Pi 支持同进程多 session 并发,session A 退出会误清 session B
|
|
150
|
+
* 的 pending dialog——届时需改为 per-session 隔离(入队项 QueueItem 带 sessionId,rejectAll
|
|
151
|
+
* 改 rejectAllForSession(sessionId),session_shutdown 只清当前 session)。
|
|
152
|
+
*/
|
|
153
|
+
export class DialogGlobalQueue {
|
|
154
|
+
/** 等待处理的队列(FIFO)。正在处理的项从 queue shift 出后由 current 持有。 */
|
|
155
|
+
private queue: QueueItem[] = [];
|
|
156
|
+
/** 正在处理的项(handler 已调、未 settle)。用于 rejectChildDialogs 取消占位中的 dialog。 */
|
|
157
|
+
private current: QueueItem | undefined;
|
|
158
|
+
private processing = false;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 入队一个 UI 请求,返回 Promise<UiResponse>。
|
|
162
|
+
*
|
|
163
|
+
* 调用方约定:只对 dialog 类(isDialogMethod===true)调 enqueue。fire-and-forget 由
|
|
164
|
+
* 调用方(ui-request-handler-factory.ts)在 enqueue 前判 isDialogMethod 后直接调 handler,
|
|
165
|
+
* 不经过本队列。enqueue 内仍防御性兼容 fire-and-forget(万一调用方未判):直接调 handler 返回,
|
|
166
|
+
* 不入队串行,但调用方不应依赖此防御行为。
|
|
167
|
+
*
|
|
168
|
+
* dialog 项处理(TC-E4 case 1):进队列 FIFO 串行,等前一个 settle 后才调 handler
|
|
169
|
+
*(争输入焦点,防并发弹窗)。
|
|
170
|
+
*
|
|
171
|
+
* handler 抛错兜底:catch → 回 {cancelled:true}(dialog 路径,队列不卡死)。
|
|
172
|
+
* SR-4:opts.child 用于 rejectChildDialogs 关联(dialog 项会被批量 reject,含 current)。
|
|
173
|
+
*
|
|
174
|
+
* @param req UI 请求(约定只传 dialog 类;fire-and-forget 防御性兼容)
|
|
175
|
+
* @param handler 真正执行请求的 handler(TUI/GUI 模式分流后的 realHandler)
|
|
176
|
+
* @param opts 可选 child 引用(用于 rejectChildDialogs 关联)
|
|
177
|
+
* @returns handler 的响应;dialog 抛错时回 {cancelled:true};child close 时回 {cancelled:true}
|
|
178
|
+
*/
|
|
179
|
+
enqueue(
|
|
180
|
+
req: UiRequest,
|
|
181
|
+
handler: UiRequestHandler,
|
|
182
|
+
opts?: EnqueueOptions,
|
|
183
|
+
): Promise<UiResponse> {
|
|
184
|
+
// 防御性兼容 fire-and-forget:调用方约定不应传此类 req 进 enqueue(factory 层已判
|
|
185
|
+
// isDialogMethod 直接调 handler),但万一调用方未判,这里直接调 handler 不入队,避免阻塞。
|
|
186
|
+
// 直接返回 handler 真实结果(不做兜底形变为 cancelled——fire-and-forget 无串行语义)。
|
|
187
|
+
if (!isDialogMethod(req.method)) {
|
|
188
|
+
return handler(req);
|
|
189
|
+
}
|
|
190
|
+
// dialog:入队 FIFO 串行
|
|
191
|
+
return new Promise<UiResponse>((resolve) => {
|
|
192
|
+
this.queue.push({
|
|
193
|
+
req,
|
|
194
|
+
handler,
|
|
195
|
+
resolve,
|
|
196
|
+
childPid: opts?.child?.pid,
|
|
197
|
+
settled: false,
|
|
198
|
+
});
|
|
199
|
+
void this.processNext();
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* settle 一个 item(幂等)。handler 完成 / rejectChildDialogs / rejectAll 都通过本方法,
|
|
205
|
+
* settled 标志保证只 settle 一次(防竞争)。
|
|
206
|
+
*
|
|
207
|
+
* #19 单一推进点:本方法 settle Promise + 清状态后,**唯一**调 processNext 推进队列。
|
|
208
|
+
* processNext 尾部不再调 processNext(旧代码双重推进,虽靠 processing 标志幂等,但语义混乱)。
|
|
209
|
+
* 为什么推进必须在 settleItem 而非 processNext 尾部:rejectChildDialogs 取消一个永不 settle
|
|
210
|
+
* 的 current(handler 等用户输入卡死)时,processNext 的 `await item.handler` 永不 resume,
|
|
211
|
+
* 尾部不会执行;只有 settleItem 里的 processNext 才能打破死锁,推进下一个。
|
|
212
|
+
*/
|
|
213
|
+
private settleItem(item: QueueItem, resp: UiResponse): void {
|
|
214
|
+
if (item.settled) return;
|
|
215
|
+
item.settled = true;
|
|
216
|
+
item.resolve(resp);
|
|
217
|
+
// 若是正在处理的项,清空 current/processing 并推进队列(唯一推进点)。
|
|
218
|
+
// 非当前项(队列中被 reject)只 settle Promise,不影响 current/推进。
|
|
219
|
+
if (this.current === item) {
|
|
220
|
+
this.current = undefined;
|
|
221
|
+
this.processing = false;
|
|
222
|
+
void this.processNext();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* SR-4:把指定 child 的所有 pending dialog resolve 为 {cancelled:true}。
|
|
228
|
+
*
|
|
229
|
+
* 触发场景:子进程 close(用户取消 / crash / 超时 kill)时,其 pending dialog 的 handler
|
|
230
|
+
* 可能永不 settle(等用户输入),导致 Promise 永挂 + 内存泄漏。本方法批量清理。
|
|
231
|
+
*
|
|
232
|
+
* 处理范围(TC-E4 case 2):
|
|
233
|
+
* - 正在处理中(current)的该 child 项:settle {cancelled:true},解阻塞队列推进下一个
|
|
234
|
+
* (关键:handler 可能永不 settle,必须由这里打破死锁)
|
|
235
|
+
* - 队列中等待处理的该 child 项:settle {cancelled:true} 并移除
|
|
236
|
+
*
|
|
237
|
+
* 不影响其他 child 的 pending dialog(TC-E4 case 2 子测试 2)。
|
|
238
|
+
*/
|
|
239
|
+
rejectChildDialogs(child: DialogChildRef): void {
|
|
240
|
+
// 先处理正在处理的项(可能永不 settle,必须由这里解阻塞)
|
|
241
|
+
if (this.current && this.current.childPid === child.pid) {
|
|
242
|
+
this.settleItem(this.current, { cancelled: true });
|
|
243
|
+
}
|
|
244
|
+
// 再处理队列中等待的项
|
|
245
|
+
if (this.queue.length === 0) return;
|
|
246
|
+
const remaining: QueueItem[] = [];
|
|
247
|
+
for (const item of this.queue) {
|
|
248
|
+
if (item.childPid === child.pid) {
|
|
249
|
+
// settle 该 child 的 pending Promise 为 cancelled
|
|
250
|
+
this.settleItem(item, { cancelled: true });
|
|
251
|
+
} else {
|
|
252
|
+
remaining.push(item);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
this.queue = remaining;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* 处理队列下一项(FIFO)。
|
|
260
|
+
*
|
|
261
|
+
* processing 标志保证串行:handler 运行期间 processing=true,新的 processNext 调用直接返回;
|
|
262
|
+
* handler settle 后由 settleItem 清 processing=false 并推进下一项(#19 单一推进点)。
|
|
263
|
+
*
|
|
264
|
+
* handler 抛错兜底(TC-E4 case 3):catch → settle {cancelled:true} → 继续。
|
|
265
|
+
* 不能让一个失败卡死队列(processing 永远 true)。
|
|
266
|
+
*/
|
|
267
|
+
private async processNext(): Promise<void> {
|
|
268
|
+
if (this.processing) return;
|
|
269
|
+
if (this.queue.length === 0) return;
|
|
270
|
+
this.processing = true;
|
|
271
|
+
const item = this.queue.shift()!;
|
|
272
|
+
this.current = item;
|
|
273
|
+
try {
|
|
274
|
+
const resp = await item.handler(item.req);
|
|
275
|
+
// handler 完成:settle(若已被 rejectChildDialogs 抢先 settle 则 noop)。
|
|
276
|
+
// settleItem 内会清 current/processing 并推进队列(#19 单一推进点)。
|
|
277
|
+
this.settleItem(item, resp);
|
|
278
|
+
} catch {
|
|
279
|
+
// handler 抛错兜底:回 cancelled,不向上抛(队列不能卡死)
|
|
280
|
+
this.settleItem(item, { cancelled: true });
|
|
281
|
+
}
|
|
282
|
+
// #19:不在尾部再调 processNext——推进统一由 settleItem 负责(避免双重推进)。
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* #10:把所有 pending dialog(queue + current)全部 settle 为 {cancelled:true},
|
|
287
|
+
* 并清空 queue/current/processing 状态。session_shutdown 调用,保证不留永挂 Promise。
|
|
288
|
+
*
|
|
289
|
+
* 约定签名:rejectAll(): void(无参,返 void)。Group C 的 index.ts session_shutdown 依赖此契约。
|
|
290
|
+
*
|
|
291
|
+
* 幂等:依赖 settleItem 的 settled 标志——重复调用只会对已 settled 项 noop。
|
|
292
|
+
* 顺序敏感(#19 推进点在 settleItem):必须先清空 queue 数组再 settle current,
|
|
293
|
+
* 否则 settleItem(current) 同步触发的 processNext 会从旧 queue 抢占下一项作为新 current,
|
|
294
|
+
* 避开本方法的 cancel 语义。清空后 processNext 看到空队列直接返回,新 current 不会被抢占。
|
|
295
|
+
*
|
|
296
|
+
* 单 session 假设(M-2):见类注释。本方法清空所有 pending 不分 session——依赖 Pi 单进程
|
|
297
|
+
* 单 session 串行保证。session_shutdown handler(index.ts)调用本方法时,进程内只会有当前
|
|
298
|
+
* session 的 pending dialog。多 session 并发场景的迁移策略(rejectAllForSession)见类注释。
|
|
299
|
+
*/
|
|
300
|
+
rejectAll(): void {
|
|
301
|
+
// 先捕获并清空队列——防 settleItem(current) 触发的 processNext 抢占同队列下一项
|
|
302
|
+
const items = this.queue;
|
|
303
|
+
this.queue = [];
|
|
304
|
+
// 再 settle current(processNext 此时看到空队列,不会抢占)
|
|
305
|
+
if (this.current) {
|
|
306
|
+
this.settleItem(this.current, { cancelled: true });
|
|
307
|
+
}
|
|
308
|
+
// settle 所有原队列项(Promise 必须 resolve,不能挂)
|
|
309
|
+
for (const item of items) {
|
|
310
|
+
this.settleItem(item, { cancelled: true });
|
|
311
|
+
}
|
|
312
|
+
// 状态清理(幂等:settleItem(current) 可能已清)
|
|
313
|
+
this.current = undefined;
|
|
314
|
+
this.processing = false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** 清空队列(dispose 用)。pending 项的 Promise 不 settle(dispose 时调用方已不关心)。
|
|
318
|
+
* 如需 settle,dispose 前应先 rejectChildDialogs 或 rejectAll。 */
|
|
319
|
+
clear(): void {
|
|
320
|
+
this.queue = [];
|
|
321
|
+
this.current = undefined;
|
|
322
|
+
this.processing = false;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** 当前队列长度(测试/诊断用)。含等待处理项(不含 current)。 */
|
|
326
|
+
get size(): number {
|
|
327
|
+
return this.queue.length;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
@@ -9,6 +9,12 @@ import type { AgentCallOpts } from "../orchestration/models/types.ts";
|
|
|
9
9
|
import type { ModelInfo } from "./model-resolver.ts";
|
|
10
10
|
import type { ExecuteOptions } from "./types.ts";
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* slug 最大长度(字符)。subagent/workflow 创建时 slug 超过此值会被截断。
|
|
14
|
+
* 与 subagent tool schema 的 maxLength: 20 保持一致。
|
|
15
|
+
*/
|
|
16
|
+
export const SLUG_MAX_LENGTH = 20;
|
|
17
|
+
|
|
12
18
|
/**
|
|
13
19
|
* D-A2: AgentCallOpts → ExecuteOptions 映射。
|
|
14
20
|
*
|
|
@@ -17,30 +23,41 @@ import type { ExecuteOptions } from "./types.ts";
|
|
|
17
23
|
*
|
|
18
24
|
* 映射规则:
|
|
19
25
|
* prompt → task
|
|
26
|
+
* description → slug(≤20 字符,超长截断。缺失时回落 agent 名)
|
|
20
27
|
* agent → agent
|
|
21
28
|
* schema → schema
|
|
22
29
|
* schemaEnv → schemaEnv(D-A6 bridge)
|
|
23
30
|
* cwd → cwd
|
|
24
|
-
* model → model
|
|
31
|
+
* model → model(显式 override 透传,ctxModel 不再压扁为 id 填底)
|
|
32
|
+
* ctxModel → ctxModel(完整 ModelInfo 对象透传,让 resolveModel 走第 3 层透明传递)
|
|
25
33
|
* skillPath → skillPath
|
|
34
|
+
* thinkingLevel → thinkingLevel(M1: 否则下游 subagent-service 读到 undefined)
|
|
35
|
+
* systemPromptFiles → appendSystemPrompt(M2: resolveAgentOpts 写盘的临时文件需被 session-runner 消费)
|
|
26
36
|
*
|
|
27
37
|
* 忽略字段(委托后由 executeAndAwait 内部机制替代):
|
|
28
|
-
* systemPromptFiles —— resolveIdentity 从 agentConfig.systemPrompt 读
|
|
29
38
|
* timeoutMs —— mergeTimeoutSignal 单独处理
|
|
30
|
-
* scene
|
|
39
|
+
* scene —— subagents 不消费
|
|
40
|
+
*
|
|
41
|
+
* description 原先被忽略,现作为 slug 透传(ExecuteOptions.slug 必填)。
|
|
31
42
|
*/
|
|
32
43
|
export function mapToExecuteOptions(
|
|
33
44
|
opts: AgentCallOpts,
|
|
34
45
|
ctxModel?: ModelInfo,
|
|
35
46
|
): ExecuteOptions {
|
|
47
|
+
// slug:优先 description,缺失回落 agent 名(保证非空)。超长截断。
|
|
48
|
+
const rawSlug = opts.description ?? opts.agent ?? "workflow-agent";
|
|
36
49
|
return {
|
|
37
50
|
task: opts.prompt,
|
|
51
|
+
slug: rawSlug.length > SLUG_MAX_LENGTH ? rawSlug.slice(0, SLUG_MAX_LENGTH) : rawSlug,
|
|
38
52
|
agent: opts.agent,
|
|
39
53
|
schema: opts.schema,
|
|
40
54
|
schemaEnv: opts.schemaEnv,
|
|
41
55
|
cwd: opts.cwd,
|
|
42
|
-
model: opts.model
|
|
56
|
+
model: opts.model,
|
|
57
|
+
ctxModel,
|
|
43
58
|
skillPath: opts.skillPath,
|
|
59
|
+
thinkingLevel: opts.thinkingLevel,
|
|
60
|
+
appendSystemPrompt: opts.systemPromptFiles,
|
|
44
61
|
};
|
|
45
62
|
}
|
|
46
63
|
|
|
@@ -147,6 +147,8 @@ export function createRecord(
|
|
|
147
147
|
thinkingLevel?: string;
|
|
148
148
|
mode: ExecutionMode;
|
|
149
149
|
task: string;
|
|
150
|
+
/** 短标签(≤20 字符),必填。持久化兜底空串。 */
|
|
151
|
+
slug: string;
|
|
150
152
|
startedAt: number;
|
|
151
153
|
/** 根 Pi session ID(session 隔离过滤用)。递归链上所有层同值。 */
|
|
152
154
|
rootSessionId?: string;
|
|
@@ -164,6 +166,7 @@ export function createRecord(
|
|
|
164
166
|
thinkingLevel: identity.thinkingLevel,
|
|
165
167
|
mode: identity.mode,
|
|
166
168
|
task: identity.task,
|
|
169
|
+
slug: identity.slug,
|
|
167
170
|
startedAt: identity.startedAt,
|
|
168
171
|
rootSessionId: identity.rootSessionId,
|
|
169
172
|
parentRecordId: identity.parentRecordId,
|
|
@@ -607,6 +610,7 @@ export function project(record: ExecutionRecord): SubagentToolDetails {
|
|
|
607
610
|
agent: record.agent,
|
|
608
611
|
model: record.model,
|
|
609
612
|
thinkingLevel: record.thinkingLevel,
|
|
613
|
+
slug: record.slug,
|
|
610
614
|
turns: record.turnCount,
|
|
611
615
|
totalTokens: record.totalTokens,
|
|
612
616
|
elapsedSeconds: computeElapsedSeconds(record),
|
|
@@ -657,6 +661,7 @@ export function snapshot(record: ExecutionRecord): RecordSnapshot {
|
|
|
657
661
|
thinkingLevel: record.thinkingLevel,
|
|
658
662
|
mode: record.mode,
|
|
659
663
|
task: record.task,
|
|
664
|
+
slug: record.slug,
|
|
660
665
|
status: record.status,
|
|
661
666
|
turns: record.turnCount,
|
|
662
667
|
totalTokens: record.totalTokens,
|