pi-task-dispatcher 3.1.2 → 3.2.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 +11 -10
- package/extensions/task-dispatcher.ts +665 -35
- package/package.json +1 -1
- package/tests/fake-rpc-check.mjs +313 -127
- package/tests/fake-rpc-worker.mjs +287 -148
- package/tests/gate-probe.mjs +185 -162
- package/tests/static-check.mjs +26 -3
package/package.json
CHANGED
package/tests/fake-rpc-check.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Exercise
|
|
1
|
+
/** Exercise worker lifecycle, manager-proxied capabilities, and cleanup against a deterministic fake child. */
|
|
2
2
|
|
|
3
3
|
import assert from "node:assert/strict";
|
|
4
4
|
import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
@@ -17,14 +17,71 @@ const projectRoot = path.resolve(import.meta.dirname, "..");
|
|
|
17
17
|
const temporaryDirectory = await mkdtemp(path.join(tmpdir(), "pi-task-dispatcher-fake-rpc-"));
|
|
18
18
|
const configPath = path.join(temporaryDirectory, "task-dispatcher.json");
|
|
19
19
|
const promptMarker = path.join(temporaryDirectory, "prompts.txt");
|
|
20
|
+
const observationPath = path.join(temporaryDirectory, "observations.jsonl");
|
|
20
21
|
const inheritedWorkerRole = process.env.PI_TASK_DISPATCHER_WORKER;
|
|
21
22
|
|
|
23
|
+
function usage(input, output) {
|
|
24
|
+
return {
|
|
25
|
+
input,
|
|
26
|
+
output,
|
|
27
|
+
cacheRead: 0,
|
|
28
|
+
cacheWrite: 0,
|
|
29
|
+
totalTokens: input + output,
|
|
30
|
+
cost: { input: input / 1000, output: output / 1000, cacheRead: 0, cacheWrite: 0, total: (input + output) / 1000 },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function tool(name, label, description, annotations, executionMode = "parallel") {
|
|
35
|
+
return {
|
|
36
|
+
name,
|
|
37
|
+
label,
|
|
38
|
+
description,
|
|
39
|
+
parameters: { type: "object", additionalProperties: false, properties: { path: { type: "string" }, action: { type: "string" }, value: { type: "string" } } },
|
|
40
|
+
outputSchema: { type: "object", additionalProperties: true },
|
|
41
|
+
executionMode,
|
|
42
|
+
annotations,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function terminal(status) {
|
|
47
|
+
return new Set(["completed", "failed", "cancelled", "checkpointed", "taken_over", "hard_stopped"]).has(status);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function observations() {
|
|
51
|
+
const text = await readFile(observationPath, "utf8").catch(() => "");
|
|
52
|
+
return text.split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function clearObservations() {
|
|
56
|
+
await writeFile(observationPath, "", "utf8");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function waitForObservation(predicate, label) {
|
|
60
|
+
const deadline = Date.now() + 3_000;
|
|
61
|
+
while (Date.now() < deadline) {
|
|
62
|
+
const match = (await observations()).find(predicate);
|
|
63
|
+
if (match) return match;
|
|
64
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
65
|
+
}
|
|
66
|
+
throw new Error(`Timed out waiting for ${label}.`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function waitFor(predicate, label) {
|
|
70
|
+
const deadline = Date.now() + 3_000;
|
|
71
|
+
while (Date.now() < deadline) {
|
|
72
|
+
if (predicate()) return;
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
74
|
+
}
|
|
75
|
+
throw new Error(`Timed out waiting for ${label}.`);
|
|
76
|
+
}
|
|
77
|
+
|
|
22
78
|
// The test loads the parent extension even when invoked from a delegated review worker.
|
|
23
79
|
delete process.env.PI_TASK_DISPATCHER_WORKER;
|
|
24
80
|
process.env.PI_TASK_DISPATCHER_TEST_MODE = "1";
|
|
25
81
|
process.env.PI_TASK_DISPATCHER_CONFIG_PATH = configPath;
|
|
26
82
|
process.env.PI_TASK_DISPATCHER_CLI_OVERRIDE = path.join(projectRoot, "tests", "fake-rpc-worker.mjs");
|
|
27
83
|
process.env.PI_TASK_DISPATCHER_FAKE_PROMPT_MARKER = promptMarker;
|
|
84
|
+
process.env.PI_TASK_DISPATCHER_FAKE_OBSERVATION_MARKER = observationPath;
|
|
28
85
|
|
|
29
86
|
try {
|
|
30
87
|
await writeFile(configPath, "{}\n", "utf8");
|
|
@@ -38,22 +95,47 @@ try {
|
|
|
38
95
|
});
|
|
39
96
|
const loaded = await jiti.import(path.join(projectRoot, "extensions", "task-dispatcher.ts"));
|
|
40
97
|
const register = loaded.default ?? loaded;
|
|
41
|
-
const
|
|
98
|
+
const registeredTools = new Map();
|
|
42
99
|
const handlers = new Map();
|
|
100
|
+
const managerTools = [
|
|
101
|
+
tool("read", "Read", "Manager-controlled read override", { readOnlyHint: true }, "parallel"),
|
|
102
|
+
tool("bash", "Bash", "Manager-controlled command", { destructiveHint: true }, "sequential"),
|
|
103
|
+
tool("edit", "Edit", "Manager-controlled edit", { destructiveHint: true }, "sequential"),
|
|
104
|
+
tool("write", "Write", "Manager-controlled write", { destructiveHint: true }, "sequential"),
|
|
105
|
+
tool("codemode", "Codemode", "Child-local codemode capability", { readOnlyHint: false }, "sequential"),
|
|
106
|
+
tool("extension_tool", "Extension tool", "An extension-defined callable tool", { destructiveHint: true }, "parallel"),
|
|
107
|
+
tool("rich_tool", "Rich tool", "A proxy serialization exercise", { destructiveHint: true }, "parallel"),
|
|
108
|
+
];
|
|
109
|
+
const managerMetadata = managerTools.map((entry) => ({
|
|
110
|
+
name: entry.name,
|
|
111
|
+
description: entry.description,
|
|
112
|
+
parameters: entry.parameters,
|
|
113
|
+
namespace: entry.name === "extension_tool" ? { name: "extension", description: "Manager extension namespace", instructions: "Use exact manager arguments." } : undefined,
|
|
114
|
+
annotations: entry.annotations,
|
|
115
|
+
promptGuidelines: [`${entry.name} is controlled by the manager.`],
|
|
116
|
+
sourceInfo: { path: `<fake:${entry.name}>`, source: "fake", scope: "temporary", origin: "top-level", baseDir: undefined },
|
|
117
|
+
}));
|
|
118
|
+
managerMetadata.push({ name: "registered_only", description: "Registered but excluded from this invocation", annotations: { readOnlyHint: true }, promptGuidelines: [], sourceInfo: { path: "<fake:registered-only>", source: "fake", scope: "temporary", origin: "top-level" } });
|
|
43
119
|
const pi = {
|
|
44
|
-
registerTool(
|
|
45
|
-
getActiveTools() { return
|
|
120
|
+
registerTool(entry) { registeredTools.set(entry.name, entry); },
|
|
121
|
+
getActiveTools() { return managerTools.map((entry) => entry.name); },
|
|
122
|
+
getAllTools() { return managerMetadata; },
|
|
46
123
|
on(event, handler) { handlers.set(event, handler); },
|
|
47
124
|
};
|
|
48
125
|
register(pi);
|
|
49
126
|
|
|
50
127
|
const activeModel = { provider: "fake-provider", id: "active-model", contextWindow: 272_000, maxTokens: 128_000 };
|
|
51
128
|
const statusUpdates = [];
|
|
129
|
+
const parentExecutions = [];
|
|
130
|
+
const transitiveDecisions = [];
|
|
131
|
+
let activeParentExecutions = 0;
|
|
132
|
+
let maximumConcurrentParentExecutions = 0;
|
|
52
133
|
const context = {
|
|
53
134
|
cwd: projectRoot,
|
|
54
135
|
hasUI: true,
|
|
55
136
|
model: activeModel,
|
|
56
137
|
thinkingLevel: "medium",
|
|
138
|
+
get tools() { return managerTools.filter((tool) => tool.name !== "codemode"); },
|
|
57
139
|
ui: {
|
|
58
140
|
theme: { fg(_color, text) { return text; } },
|
|
59
141
|
setWidget() {},
|
|
@@ -63,20 +145,71 @@ try {
|
|
|
63
145
|
find(provider, id) { return { provider, id, contextWindow: 272_000, maxTokens: 128_000 }; },
|
|
64
146
|
hasConfiguredAuth() { return true; },
|
|
65
147
|
},
|
|
148
|
+
async executeTool(name, args, options) {
|
|
149
|
+
const call = { name, args, options, aborted: false, usage: undefined };
|
|
150
|
+
parentExecutions.push(call);
|
|
151
|
+
activeParentExecutions++;
|
|
152
|
+
maximumConcurrentParentExecutions = Math.max(maximumConcurrentParentExecutions, activeParentExecutions);
|
|
153
|
+
try {
|
|
154
|
+
if (process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO === "transitive" && name === "extension_tool") {
|
|
155
|
+
const authorize = handlers.get("tool_call");
|
|
156
|
+
transitiveDecisions.push(
|
|
157
|
+
authorize?.({ type: "tool_call", toolCallId: "start-transitive/1", parentToolCallId: "start-transitive", toolName: "extension_tool", input: args }, context),
|
|
158
|
+
authorize?.({ type: "tool_call", toolCallId: "start-transitive/1/1", parentToolCallId: "start-transitive/1", toolName: "read", input: { path: "allowed.txt" } }, context),
|
|
159
|
+
authorize?.({ type: "tool_call", toolCallId: "start-transitive/1/2", parentToolCallId: "start-transitive/1", toolName: "edit", input: { path: "blocked.txt" } }, context),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (args?.path === "wait-for-abort.txt") {
|
|
163
|
+
await new Promise((resolve) => {
|
|
164
|
+
if (options.signal.aborted) return resolve();
|
|
165
|
+
options.signal.addEventListener("abort", resolve, { once: true });
|
|
166
|
+
});
|
|
167
|
+
call.aborted = true;
|
|
168
|
+
return { toolCall: { id: "aborted" }, isError: true, result: { content: [{ type: "text", text: "manager operation cancelled" }] } };
|
|
169
|
+
}
|
|
170
|
+
if (args?.path === "presentation.txt") {
|
|
171
|
+
options.onUpdate?.({ content: [{ type: "text", text: "partial read result that must be replaced" }], details: { internal: "hidden" }, usage: usage(99, 99) });
|
|
172
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
173
|
+
const result = { content: [{ type: "text", text: "final read result six red" }], details: { internal: "hidden" }, usage: usage(3, 1) };
|
|
174
|
+
call.usage = result.usage;
|
|
175
|
+
return { toolCall: { id: "presentation" }, isError: false, result };
|
|
176
|
+
}
|
|
177
|
+
if (name === "rich_tool") {
|
|
178
|
+
options.onUpdate?.({ content: [{ type: "text", text: "partial rich result" }] });
|
|
179
|
+
const result = {
|
|
180
|
+
content: [{ type: "text", text: "rich manager result" }, { type: "image", data: "ZmFrZQ==", mimeType: "image/png" }],
|
|
181
|
+
structuredContent: { source: "manager", count: 1 },
|
|
182
|
+
terminate: true,
|
|
183
|
+
usage: usage(3, 1),
|
|
184
|
+
};
|
|
185
|
+
call.usage = result.usage;
|
|
186
|
+
return { toolCall: { id: "rich" }, isError: true, result };
|
|
187
|
+
}
|
|
188
|
+
if (process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO === "concurrent") await new Promise((resolve) => setTimeout(resolve, 40));
|
|
189
|
+
const result = {
|
|
190
|
+
content: [{ type: "text", text: name === "extension_tool" ? "manager extension override" : `manager read override: ${args?.path ?? args?.action ?? "unknown"}` }],
|
|
191
|
+
details: { managerHook: true },
|
|
192
|
+
usage: usage(3, 1),
|
|
193
|
+
};
|
|
194
|
+
call.usage = result.usage;
|
|
195
|
+
return { toolCall: { id: `${name}-${parentExecutions.length}` }, isError: false, result };
|
|
196
|
+
} finally {
|
|
197
|
+
activeParentExecutions--;
|
|
198
|
+
}
|
|
199
|
+
},
|
|
66
200
|
};
|
|
67
201
|
const workload = { investigationUnits: 1, changeUnits: 0, verificationUnits: 1, expectedLongRunningSeconds: 0 };
|
|
68
|
-
const start =
|
|
69
|
-
const designerStart =
|
|
70
|
-
const control =
|
|
202
|
+
const start = registeredTools.get("delegate_engineering");
|
|
203
|
+
const designerStart = registeredTools.get("delegate_designer");
|
|
204
|
+
const control = registeredTools.get("delegate_control");
|
|
71
205
|
assert(start && designerStart && control, "Expected supervised tools to register.");
|
|
72
|
-
assert(
|
|
73
|
-
|
|
74
|
-
const schemaGuidance = tools.get("delegate_mechanical").parameters.properties.tools.description;
|
|
206
|
+
assert(registeredTools.has("delegate_mechanical") && registeredTools.has("delegate_assistant"), "Expected every worker role to register.");
|
|
207
|
+
const schemaGuidance = registeredTools.get("delegate_mechanical").parameters.properties.tools.description;
|
|
75
208
|
assert.match(schemaGuidance, /codemode.*allowlist.*direct tool.*otherwise omit/i, "Schema guidance no longer describes the codemode delegation rule.");
|
|
76
209
|
for (const name of ["delegate_mechanical", "delegate_assistant", "delegate_engineering", "delegate_designer"]) {
|
|
77
|
-
const delegate =
|
|
78
|
-
assert.equal(delegate.parameters.properties.tools.description, schemaGuidance, `${name} does not expose
|
|
79
|
-
assert(delegate.promptGuidelines.includes(schemaGuidance), `${name} does not expose
|
|
210
|
+
const delegate = registeredTools.get(name);
|
|
211
|
+
assert.equal(delegate.parameters.properties.tools.description, schemaGuidance, `${name} does not expose shared codemode guidance.`);
|
|
212
|
+
assert(delegate.promptGuidelines.includes(schemaGuidance), `${name} does not expose manager codemode guidance.`);
|
|
80
213
|
}
|
|
81
214
|
const theme = { fg(_color, text) { return text; }, bold(text) { return text; } };
|
|
82
215
|
let rendererInvalidations = 0;
|
|
@@ -84,7 +217,7 @@ try {
|
|
|
84
217
|
const callComponent = start.renderCall({}, theme, renderContext);
|
|
85
218
|
assert.doesNotThrow(() => callComponent.render(80), "Partial streamed arguments must never crash the renderer.");
|
|
86
219
|
|
|
87
|
-
// A normal
|
|
220
|
+
// A normal job proves built-in names resolve through the manager context, without merging nested usage into child output.
|
|
88
221
|
process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "normal";
|
|
89
222
|
let result = await start.execute("start-normal", { task: "fake normal job", tools: ["read"], workload }, undefined, undefined, context);
|
|
90
223
|
assert.equal(result.isError, false);
|
|
@@ -94,34 +227,26 @@ try {
|
|
|
94
227
|
assert.equal(result.details.sequence, 1);
|
|
95
228
|
assert.equal(result.details.turns, 1);
|
|
96
229
|
assert.equal(result.details.actionLedger.length, 1);
|
|
97
|
-
assert.equal(result.
|
|
98
|
-
assert.equal(result.usage.
|
|
99
|
-
assert.equal(result.usage.
|
|
230
|
+
assert.equal(result.details.actionLedger[0].mutating, false, "Read-only annotations must classify read as non-mutating.");
|
|
231
|
+
assert.equal(result.usage.input, 10, "Nested manager usage must not be sent back into child accounting.");
|
|
232
|
+
assert.equal(result.usage.output, 2);
|
|
233
|
+
assert.equal(parentExecutions.at(-1).name, "read");
|
|
234
|
+
assert.equal(parentExecutions.at(-1).usage.totalTokens, 4, "The parent manager hook must observe nested usage exactly once.");
|
|
235
|
+
assert.equal(parentExecutions.at(-1).args.path, "package.json", "Built-in name must route through manager override arguments.");
|
|
100
236
|
const partialRender = start.renderResult(result, { expanded: false, isPartial: true }, theme, renderContext).render(100).join("\n");
|
|
101
237
|
assert.match(partialRender, /Last: read package\.json/);
|
|
102
238
|
assert.match(partialRender, /Elapsed:/);
|
|
103
239
|
assert.equal(statusUpdates.at(-1)?.text, "engineer-1 · 11.0%/1k • active-model • medium");
|
|
104
240
|
const resultComponent = start.renderResult(result, { expanded: false, isPartial: false }, theme, renderContext);
|
|
105
241
|
renderContext.lastComponent = resultComponent;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
assert.
|
|
110
|
-
assert.doesNotMatch(firstRender, /Elapsed:/);
|
|
111
|
-
for (let index = 0; index < 1_000; index++) {
|
|
112
|
-
const repeatedComponent = start.renderResult(result, { expanded: false, isPartial: false }, theme, renderContext);
|
|
113
|
-
assert.equal(repeatedComponent.render(100).join("\n"), firstRender, "Repeated render output changed.");
|
|
114
|
-
assert.match(callComponent.render(100).join("\n"), /engineer-1: paused/);
|
|
115
|
-
}
|
|
116
|
-
assert.equal(rendererInvalidations, 0, "A renderer invalidated its own render pass, risking an infinite loop.");
|
|
117
|
-
assert.match(callComponent.render(100).join("\n"), /engineer-1: paused/, "The shared call header did not update to paused.");
|
|
242
|
+
assert.equal(start.renderResult(result, { expanded: false, isPartial: false }, theme, renderContext), resultComponent, "The result renderer did not reuse Pi's last component.");
|
|
243
|
+
const firstRender = resultComponent.render(100).join("\n");
|
|
244
|
+
for (let index = 0; index < 100; index++) assert.equal(start.renderResult(result, { expanded: false, isPartial: false }, theme, renderContext).render(100).join("\n"), firstRender);
|
|
245
|
+
assert.equal(rendererInvalidations, 0, "A renderer invalidated its own render pass.");
|
|
118
246
|
const normalJobId = result.details.jobId;
|
|
119
247
|
const managerArgs = { jobId: normalJobId, action: "continue", actionBatches: 1 };
|
|
120
248
|
const managerRenderContext = { args: managerArgs, state: {}, invalidate() { rendererInvalidations++; }, cwd: projectRoot };
|
|
121
|
-
|
|
122
|
-
assert.match(managerCall.render(100).join("\n"), /manager: engineer-1 → continue/);
|
|
123
|
-
assert.doesNotMatch(managerCall.render(100).join("\n"), /continue\//);
|
|
124
|
-
|
|
249
|
+
assert.match(control.renderCall(managerArgs, theme, managerRenderContext).render(100).join("\n"), /manager: engineer-1 → continue/);
|
|
125
250
|
result = await control.execute("continue-normal", managerArgs, undefined, undefined, context);
|
|
126
251
|
assert.equal(result.isError, false);
|
|
127
252
|
assert.equal(result.details.status, "completed");
|
|
@@ -129,13 +254,8 @@ try {
|
|
|
129
254
|
assert.equal(result.details.workerText, "deterministic worker complete");
|
|
130
255
|
assert.equal(result.usage.input, 5, "Continuation must return only newly observed usage.");
|
|
131
256
|
assert.equal(result.usage.totalTokens, 8);
|
|
132
|
-
assert.equal(statusUpdates.at(-1)?.text, "engineer-1 · 12.0%/1k • active-model • medium");
|
|
133
|
-
assert(!statusUpdates.some((update) => update.text === undefined), "Worker status must remain visible after it first appears.");
|
|
134
|
-
control.renderResult(result, { expanded: false, isPartial: false }, theme, managerRenderContext).render(100);
|
|
135
|
-
assert.match(managerCall.render(100).join("\n"), /manager: engineer-1 → completed/);
|
|
136
|
-
assert.doesNotMatch(managerCall.render(100).join("\n"), /continue\/completed/);
|
|
137
257
|
|
|
138
|
-
//
|
|
258
|
+
// Codemode remains local while only the manager-selected non-codemode capability is proxied.
|
|
139
259
|
await writeFile(configPath, JSON.stringify({ designer: { model: "media-provider/media-model", thinking: "high" } }), "utf8");
|
|
140
260
|
process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "codemode";
|
|
141
261
|
result = await designerStart.execute("start-designer", { task: "fake image job", tools: ["read", "codemode"], workload }, undefined, undefined, context);
|
|
@@ -148,150 +268,216 @@ try {
|
|
|
148
268
|
assert.equal(result.details.actionCounts.mutations, 1);
|
|
149
269
|
assert.equal(result.details.recentActions.at(-1).toolName, "codemode");
|
|
150
270
|
assert.deepEqual(result.details.recentActions.at(-1).mediaOutputs, [{ mimeType: "image/png", bytes: 4 }]);
|
|
151
|
-
assert.
|
|
152
|
-
assert.
|
|
153
|
-
|
|
154
|
-
assert.equal(result.usage.output, 6);
|
|
155
|
-
assert.equal(result.usage.totalTokens, 26);
|
|
156
|
-
assert.equal(
|
|
157
|
-
statusUpdates.at(-1)?.text,
|
|
158
|
-
"designer-2 · 11.0%/1k • media-model • high | engineer-1 · 12.0%/1k • active-model • medium",
|
|
159
|
-
"Starting another role must retain the previous role's footer status.",
|
|
160
|
-
);
|
|
161
|
-
result = await control.execute(
|
|
162
|
-
"continue-designer",
|
|
163
|
-
{ jobId: result.details.jobId, action: "continue", actionBatches: 1 },
|
|
164
|
-
undefined,
|
|
165
|
-
undefined,
|
|
166
|
-
context,
|
|
167
|
-
);
|
|
271
|
+
assert.equal(result.usage.input, 17, "Child-local codemode usage remains child usage; proxied manager usage does not duplicate.");
|
|
272
|
+
assert.equal(result.usage.output, 5);
|
|
273
|
+
result = await control.execute("continue-designer", { jobId: result.details.jobId, action: "continue", actionBatches: 1 }, undefined, undefined, context);
|
|
168
274
|
assert.equal(result.details.status, "completed");
|
|
169
|
-
assert.match(statusUpdates.at(-1)?.text ?? "", /designer-2.*\|.*engineer-1/);
|
|
170
275
|
await writeFile(configPath, "{}\n", "utf8");
|
|
171
276
|
|
|
172
|
-
//
|
|
277
|
+
// Parent streaming updates replace partial presentation text and preserve the bounded/expanded display contract.
|
|
173
278
|
process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "presentation";
|
|
174
279
|
const presentationUpdates = [];
|
|
175
|
-
result = await start.execute(
|
|
176
|
-
"start-presentation",
|
|
177
|
-
{ task: "fake presentation job", tools: ["read"], workload },
|
|
178
|
-
undefined,
|
|
179
|
-
(update) => presentationUpdates.push(update.details?.presentationText ?? ""),
|
|
180
|
-
context,
|
|
181
|
-
);
|
|
280
|
+
result = await start.execute("start-presentation", { task: "fake presentation job", tools: ["read"], workload }, undefined, (update) => presentationUpdates.push(update.details?.presentationText ?? ""), context);
|
|
182
281
|
assert(presentationUpdates.some((text) => text.includes("stream delta that must be replaced")), "Live assistant stream text was not presented.");
|
|
183
|
-
assert(presentationUpdates.some((text) => text.includes("partial read result that must be replaced")), "
|
|
184
|
-
assert.equal(result.isError, false);
|
|
282
|
+
assert(presentationUpdates.some((text) => text.includes("partial read result that must be replaced")), "Manager-proxied streaming update was not presented.");
|
|
185
283
|
assert.equal(result.details.status, "paused");
|
|
186
284
|
assert.match(result.details.presentationText, /authoritative first assistant message/);
|
|
187
|
-
assert.match(result.details.presentationText, /tool start: read presentation\.txt/);
|
|
188
|
-
assert.match(result.details.presentationText, /tool completed: read presentation\.txt/);
|
|
189
285
|
assert.match(result.details.presentationText, /final read result six red/);
|
|
190
286
|
assert.doesNotMatch(result.details.presentationText, /stream delta that must be replaced|stream block that must be replaced|partial read result that must be replaced/);
|
|
191
|
-
assert.doesNotMatch(result.details.presentationText, /\u001b/, "Presentation text must not retain terminal control sequences.");
|
|
192
287
|
const presentationRenderContext = { args: {}, state: {}, invalidate() { rendererInvalidations++; }, cwd: projectRoot };
|
|
193
288
|
const collapsedComponent = start.renderResult(result, { expanded: false, isPartial: false }, theme, presentationRenderContext);
|
|
194
289
|
presentationRenderContext.lastComponent = collapsedComponent;
|
|
195
290
|
const collapsed = collapsedComponent.render(100).join("\n");
|
|
196
291
|
assert.match(collapsed, /earlier visual rows omitted/);
|
|
197
|
-
assert.match(collapsed, /to expand/);
|
|
198
292
|
assert.match(collapsed, /final read result six red/);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
result = await control.execute(
|
|
202
|
-
"continue-presentation",
|
|
203
|
-
{ jobId: result.details.jobId, action: "continue", actionBatches: 1 },
|
|
204
|
-
undefined,
|
|
205
|
-
undefined,
|
|
206
|
-
context,
|
|
207
|
-
);
|
|
208
|
-
assert.equal(result.isError, false);
|
|
293
|
+
result = await control.execute("continue-presentation", { jobId: result.details.jobId, action: "continue", actionBatches: 1 }, undefined, undefined, context);
|
|
209
294
|
assert.equal(result.details.status, "completed");
|
|
210
295
|
assert(result.details.workerTextDisplay.length <= 5_000, "Model-facing worker text was not bounded.");
|
|
211
296
|
assert(result.details.presentationText.length > 5_000, "Structured presentation text was unexpectedly bounded.");
|
|
212
297
|
assert.match(result.details.presentationText, /MIDDLE-ONLY-IN-EXPANDED-PRESENTATION/);
|
|
213
|
-
assert.doesNotMatch(result.content[0].text, /MIDDLE-ONLY-IN-EXPANDED-PRESENTATION
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
assert.
|
|
219
|
-
|
|
220
|
-
assert.equal(
|
|
221
|
-
assert.
|
|
298
|
+
assert.doesNotMatch(result.content[0].text, /MIDDLE-ONLY-IN-EXPANDED-PRESENTATION/);
|
|
299
|
+
|
|
300
|
+
// Extension-defined tools are accepted when callable in ctx.tools and expose only the exact selected proxy surface.
|
|
301
|
+
process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "extension";
|
|
302
|
+
result = await start.execute("start-extension", { task: "use extension capability", tools: ["extension_tool"], workload }, undefined, undefined, context);
|
|
303
|
+
assert.equal(result.isError, false);
|
|
304
|
+
assert.equal(result.details.status, "paused");
|
|
305
|
+
assert.equal(parentExecutions.at(-1).name, "extension_tool");
|
|
306
|
+
assert.match(result.details.presentationText, /manager extension override/);
|
|
307
|
+
await control.execute("continue-extension", { jobId: result.details.jobId, action: "continue", actionBatches: 1 }, undefined, undefined, context);
|
|
308
|
+
|
|
309
|
+
// Transitive manager calls remain inside the exact selected grant, including through extension tools.
|
|
310
|
+
process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "transitive";
|
|
311
|
+
result = await start.execute("start-transitive", { task: "enforce transitive capabilities", tools: ["extension_tool", "read"], workload }, undefined, undefined, context);
|
|
312
|
+
assert.equal(result.details.status, "paused");
|
|
313
|
+
transitiveDecisions.push(handlers.get("tool_call")?.({ type: "tool_call", toolCallId: "start-transitive/1/3", parentToolCallId: "start-transitive/1", toolName: "read", input: { path: "late.txt" } }, context));
|
|
314
|
+
assert.equal(transitiveDecisions.length, 4);
|
|
315
|
+
assert.equal(transitiveDecisions[0], undefined, "The selected first-level proxy call was blocked.");
|
|
316
|
+
assert.equal(transitiveDecisions[1], undefined, "A selected transitive manager call was blocked.");
|
|
317
|
+
assert.equal(transitiveDecisions[2]?.block, true, "An unselected transitive manager call was not blocked.");
|
|
318
|
+
assert.match(transitiveDecisions[2]?.reason ?? "", /outside this delegation's selected capability grant/i);
|
|
319
|
+
assert.equal(transitiveDecisions[3]?.block, true, "A delayed transitive call survived executor-lease revocation.");
|
|
320
|
+
assert.match(transitiveDecisions[3]?.reason ?? "", /executor lease is no longer active/i);
|
|
321
|
+
await control.execute("continue-transitive", { jobId: result.details.jobId, action: "continue", actionBatches: 1 }, undefined, undefined, context);
|
|
322
|
+
|
|
323
|
+
// Proxy forwarding preserves streamed content, image and structured output, isError, terminate, and strips nested usage/details.
|
|
324
|
+
await clearObservations();
|
|
325
|
+
process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "proxy-rich";
|
|
326
|
+
result = await start.execute("start-rich", { task: "exercise proxy result transport", tools: ["rich_tool"], workload }, undefined, undefined, context);
|
|
327
|
+
assert.equal(result.details.status, "paused");
|
|
328
|
+
assert.equal(result.details.recentActions.at(-1).status, "failed");
|
|
329
|
+
assert.deepEqual(result.details.recentActions.at(-1).mediaOutputs, [{ mimeType: "image/png", bytes: 4 }]);
|
|
330
|
+
const rich = await waitForObservation((entry) => entry.kind === "rich", "rich proxy observation");
|
|
331
|
+
assert.equal(rich.updates, 1);
|
|
332
|
+
assert.equal(rich.hasImage, true);
|
|
333
|
+
assert.deepEqual(rich.structuredContent, { source: "manager", count: 1 });
|
|
334
|
+
assert.equal(rich.isError, true);
|
|
335
|
+
assert.equal(rich.terminate, true);
|
|
336
|
+
assert.equal(rich.hasUsage, false, "Nested usage must be stripped before delivery to the child.");
|
|
337
|
+
await control.execute("continue-rich", { jobId: result.details.jobId, action: "continue", actionBatches: 1 }, undefined, undefined, context);
|
|
338
|
+
|
|
339
|
+
// Multiple child proxy requests can execute concurrently through the manager hook.
|
|
340
|
+
maximumConcurrentParentExecutions = 0;
|
|
341
|
+
process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "concurrent";
|
|
342
|
+
result = await start.execute("start-concurrent", { task: "run two manager tools", tools: ["read", "extension_tool"], workload }, undefined, undefined, context);
|
|
343
|
+
assert.equal(result.details.status, "paused");
|
|
344
|
+
assert.equal(maximumConcurrentParentExecutions, 2, "Independent proxy requests were serialized instead of using manager execution concurrency.");
|
|
345
|
+
await control.execute("continue-concurrent", { jobId: result.details.jobId, action: "continue", actionBatches: 1 }, undefined, undefined, context);
|
|
346
|
+
|
|
347
|
+
// Reject spoofed job nonces, unknown names, replayed ids, and post-boundary requests without invoking unauthorized manager tools.
|
|
348
|
+
await clearObservations();
|
|
349
|
+
const securityCallsBefore = parentExecutions.length;
|
|
350
|
+
process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "security";
|
|
351
|
+
result = await start.execute("start-security", { task: "probe private proxy validation", tools: ["read"], workload }, undefined, undefined, context);
|
|
352
|
+
assert.equal(result.details.status, "paused");
|
|
353
|
+
const activeSecurity = await waitForObservation((entry) => entry.kind === "security-active", "active security rejections");
|
|
354
|
+
assert.match(activeSecurity.cross, /nonce is invalid/i);
|
|
355
|
+
assert.match(activeSecurity.unknown, /not in this job's selected capability grant/i);
|
|
356
|
+
assert.match(activeSecurity.first, /manager read override: once\.txt/);
|
|
357
|
+
assert.match(activeSecurity.replay, /request id was replayed/i);
|
|
358
|
+
const pausedSecurity = await waitForObservation((entry) => entry.kind === "security-paused", "paused security rejection");
|
|
359
|
+
assert.match(pausedSecurity.text, /no bounded manager executor lease is active/i);
|
|
360
|
+
assert.equal(parentExecutions.length - securityCallsBefore, 1, "Only the first valid security probe may reach ctx.executeTool().");
|
|
361
|
+
await control.execute("continue-security", { jobId: result.details.jobId, action: "continue", actionBatches: 1 }, undefined, undefined, context);
|
|
362
|
+
|
|
363
|
+
// Continuations fail closed if current manager callable definitions no longer match the original grant.
|
|
364
|
+
process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "normal";
|
|
365
|
+
result = await start.execute("start-revalidate", { task: "revalidate manager definition", tools: ["read"], workload }, undefined, undefined, context);
|
|
366
|
+
const originalReadParameters = managerTools.find((entry) => entry.name === "read").parameters;
|
|
367
|
+
managerTools.find((entry) => entry.name === "read").parameters = { ...originalReadParameters, properties: { ...originalReadParameters.properties, changed: { type: "boolean" } } };
|
|
368
|
+
const revalidated = await control.execute("continue-revalidate", { jobId: result.details.jobId, action: "continue", actionBatches: 1 }, undefined, undefined, context);
|
|
369
|
+
assert.equal(revalidated.isError, true);
|
|
370
|
+
assert.equal(revalidated.details.status, "hard_stopped");
|
|
371
|
+
assert.match(revalidated.content[0].text, /manifest revalidation failed/i);
|
|
372
|
+
managerTools.find((entry) => entry.name === "read").parameters = originalReadParameters;
|
|
373
|
+
result = await start.execute("start-revalidate-identity", { task: "revalidate manager identity", tools: ["read"], workload }, undefined, undefined, context);
|
|
374
|
+
const readIndex = managerTools.findIndex((entry) => entry.name === "read");
|
|
375
|
+
const originalReadTool = managerTools[readIndex];
|
|
376
|
+
managerTools[readIndex] = { ...originalReadTool };
|
|
377
|
+
const identityRevalidated = await control.execute("continue-revalidate-identity", { jobId: result.details.jobId, action: "continue", actionBatches: 1 }, undefined, undefined, context);
|
|
378
|
+
assert.equal(identityRevalidated.isError, true);
|
|
379
|
+
assert.equal(identityRevalidated.details.status, "hard_stopped");
|
|
380
|
+
assert.match(identityRevalidated.content[0].text, /original callable identity/i);
|
|
381
|
+
managerTools[readIndex] = originalReadTool;
|
|
222
382
|
|
|
223
|
-
//
|
|
383
|
+
// Revalidate again at execution time so a replacement during an active lease cannot win a race.
|
|
384
|
+
await clearObservations();
|
|
385
|
+
process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "identity-race";
|
|
386
|
+
const identityRace = start.execute("start-identity-race", { task: "reject a mid-lease tool replacement", tools: ["read"], workload }, undefined, undefined, context);
|
|
387
|
+
await waitForObservation((entry) => entry.kind === "identity-race-ready", "identity-race worker readiness");
|
|
388
|
+
managerTools[readIndex] = { ...originalReadTool };
|
|
389
|
+
result = await identityRace;
|
|
390
|
+
assert.equal(result.isError, true);
|
|
391
|
+
assert.equal(result.details.status, "hard_stopped");
|
|
392
|
+
assert.match(result.content[0].text, /grant changed before read execution|original callable identity/i);
|
|
393
|
+
managerTools[readIndex] = originalReadTool;
|
|
394
|
+
|
|
395
|
+
// Fail closed on non-JSON-safe callable schemas, recursive names, and registered-but-noncallable requests.
|
|
396
|
+
managerTools.find((entry) => entry.name === "read").parameters = { type: "object", invalid: () => undefined };
|
|
397
|
+
process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "normal";
|
|
398
|
+
result = await start.execute("start-invalid-schema", { task: "reject unsafe schema", tools: ["read"], workload }, undefined, undefined, context);
|
|
399
|
+
assert.equal(result.isError, true);
|
|
400
|
+
assert.match(result.content[0].text, /not JSON-serializable/i);
|
|
401
|
+
managerTools.find((entry) => entry.name === "read").parameters = originalReadParameters;
|
|
402
|
+
result = await start.execute("start-recursive", { task: "reject recursive delegation", tools: ["delegate_engineering"], workload }, undefined, undefined, context);
|
|
403
|
+
assert.equal(result.isError, true);
|
|
404
|
+
assert.match(result.content[0].text, /cannot be delegated recursively/i);
|
|
405
|
+
result = await start.execute("start-noncallable", { task: "reject unavailable tool", tools: ["registered_only"], workload }, undefined, undefined, context);
|
|
406
|
+
assert.equal(result.isError, true);
|
|
407
|
+
assert.match(result.content[0].text, /registered but not callable/i);
|
|
408
|
+
|
|
409
|
+
// Existing lifecycle fail-closed paths remain enforced with the manager proxy present.
|
|
224
410
|
process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "length";
|
|
225
411
|
result = await start.execute("start-length", { task: "fake truncated job", tools: ["read"], workload }, undefined, undefined, context);
|
|
226
|
-
assert.equal(result.isError, false);
|
|
227
412
|
assert.equal(result.details.status, "paused");
|
|
228
|
-
assert.equal(result.details.turns, 1);
|
|
229
|
-
assert.equal(result.details.workerText, "truncated worker report");
|
|
230
413
|
assert.match(result.details.phase, /output limit/i);
|
|
231
|
-
|
|
232
|
-
result = await control.execute(
|
|
233
|
-
"continue-length",
|
|
234
|
-
{ jobId: result.details.jobId, action: "continue", actionBatches: 1 },
|
|
235
|
-
undefined,
|
|
236
|
-
undefined,
|
|
237
|
-
context,
|
|
238
|
-
);
|
|
239
|
-
assert.equal(result.isError, false);
|
|
414
|
+
result = await control.execute("continue-length", { jobId: result.details.jobId, action: "continue", actionBatches: 1 }, undefined, undefined, context);
|
|
240
415
|
assert.equal(result.details.status, "completed");
|
|
241
|
-
assert.equal(result.details.workerText, "deterministic worker complete");
|
|
242
|
-
|
|
243
|
-
// A handled prompt starts no agent run and must fail promptly rather than waiting for settlement.
|
|
244
416
|
process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "handled";
|
|
245
|
-
const handledStartedAt = Date.now();
|
|
246
417
|
result = await start.execute("start-handled", { task: "fake handled job", tools: ["read"], workload }, undefined, undefined, context);
|
|
247
418
|
assert.equal(result.isError, true);
|
|
248
|
-
assert.equal(result.details.status, "hard_stopped");
|
|
249
419
|
assert.match(result.content[0].text, /intercepted by an input handler/i);
|
|
250
|
-
assert(Date.now() - handledStartedAt < 5_000, "Handled prompt should not wait for agent settlement.");
|
|
251
|
-
|
|
252
|
-
// A tool-use turn without the private IPC signal must fail closed instead of being mistaken for completion.
|
|
253
420
|
process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "missing-boundary";
|
|
254
421
|
result = await start.execute("start-missing-boundary", { task: "fake missing boundary job", tools: ["read"], workload }, undefined, undefined, context);
|
|
255
422
|
assert.equal(result.isError, true);
|
|
256
|
-
assert.equal(result.details.status, "hard_stopped");
|
|
257
423
|
assert.match(result.content[0].text, /without an enforced IPC boundary/i);
|
|
258
|
-
|
|
259
|
-
// A stats timeout after a valid boundary must remain terminal rather than resurrecting as paused.
|
|
260
424
|
await writeFile(configPath, JSON.stringify({ policy: { absoluteMaxSeconds: 5, noProgressSeconds: 1 } }), "utf8");
|
|
261
425
|
process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "stats-timeout";
|
|
262
426
|
result = await start.execute("start-stats-timeout", { task: "fake stats timeout job", tools: ["read"], workload }, undefined, undefined, context);
|
|
263
427
|
assert.equal(result.isError, true);
|
|
264
|
-
assert.equal(result.details.status, "hard_stopped");
|
|
265
428
|
assert.notEqual(result.details.status, "paused");
|
|
266
|
-
|
|
267
|
-
// A silent prompt must terminate the worker at the configured active-time ceiling.
|
|
268
429
|
await writeFile(configPath, JSON.stringify({ policy: { absoluteMaxSeconds: 1, noProgressSeconds: 1 } }), "utf8");
|
|
269
430
|
process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "timeout";
|
|
270
|
-
const timeoutStartedAt = Date.now();
|
|
271
431
|
result = await start.execute("start-timeout", { task: "fake timeout job", tools: ["read"], workload }, undefined, undefined, context);
|
|
272
432
|
assert.equal(result.isError, true);
|
|
273
433
|
assert.equal(result.details.status, "hard_stopped");
|
|
274
|
-
assert.match(result.content[0].text, /(deadline|timed out|closed)/i);
|
|
275
|
-
assert(Date.now() - timeoutStartedAt < 5_000, "Silent prompt should be bounded by test policy.");
|
|
276
|
-
|
|
277
|
-
// An already-aborted parent operation may initialize the child but must never submit task work.
|
|
278
434
|
await writeFile(configPath, "{}\n", "utf8");
|
|
435
|
+
|
|
436
|
+
// Parent cancellation aborts the nested manager execution and child exit cannot leave a live executor lease.
|
|
437
|
+
process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "cancel-proxy";
|
|
438
|
+
const cancellation = new AbortController();
|
|
439
|
+
const cancellationResult = start.execute("start-cancel-proxy", { task: "cancel nested manager work", tools: ["read"], workload }, cancellation.signal, undefined, context);
|
|
440
|
+
await waitFor(() => parentExecutions.some((call) => call.args?.path === "wait-for-abort.txt"), "nested manager execution start");
|
|
441
|
+
cancellation.abort();
|
|
442
|
+
result = await cancellationResult;
|
|
443
|
+
assert.equal(result.isError, true);
|
|
444
|
+
assert.equal(result.details.status, "cancelled");
|
|
445
|
+
await waitFor(() => parentExecutions.some((call) => call.args?.path === "wait-for-abort.txt" && call.aborted), "nested manager execution abort");
|
|
446
|
+
process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "child-exit";
|
|
447
|
+
result = await start.execute("start-child-exit", { task: "child exits", tools: ["read"], workload }, undefined, undefined, context);
|
|
448
|
+
assert.equal(result.isError, true);
|
|
449
|
+
assert(terminal(result.details.status), "Child exit left a nonterminal worker state.");
|
|
450
|
+
|
|
451
|
+
// An already-aborted operation never submits task work, and session shutdown terminates an idle child.
|
|
279
452
|
await rm(promptMarker, { force: true });
|
|
280
453
|
process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "normal";
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
result = await start.execute("start-aborted", { task: "must not be submitted", tools: ["read"], workload },
|
|
454
|
+
const alreadyAborted = new AbortController();
|
|
455
|
+
alreadyAborted.abort();
|
|
456
|
+
result = await start.execute("start-aborted", { task: "must not be submitted", tools: ["read"], workload }, alreadyAborted.signal, undefined, context);
|
|
284
457
|
assert.equal(result.isError, true);
|
|
285
458
|
assert.equal(result.details.status, "cancelled");
|
|
286
459
|
await assert.rejects(access(promptMarker), /ENOENT/, "An already-aborted operation submitted a prompt.");
|
|
287
|
-
|
|
460
|
+
await clearObservations();
|
|
461
|
+
result = await start.execute("start-shutdown", { task: "session cleanup", tools: ["read"], workload }, undefined, undefined, context);
|
|
462
|
+
assert.equal(result.details.status, "paused");
|
|
463
|
+
const workerStart = await waitForObservation((entry) => entry.kind === "worker-start" && Number.isInteger(entry.pid), "session shutdown child identity");
|
|
288
464
|
await handlers.get("session_shutdown")?.({}, context);
|
|
289
|
-
|
|
465
|
+
await waitFor(() => {
|
|
466
|
+
try {
|
|
467
|
+
process.kill(workerStart.pid, 0);
|
|
468
|
+
return false;
|
|
469
|
+
} catch {
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
}, "session shutdown child termination");
|
|
473
|
+
|
|
474
|
+
console.log("Deterministic fake-RPC lifecycle and manager-proxy checks passed.");
|
|
290
475
|
} finally {
|
|
291
476
|
delete process.env.PI_TASK_DISPATCHER_TEST_MODE;
|
|
292
477
|
delete process.env.PI_TASK_DISPATCHER_CONFIG_PATH;
|
|
293
478
|
delete process.env.PI_TASK_DISPATCHER_CLI_OVERRIDE;
|
|
294
479
|
delete process.env.PI_TASK_DISPATCHER_FAKE_PROMPT_MARKER;
|
|
480
|
+
delete process.env.PI_TASK_DISPATCHER_FAKE_OBSERVATION_MARKER;
|
|
295
481
|
delete process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO;
|
|
296
482
|
if (inheritedWorkerRole === undefined) delete process.env.PI_TASK_DISPATCHER_WORKER;
|
|
297
483
|
else process.env.PI_TASK_DISPATCHER_WORKER = inheritedWorkerRole;
|