@zhushanwen/pi-subagent-workflow 0.1.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/agents/context-builder.md +17 -0
- package/agents/general-purpose.md +16 -0
- package/agents/oracle.md +17 -0
- package/agents/planner.md +17 -0
- package/agents/researcher.md +17 -0
- package/agents/reviewer.md +17 -0
- package/agents/scout.md +17 -0
- package/agents/worker.md +16 -0
- package/examples/README.md +43 -0
- package/examples/chain.example.js +92 -0
- package/examples/map-reduce.example.js +99 -0
- package/examples/parallel.example.js +82 -0
- package/examples/scatter-gather.example.js +106 -0
- package/index.ts +1 -0
- package/package.json +66 -0
- package/skills/workflow-script-format/SKILL.md +328 -0
- package/src/execution/__tests__/agent-registry.test.ts +164 -0
- package/src/execution/__tests__/agent-result-mapper.test.ts +128 -0
- package/src/execution/__tests__/alive-store.test.ts +147 -0
- package/src/execution/__tests__/bg-notify-render.test.ts +256 -0
- package/src/execution/__tests__/concurrency-pool.test.ts +217 -0
- package/src/execution/__tests__/config.test.ts +110 -0
- package/src/execution/__tests__/crash-recovery.test.ts +311 -0
- package/src/execution/__tests__/execute-nesting.test.ts +359 -0
- package/src/execution/__tests__/execute-options-mapper.test.ts +138 -0
- package/src/execution/__tests__/execution-record.test.ts +959 -0
- package/src/execution/__tests__/finalized-marker.test.ts +82 -0
- package/src/execution/__tests__/format-schema-instruction.test.ts +135 -0
- package/src/execution/__tests__/format.test.ts +320 -0
- package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
- package/src/execution/__tests__/list-component.test.ts +347 -0
- package/src/execution/__tests__/model-resolver.test.ts +356 -0
- package/src/execution/__tests__/output-collector.test.ts +61 -0
- package/src/execution/__tests__/path-encoding.test.ts +75 -0
- package/src/execution/__tests__/pi-invocation.test.ts +73 -0
- package/src/execution/__tests__/record-store.test.ts +545 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +439 -0
- package/src/execution/__tests__/run-spawn-integration.test.ts +897 -0
- package/src/execution/__tests__/sdk-contract.test.ts +272 -0
- package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
- package/src/execution/__tests__/session-file-gc.test.ts +247 -0
- package/src/execution/__tests__/session-reconstructor.test.ts +359 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +314 -0
- package/src/execution/__tests__/session-start-reaper.test.ts +227 -0
- package/src/execution/__tests__/spawn-args.test.ts +244 -0
- package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
- package/src/execution/__tests__/subagent-service.test.ts +678 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +389 -0
- package/src/execution/__tests__/temp-prompt.test.ts +53 -0
- package/src/execution/__tests__/timeout-integration.test.ts +381 -0
- package/src/execution/__tests__/tombstone-store.test.ts +73 -0
- package/src/execution/__tests__/tool-action.test.ts +330 -0
- package/src/execution/__tests__/turn-limiter.test.ts +65 -0
- package/src/execution/__tests__/worktree-manager.test.ts +423 -0
- package/src/execution/__tests__/worktree-registry.test.ts +161 -0
- package/src/execution/agent-registry.ts +252 -0
- package/src/execution/agent-result-mapper.ts +84 -0
- package/src/execution/alive-store.ts +92 -0
- package/src/execution/best-effort.ts +30 -0
- package/src/execution/concurrency-pool.ts +84 -0
- package/src/execution/config.ts +73 -0
- package/src/execution/execute-options-mapper.ts +86 -0
- package/src/execution/execution-record.ts +778 -0
- package/src/execution/finalized-marker.ts +51 -0
- package/src/execution/model-config-service.ts +225 -0
- package/src/execution/model-resolver.ts +247 -0
- package/src/execution/notifier.ts +168 -0
- package/src/execution/output-collector.ts +88 -0
- package/src/execution/path-encoding.ts +34 -0
- package/src/execution/pi-invocation.ts +70 -0
- package/src/execution/record-store.ts +350 -0
- package/src/execution/session-context-resolver.ts +64 -0
- package/src/execution/session-file-gc.ts +98 -0
- package/src/execution/session-reconstructor.ts +450 -0
- package/src/execution/session-runner.ts +725 -0
- package/src/execution/spawn-event-adapter.ts +150 -0
- package/src/execution/subagent-service.ts +973 -0
- package/src/execution/subprocess-agent-runner.ts +108 -0
- package/src/execution/temp-prompt.ts +57 -0
- package/src/execution/tombstone-store.ts +72 -0
- package/src/execution/turn-limiter.ts +88 -0
- package/src/execution/types.ts +634 -0
- package/src/execution/worktree-manager.ts +285 -0
- package/src/execution/worktree-registry.ts +144 -0
- package/src/index.ts +454 -0
- package/src/interface/bg-notify-render.ts +286 -0
- package/src/interface/commands.ts +157 -0
- package/src/interface/format.ts +501 -0
- package/src/interface/gui-adapter.ts +136 -0
- package/src/interface/helpers.ts +110 -0
- package/src/interface/list-component.ts +643 -0
- package/src/interface/list-shared.ts +84 -0
- package/src/interface/list-view.ts +373 -0
- package/src/interface/reentry-guard.ts +30 -0
- package/src/interface/subagent-actions.ts +294 -0
- package/src/interface/subagent-tool.ts +294 -0
- package/src/interface/subagents.ts +30 -0
- package/src/interface/tool-render.ts +333 -0
- package/src/interface/tool-workflow-script.ts +351 -0
- package/src/interface/tool-workflow.ts +485 -0
- package/src/interface/views/WorkflowsView.ts +944 -0
- package/src/interface/views/detail-content.ts +298 -0
- package/src/interface/views/format.ts +320 -0
- package/src/orchestration/__tests__/concurrency-gate.test.ts +125 -0
- package/src/orchestration/__tests__/config-loader.test.ts +381 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +332 -0
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +248 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +385 -0
- package/src/orchestration/__tests__/script-lint.test.ts +347 -0
- package/src/orchestration/__tests__/worker-script-builder.test.ts +42 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +319 -0
- package/src/orchestration/agent-opts-resolver.ts +128 -0
- package/src/orchestration/concurrency-gate.ts +69 -0
- package/src/orchestration/config-loader.ts +313 -0
- package/src/orchestration/error-recovery.ts +578 -0
- package/src/orchestration/execute-agent-call.ts +174 -0
- package/src/orchestration/jsonl-run-store.ts +292 -0
- package/src/orchestration/launcher.ts +368 -0
- package/src/orchestration/lifecycle.ts +373 -0
- package/src/orchestration/models/__tests__/budget.test.ts +367 -0
- package/src/orchestration/models/agent-call.ts +76 -0
- package/src/orchestration/models/budget.ts +148 -0
- package/src/orchestration/models/ports.ts +165 -0
- package/src/orchestration/models/run-runtime.ts +91 -0
- package/src/orchestration/models/run-spec.ts +54 -0
- package/src/orchestration/models/run-state.ts +44 -0
- package/src/orchestration/models/trace.ts +102 -0
- package/src/orchestration/models/types.ts +242 -0
- package/src/orchestration/models/workflow-run.ts +275 -0
- package/src/orchestration/models/workflow-script-registry.ts +32 -0
- package/src/orchestration/models/workflow-script.ts +90 -0
- package/src/orchestration/node-ops.ts +192 -0
- package/src/orchestration/script-lint.ts +387 -0
- package/src/orchestration/skill-discovery.ts +60 -0
- package/src/orchestration/worker-handle.ts +115 -0
- package/src/orchestration/worker-host.ts +93 -0
- package/src/orchestration/worker-script-builder.ts +281 -0
- package/src/orchestration/workflow-files.ts +85 -0
- package/src/orchestration/workflow-script-registry-impl.ts +128 -0
- package/src/shared/__tests__/resource-discovery.test.ts +226 -0
- package/src/shared/agent-event.ts +13 -0
- package/src/shared/resource-discovery.ts +535 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workflow() 嵌套调用 — E2E 集成测试。
|
|
3
|
+
*
|
|
4
|
+
* 从 handleWorkerMessage 入口(发 workflow-call 消息)出发,走完完整链路:
|
|
5
|
+
* handleWorkerMessage → dispatchWorkflowCall → deps.onWorkflowCall(executeNestedWorkflow)
|
|
6
|
+
* → runWorkflow(mock) → pollRunToResult → postMessage(workflow-result)。
|
|
7
|
+
*
|
|
8
|
+
* 验证 Wave 1(协议层 dispatchWorkflowCall)与 Wave 2(实现 executeNestedWorkflow)
|
|
9
|
+
* 的端到端对接正确。
|
|
10
|
+
*
|
|
11
|
+
* Mock 策略:
|
|
12
|
+
* - vi.mock("../lifecycle.ts") 控制 runWorkflow:返回固定 runId + 把预构造的 child run
|
|
13
|
+
* 注入 deps.runs(使 pollRunToResult 首轮命中 done)
|
|
14
|
+
* - deps.registry.get 返回预定义 WorkflowScript
|
|
15
|
+
* - parent runtime.worker.postMessage 捕获 workflow-result 消息
|
|
16
|
+
* - deps.onWorkflowCall 用真实的 executeNestedWorkflow 注入(模拟 Interface 层 makeDeps)
|
|
17
|
+
*/
|
|
18
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
19
|
+
|
|
20
|
+
// vi.mock 必须在 import 之前(hoisting 保证拿到 mock 版本)
|
|
21
|
+
vi.mock("../lifecycle.ts", () => ({
|
|
22
|
+
runWorkflow: vi.fn(),
|
|
23
|
+
abortRun: vi.fn(async () => {}),
|
|
24
|
+
pauseRun: vi.fn(async () => {}),
|
|
25
|
+
resumeRun: vi.fn(async () => {}),
|
|
26
|
+
scheduleTimeBudget: vi.fn(() => undefined),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
import { handleWorkerMessage } from "../error-recovery.ts";
|
|
30
|
+
import { executeNestedWorkflow, type LauncherDeps } from "../launcher.ts";
|
|
31
|
+
import { runWorkflow } from "../lifecycle.ts";
|
|
32
|
+
import { Budget } from "../models/budget.ts";
|
|
33
|
+
import type { RunSpec } from "../models/run-spec.ts";
|
|
34
|
+
import type { LifecycleDeps, WorkerHandlers } from "../models/ports.ts";
|
|
35
|
+
import type { WorkflowRun } from "../models/workflow-run.ts";
|
|
36
|
+
import type { WorkflowScript } from "../models/workflow-script.ts";
|
|
37
|
+
import type { LintResult } from "../script-lint.ts";
|
|
38
|
+
|
|
39
|
+
const MOCK_RUN_ID = "wf-e2e-child";
|
|
40
|
+
|
|
41
|
+
// ── helpers ──────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/** flush microtask 队列,让 void .then().catch() 异步链路跑完。 */
|
|
44
|
+
async function flushMicrotasks(): Promise<void> {
|
|
45
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 构造 mock WorkflowScript(validate / toExecutable 可控)。 */
|
|
49
|
+
function makeScript(opts: { valid?: boolean; lintErrorMsg?: string } = {}): WorkflowScript {
|
|
50
|
+
const valid = opts.valid ?? true;
|
|
51
|
+
return {
|
|
52
|
+
name: "child-wf",
|
|
53
|
+
path: "/fake/child-wf.js",
|
|
54
|
+
meta: { name: "child-wf", description: "child workflow", phases: [] },
|
|
55
|
+
toExecutable: () => "const meta = {}; execute() {}",
|
|
56
|
+
validate: (): LintResult => ({
|
|
57
|
+
valid,
|
|
58
|
+
findings: valid
|
|
59
|
+
? []
|
|
60
|
+
: [
|
|
61
|
+
{
|
|
62
|
+
severity: "error",
|
|
63
|
+
line: 3,
|
|
64
|
+
message: opts.lintErrorMsg ?? "lint boom",
|
|
65
|
+
suggestion: "fix it",
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
}),
|
|
69
|
+
} as unknown as WorkflowScript;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 构造 mock parent WorkflowRun(status="running",有 postMessage + controller)。
|
|
74
|
+
*
|
|
75
|
+
* 同时满足 handleWorkerMessage(需 status + runtime.worker)和 executeNestedWorkflow
|
|
76
|
+
* (需 spec.scriptName/chain + runtime.controller.signal + state.budget)的要求。
|
|
77
|
+
*/
|
|
78
|
+
function makeParentRun(opts: {
|
|
79
|
+
scriptName?: string;
|
|
80
|
+
parentWorkflowChain?: readonly string[];
|
|
81
|
+
budget?: Budget;
|
|
82
|
+
postMessage?: ReturnType<typeof vi.fn>;
|
|
83
|
+
} = {}): WorkflowRun {
|
|
84
|
+
const controller = new AbortController();
|
|
85
|
+
const postMessage = opts.postMessage ?? vi.fn();
|
|
86
|
+
return {
|
|
87
|
+
spec: {
|
|
88
|
+
scriptName: opts.scriptName ?? "parent-wf",
|
|
89
|
+
parentWorkflowChain: opts.parentWorkflowChain,
|
|
90
|
+
},
|
|
91
|
+
state: {
|
|
92
|
+
status: "running",
|
|
93
|
+
budget: opts.budget ?? new Budget({ maxTokens: 10000 }),
|
|
94
|
+
},
|
|
95
|
+
runtime: { controller, worker: { postMessage } },
|
|
96
|
+
} as unknown as WorkflowRun;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 构造 mock child WorkflowRun(done 终态,pollRunToResult 首轮命中)。 */
|
|
100
|
+
function makeDoneChildRun(opts: {
|
|
101
|
+
reason?: "completed" | "failed" | "aborted";
|
|
102
|
+
scriptResult?: unknown;
|
|
103
|
+
error?: string;
|
|
104
|
+
usedTokens?: number;
|
|
105
|
+
usedCost?: number;
|
|
106
|
+
}): WorkflowRun {
|
|
107
|
+
const reason = opts.reason ?? "completed";
|
|
108
|
+
return {
|
|
109
|
+
runId: MOCK_RUN_ID,
|
|
110
|
+
spec: { scriptName: "child-wf" },
|
|
111
|
+
state: {
|
|
112
|
+
status: "done",
|
|
113
|
+
reason,
|
|
114
|
+
scriptResult: opts.scriptResult,
|
|
115
|
+
error: opts.error,
|
|
116
|
+
budget: { usedTokens: opts.usedTokens ?? 0, usedCost: opts.usedCost ?? 0 },
|
|
117
|
+
},
|
|
118
|
+
} as unknown as WorkflowRun;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 构造 LauncherDeps mock:registry + runs(Map)+ 占位 port。 */
|
|
122
|
+
function makeDeps(opts: {
|
|
123
|
+
script?: WorkflowScript;
|
|
124
|
+
childRun?: WorkflowRun;
|
|
125
|
+
registry?: { get: ReturnType<typeof vi.fn> };
|
|
126
|
+
} = {}): LauncherDeps {
|
|
127
|
+
const runs = new Map<string, WorkflowRun>();
|
|
128
|
+
if (opts.childRun) runs.set(MOCK_RUN_ID, opts.childRun);
|
|
129
|
+
const registry = opts.registry ?? {
|
|
130
|
+
get: vi.fn(async () => opts.script),
|
|
131
|
+
};
|
|
132
|
+
return {
|
|
133
|
+
registry,
|
|
134
|
+
runs,
|
|
135
|
+
store: { save: vi.fn(async () => {}), loadAll: vi.fn(async () => []) },
|
|
136
|
+
workerHost: { start: vi.fn(() => ({ postMessage: vi.fn() })) },
|
|
137
|
+
runner: { run: vi.fn(async () => ({})) },
|
|
138
|
+
log: vi.fn(),
|
|
139
|
+
eventBus: { emit: vi.fn() },
|
|
140
|
+
} as unknown as LauncherDeps;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 配置 runWorkflow mock:把 childRun 注入 deps.runs 并返回 MOCK_RUN_ID。 */
|
|
144
|
+
function setupRunWorkflow(childRun: WorkflowRun): void {
|
|
145
|
+
vi.mocked(runWorkflow).mockImplementation(async (_spec, deps) => {
|
|
146
|
+
deps.runs.set(MOCK_RUN_ID, childRun);
|
|
147
|
+
return MOCK_RUN_ID;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** WorkerHandlers 占位(workflow-call 路径不触发 handler 回调)。 */
|
|
152
|
+
function makeHandlers(): WorkerHandlers {
|
|
153
|
+
return {
|
|
154
|
+
onMessage: vi.fn(async () => {}),
|
|
155
|
+
onError: vi.fn(async () => {}),
|
|
156
|
+
onExit: vi.fn(async () => {}),
|
|
157
|
+
} as unknown as WorkerHandlers;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
interface PostedMsg {
|
|
161
|
+
type: string;
|
|
162
|
+
callId: number;
|
|
163
|
+
result: { content: string; parsedOutput?: unknown; error?: string };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** 从 postMessage mock 取第 0 次调用的第 0 参,类型安全窄化。 */
|
|
167
|
+
function firstPosted(postMessage: ReturnType<typeof vi.fn>): PostedMsg {
|
|
168
|
+
return postMessage.mock.calls[0]![0] as PostedMsg;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 把 executeNestedWorkflow 绑定为 deps.onWorkflowCall(模拟 Interface 层 makeDeps)。
|
|
173
|
+
*
|
|
174
|
+
* 闭包引用 deps——与 index.ts makeDeps 的模式一致。
|
|
175
|
+
*/
|
|
176
|
+
function wireOnWorkflowCall(deps: LauncherDeps): void {
|
|
177
|
+
(deps as LifecycleDeps).onWorkflowCall = (name, args, parentRun) =>
|
|
178
|
+
executeNestedWorkflow(name, args, parentRun, deps);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
beforeEach(() => {
|
|
182
|
+
vi.clearAllMocks();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ── tests ────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
describe("workflow() nesting end-to-end", () => {
|
|
188
|
+
it("end-to-end: workflow-call → executeNestedWorkflow → workflow-result posted back", async () => {
|
|
189
|
+
const postMessage = vi.fn();
|
|
190
|
+
const parent = makeParentRun({ postMessage });
|
|
191
|
+
const childRun = makeDoneChildRun({
|
|
192
|
+
reason: "completed",
|
|
193
|
+
scriptResult: { data: "test" },
|
|
194
|
+
});
|
|
195
|
+
const deps = makeDeps({ script: makeScript(), childRun });
|
|
196
|
+
setupRunWorkflow(childRun);
|
|
197
|
+
wireOnWorkflowCall(deps);
|
|
198
|
+
|
|
199
|
+
await handleWorkerMessage(
|
|
200
|
+
parent,
|
|
201
|
+
{ type: "workflow-call", callId: 1, name: "child", args: {} },
|
|
202
|
+
deps,
|
|
203
|
+
makeHandlers(),
|
|
204
|
+
);
|
|
205
|
+
await flushMicrotasks();
|
|
206
|
+
|
|
207
|
+
expect(postMessage).toHaveBeenCalledTimes(1);
|
|
208
|
+
const sent = firstPosted(postMessage);
|
|
209
|
+
expect(sent.type).toBe("workflow-result");
|
|
210
|
+
expect(sent.callId).toBe(1);
|
|
211
|
+
expect(sent.result.content).toBe(JSON.stringify({ data: "test" }));
|
|
212
|
+
expect(sent.result.parsedOutput).toEqual({ data: "test" });
|
|
213
|
+
expect(sent.result.error).toBeUndefined();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("end-to-end: circular call returns error result", async () => {
|
|
217
|
+
const postMessage = vi.fn();
|
|
218
|
+
// parent chain ["a"], scriptName "b" → target "a" 触发 a→b→a 循环
|
|
219
|
+
const parent = makeParentRun({
|
|
220
|
+
scriptName: "b",
|
|
221
|
+
parentWorkflowChain: ["a"],
|
|
222
|
+
postMessage,
|
|
223
|
+
});
|
|
224
|
+
const deps = makeDeps({ script: makeScript() });
|
|
225
|
+
wireOnWorkflowCall(deps);
|
|
226
|
+
|
|
227
|
+
await handleWorkerMessage(
|
|
228
|
+
parent,
|
|
229
|
+
{ type: "workflow-call", callId: 2, name: "a", args: {} },
|
|
230
|
+
deps,
|
|
231
|
+
makeHandlers(),
|
|
232
|
+
);
|
|
233
|
+
await flushMicrotasks();
|
|
234
|
+
|
|
235
|
+
expect(postMessage).toHaveBeenCalledTimes(1);
|
|
236
|
+
const sent = firstPosted(postMessage);
|
|
237
|
+
expect(sent.result.error).toContain("Circular workflow call detected");
|
|
238
|
+
expect(sent.result.content).toBe("");
|
|
239
|
+
expect(runWorkflow).not.toHaveBeenCalled();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("end-to-end: workflow not found returns error result", async () => {
|
|
243
|
+
const postMessage = vi.fn();
|
|
244
|
+
const parent = makeParentRun({ postMessage });
|
|
245
|
+
const deps = makeDeps({ script: undefined });
|
|
246
|
+
wireOnWorkflowCall(deps);
|
|
247
|
+
|
|
248
|
+
await handleWorkerMessage(
|
|
249
|
+
parent,
|
|
250
|
+
{ type: "workflow-call", callId: 3, name: "missing", args: {} },
|
|
251
|
+
deps,
|
|
252
|
+
makeHandlers(),
|
|
253
|
+
);
|
|
254
|
+
await flushMicrotasks();
|
|
255
|
+
|
|
256
|
+
expect(postMessage).toHaveBeenCalledTimes(1);
|
|
257
|
+
const sent = firstPosted(postMessage);
|
|
258
|
+
expect(sent.result.error).toContain("not found");
|
|
259
|
+
expect(sent.result.content).toBe("");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("end-to-end: child workflow failure propagates error", async () => {
|
|
263
|
+
const postMessage = vi.fn();
|
|
264
|
+
const parent = makeParentRun({ postMessage });
|
|
265
|
+
const childRun = makeDoneChildRun({
|
|
266
|
+
reason: "failed",
|
|
267
|
+
error: "agent crashed",
|
|
268
|
+
});
|
|
269
|
+
const deps = makeDeps({ script: makeScript(), childRun });
|
|
270
|
+
setupRunWorkflow(childRun);
|
|
271
|
+
wireOnWorkflowCall(deps);
|
|
272
|
+
|
|
273
|
+
await handleWorkerMessage(
|
|
274
|
+
parent,
|
|
275
|
+
{ type: "workflow-call", callId: 4, name: "child", args: {} },
|
|
276
|
+
deps,
|
|
277
|
+
makeHandlers(),
|
|
278
|
+
);
|
|
279
|
+
await flushMicrotasks();
|
|
280
|
+
|
|
281
|
+
expect(postMessage).toHaveBeenCalledTimes(1);
|
|
282
|
+
const sent = firstPosted(postMessage);
|
|
283
|
+
expect(sent.result.error).toBe("agent crashed");
|
|
284
|
+
expect(sent.result.content).toBe("");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("end-to-end: child shares parent budget reference (no sync-back)", async () => {
|
|
288
|
+
const postMessage = vi.fn();
|
|
289
|
+
const parentBudget = new Budget({ maxTokens: 10000 });
|
|
290
|
+
parentBudget.usedTokens = 100;
|
|
291
|
+
const parent = makeParentRun({ budget: parentBudget, postMessage });
|
|
292
|
+
const childRun = makeDoneChildRun({
|
|
293
|
+
reason: "completed",
|
|
294
|
+
scriptResult: "ok",
|
|
295
|
+
});
|
|
296
|
+
const deps = makeDeps({ script: makeScript(), childRun });
|
|
297
|
+
|
|
298
|
+
// 捕获传给 runWorkflow 的 spec——验证 budgetRef 共享父 Budget 引用
|
|
299
|
+
let capturedSpec: RunSpec | undefined;
|
|
300
|
+
vi.mocked(runWorkflow).mockImplementation(async (spec, d) => {
|
|
301
|
+
capturedSpec = spec;
|
|
302
|
+
d.runs.set(MOCK_RUN_ID, childRun);
|
|
303
|
+
return MOCK_RUN_ID;
|
|
304
|
+
});
|
|
305
|
+
wireOnWorkflowCall(deps);
|
|
306
|
+
|
|
307
|
+
await handleWorkerMessage(
|
|
308
|
+
parent,
|
|
309
|
+
{ type: "workflow-call", callId: 5, name: "child", args: {} },
|
|
310
|
+
deps,
|
|
311
|
+
makeHandlers(),
|
|
312
|
+
);
|
|
313
|
+
await flushMicrotasks();
|
|
314
|
+
|
|
315
|
+
// 子 run 直接复用父 Budget 引用(budgetRef),无需 sync-back
|
|
316
|
+
expect(capturedSpec?.budgetRef).toBe(parentBudget);
|
|
317
|
+
expect(parent.state.budget.usedTokens).toBe(100);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent options resolver — resolves agent name / skill / schema to system
|
|
3
|
+
* prompt files + env vars on every dispatch (BL-1).
|
|
4
|
+
*
|
|
5
|
+
* BL-1:解析 workflow 脚本里 `agent({agent,skill,schema})` 的 inline override,
|
|
6
|
+
* 否则 pi 子进程只收到原始 prompt,没有 --append-system-prompt / --skill /
|
|
7
|
+
* PI_WORKFLOW_SCHEMA。AgentCallOpts 从 engine/models/types 引入。
|
|
8
|
+
*
|
|
9
|
+
* 调用方:engine/error-recovery.ts dispatchAgentCall(每次 agent-call 消息)。
|
|
10
|
+
* - agent → AgentRegistry.resolve → systemPrompt 写临时文件 → systemPromptFiles(--append-system-prompt)
|
|
11
|
+
* - skill → resolveSkillPath → skillPath(--skill)
|
|
12
|
+
* - schema → 结构化输出指令写临时文件 → systemPromptFiles + schemaEnv(PI_WORKFLOW_SCHEMA)
|
|
13
|
+
*
|
|
14
|
+
* 临时文件在 activeTempFiles 集合注册,session_shutdown 时由 cleanupAllTempFiles 统一回收。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
18
|
+
import * as fs from "node:fs";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
|
|
21
|
+
import type { AgentCallOpts } from "./models/types.ts";
|
|
22
|
+
import type { AgentRegistry } from "../execution/agent-registry.ts"; // type-only(本文件不 new,只接收实例参数)
|
|
23
|
+
import { resolveSkillPath } from "./skill-discovery.ts";
|
|
24
|
+
|
|
25
|
+
const UUID_SLICE_LEN = 8;
|
|
26
|
+
|
|
27
|
+
export interface ResolveResult {
|
|
28
|
+
opts: AgentCallOpts;
|
|
29
|
+
error?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve agent name and schema into systemPromptFiles + skillPath + schemaEnv.
|
|
34
|
+
*
|
|
35
|
+
* - Agent systemPrompt -> temp file via --append-system-prompt
|
|
36
|
+
* - Skill name -> resolved SKILL.md dir path via --skill
|
|
37
|
+
* - Schema JSON -> temp file with structured-output instruction + PI_WORKFLOW_SCHEMA env
|
|
38
|
+
*
|
|
39
|
+
* Returns the enriched opts and any temp files created (registered in activeTempFiles).
|
|
40
|
+
* Caller is responsible for cleaning up files via cleanupAllTempFiles (session-scoped).
|
|
41
|
+
*/
|
|
42
|
+
export function resolveAgentOpts(
|
|
43
|
+
opts: AgentCallOpts,
|
|
44
|
+
agentRegistry: AgentRegistry,
|
|
45
|
+
sessionDir: string,
|
|
46
|
+
activeTempFiles: Set<string>,
|
|
47
|
+
): ResolveResult {
|
|
48
|
+
const systemPromptFiles: string[] = [];
|
|
49
|
+
|
|
50
|
+
// Resolve agent system prompt
|
|
51
|
+
if (opts.agent) {
|
|
52
|
+
const discovered = agentRegistry.get(opts.agent); // 新 API: get() 替代 resolve(),返回 AgentConfig(含 systemPrompt+model)
|
|
53
|
+
if (!discovered) return { opts, error: `Agent not found: ${opts.agent}` };
|
|
54
|
+
|
|
55
|
+
const hasSystemPrompt = discovered.systemPrompt.trim().length > 0;
|
|
56
|
+
if (hasSystemPrompt) {
|
|
57
|
+
try {
|
|
58
|
+
const tmpDir = path.join(sessionDir, "workflow-tmp");
|
|
59
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
60
|
+
const tmpFile = path.join(tmpDir, `agent-prompt-${randomUUID()}.md`);
|
|
61
|
+
fs.writeFileSync(tmpFile, discovered.systemPrompt, "utf-8");
|
|
62
|
+
activeTempFiles.add(tmpFile);
|
|
63
|
+
systemPromptFiles.push(tmpFile);
|
|
64
|
+
} catch (err: unknown) {
|
|
65
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
66
|
+
return { opts, error: `Temp file write error: ${msg}` };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
opts = { ...opts, model: opts.model || discovered.model };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Resolve skill name to SKILL.md path
|
|
74
|
+
if (opts.skill) {
|
|
75
|
+
const skillPath = resolveSkillPath(opts.skill);
|
|
76
|
+
if (!skillPath) {
|
|
77
|
+
return { opts, error: `Skill not found: ${opts.skill}. Searched .agents/skills/ and ~/.pi/agent/skills/` };
|
|
78
|
+
}
|
|
79
|
+
opts = { ...opts, skillPath };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Inject schema as structured-output instruction via --append-system-prompt
|
|
83
|
+
// and set environment variable for conditional tool + hook activation.
|
|
84
|
+
if (opts.schema) {
|
|
85
|
+
try {
|
|
86
|
+
const tmpDir = path.join(sessionDir, "workflow-tmp");
|
|
87
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
88
|
+
const tmpFile = path.join(tmpDir, `so-${randomUUID().slice(0, UUID_SLICE_LEN)}.txt`);
|
|
89
|
+
const schemaJson = JSON.stringify(opts.schema);
|
|
90
|
+
const content = [
|
|
91
|
+
"## MANDATORY: Structured Output Requirement",
|
|
92
|
+
"",
|
|
93
|
+
"This task requires structured output.",
|
|
94
|
+
"Your FINAL action must be calling the `structured-output` tool.",
|
|
95
|
+
"",
|
|
96
|
+
`structured-output parameters:`,
|
|
97
|
+
` schema = ${schemaJson}`,
|
|
98
|
+
` data = <your result conforming to the schema above>`,
|
|
99
|
+
"",
|
|
100
|
+
"Rules:",
|
|
101
|
+
"- Do NOT output JSON in your text response — use the structured-output tool.",
|
|
102
|
+
"- Do NOT skip this step. The structured-output call IS your result.",
|
|
103
|
+
"- Complete all other work FIRST, then call structured-output as the last action.",
|
|
104
|
+
].join("\n");
|
|
105
|
+
fs.writeFileSync(tmpFile, content, "utf-8");
|
|
106
|
+
activeTempFiles.add(tmpFile);
|
|
107
|
+
systemPromptFiles.push(tmpFile);
|
|
108
|
+
|
|
109
|
+
// Set env var for structured-output extension to activate tool + hook
|
|
110
|
+
opts = { ...opts, schemaEnv: schemaJson };
|
|
111
|
+
} catch (err: unknown) {
|
|
112
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
113
|
+
return { opts, error: `Schema temp file write error: ${msg}` };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
opts: { ...opts, ...(systemPromptFiles.length > 0 ? { systemPromptFiles } : {}) },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Remove all remaining active temp files (called from session_shutdown). */
|
|
123
|
+
export function cleanupAllTempFiles(activeTempFiles: Set<string>): void {
|
|
124
|
+
for (const fp of activeTempFiles) {
|
|
125
|
+
try { fs.unlinkSync(fp); } catch { /* already deleted */ void undefined; }
|
|
126
|
+
}
|
|
127
|
+
activeTempFiles.clear();
|
|
128
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Extension — Concurrency Gate
|
|
3
|
+
*
|
|
4
|
+
* 并发信号量薄封装。Engine 层 dispatchAgentCall 通过 withSlot 包装
|
|
5
|
+
* executeAgentCall,gate 仅处理 signal abort 检查。实际并发调度
|
|
6
|
+
* 由 SubagentService 内部的 ConcurrencyPool 统一管理。
|
|
7
|
+
*
|
|
8
|
+
* Wave 3 (D-A7):withSlot 退化为 abort 薄封装,不独立占池。
|
|
9
|
+
* 并发槽位由 execution/ConcurrencyPool(SubagentService 持有)统一管理,
|
|
10
|
+
* 消除 gate + pool 双重计数的 2N 问题(T3.20)。
|
|
11
|
+
*
|
|
12
|
+
* 层归属:Infra(D-12)。RunRuntime 直接持有具体类。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ── Constants ─────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 默认并发上限(D-13)。保留常量向后兼容。
|
|
19
|
+
* 实际并发由 ConcurrencyPool 管理。
|
|
20
|
+
*/
|
|
21
|
+
export const DEFAULT_CONCURRENCY = 4;
|
|
22
|
+
|
|
23
|
+
// ── Public options ────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export interface ConcurrencyGateOptions {
|
|
26
|
+
/** 最大并发数,缺省 4(D-13)。保留向后兼容。 */
|
|
27
|
+
maxConcurrency?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── ConcurrencyGate ───────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
export class ConcurrencyGate {
|
|
33
|
+
private readonly maxConcurrency: number;
|
|
34
|
+
|
|
35
|
+
constructor(opts: ConcurrencyGateOptions | number = {}) {
|
|
36
|
+
this.maxConcurrency =
|
|
37
|
+
typeof opts === "number" ? opts : (opts.maxConcurrency ?? DEFAULT_CONCURRENCY);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 当前在飞的 agent 调用数(不独立计槽,恒为 0)。 */
|
|
41
|
+
get activeCount(): number {
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 排队等待派发的调用数(不独立计槽,恒为 0)。 */
|
|
46
|
+
get queueLength(): number {
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 获取一个并发槽位,执行 `fn`。
|
|
52
|
+
*
|
|
53
|
+
* Wave 3 简化:不独立管理槽位。仅做 signal pre-abort 检查,
|
|
54
|
+
* 直接执行 fn。实际并发调度由 SubagentService 的 ConcurrencyPool 负责。
|
|
55
|
+
*
|
|
56
|
+
* @param fn 槽位获取后执行的异步函数
|
|
57
|
+
* @param signal 外部 abort signal(pre-aborted 时立即 reject)
|
|
58
|
+
* @returns fn 的返回值
|
|
59
|
+
* @throws AbortError(signal 已 abort 时)
|
|
60
|
+
*/
|
|
61
|
+
async withSlot<T>(fn: () => Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
62
|
+
if (signal?.aborted) {
|
|
63
|
+
const err = new Error("Operation aborted before start");
|
|
64
|
+
err.name = "AbortError";
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
return fn();
|
|
68
|
+
}
|
|
69
|
+
}
|