pi-task-dispatcher 3.1.2 → 3.2.1
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 +659 -46
- 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
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
/** Deterministic JSONL/IPC worker used only by the dispatcher's lifecycle tests. */
|
|
1
|
+
/** Deterministic JSONL/IPC worker used only by the dispatcher's lifecycle and proxy tests. */
|
|
2
2
|
|
|
3
|
+
import { appendFileSync } from "node:fs";
|
|
3
4
|
import { appendFile, readFile } from "node:fs/promises";
|
|
4
5
|
import { readStrictJsonl } from "./strict-jsonl.mjs";
|
|
5
6
|
|
|
6
7
|
const scenario = process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO ?? "normal";
|
|
7
8
|
const markerPath = process.env.PI_TASK_DISPATCHER_FAKE_PROMPT_MARKER;
|
|
9
|
+
const observationPath = process.env.PI_TASK_DISPATCHER_FAKE_OBSERVATION_MARKER;
|
|
8
10
|
let promptCount = 0;
|
|
11
|
+
let proxyConfiguration;
|
|
12
|
+
const pendingProxyCalls = new Map();
|
|
9
13
|
|
|
10
14
|
function send(event) {
|
|
11
15
|
process.stdout.write(`${JSON.stringify(event)}\n`);
|
|
@@ -22,7 +26,282 @@ function usage(input, output) {
|
|
|
22
26
|
};
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
function textFromResult(result) {
|
|
30
|
+
return (result?.content ?? [])
|
|
31
|
+
.filter((part) => part?.type === "text" && typeof part.text === "string")
|
|
32
|
+
.map((part) => part.text)
|
|
33
|
+
.join("\n");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function observe(value) {
|
|
37
|
+
if (observationPath) await appendFile(observationPath, `${JSON.stringify(value)}\n`, "utf8");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Capture responses the parent dispatcher sends over its private IPC channel. */
|
|
41
|
+
process.on("message", (message) => {
|
|
42
|
+
if (!message || typeof message !== "object") return;
|
|
43
|
+
if (message.type !== "task_dispatcher_proxy_update" && message.type !== "task_dispatcher_proxy_result") return;
|
|
44
|
+
const pending = pendingProxyCalls.get(message.requestId);
|
|
45
|
+
if (!pending) return;
|
|
46
|
+
if (message.type === "task_dispatcher_proxy_update") {
|
|
47
|
+
pending.updates.push(message.result);
|
|
48
|
+
pending.onUpdate?.(message.result);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
pendingProxyCalls.delete(message.requestId);
|
|
52
|
+
pending.resolve({ result: message.result, updates: pending.updates });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/** Load and inspect the generated proxy configuration without executing its extension source. */
|
|
56
|
+
async function loadProxyConfiguration(extensionValues) {
|
|
57
|
+
const proxyPath = extensionValues.find((value) => value?.endsWith("tool-proxies.ts"));
|
|
58
|
+
if (!proxyPath) throw new Error("runtime proxy extension was not loaded");
|
|
59
|
+
const source = await readFile(proxyPath, "utf8");
|
|
60
|
+
const match = source.match(/^const configuration = (.+) as \{ nonce: string; tools: ProxyTool\[\] \};$/m);
|
|
61
|
+
if (!match) throw new Error("runtime proxy extension omitted its serialized configuration");
|
|
62
|
+
const configuration = JSON.parse(match[1]);
|
|
63
|
+
if (!configuration || typeof configuration.nonce !== "string" || !Array.isArray(configuration.tools)) {
|
|
64
|
+
throw new Error("runtime proxy extension configuration is invalid");
|
|
65
|
+
}
|
|
66
|
+
return { configuration, source };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Send one simulated child proxy request and retain every parent-streamed partial result. */
|
|
70
|
+
function proxyCall(toolName, args, requestId, nonce = proxyConfiguration.nonce, onUpdate) {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
if (typeof process.send !== "function") {
|
|
73
|
+
reject(new Error("fake worker has no IPC channel"));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
pendingProxyCalls.set(requestId, { resolve, updates: [], onUpdate });
|
|
77
|
+
process.send({ type: "task_dispatcher_proxy_request", nonce, requestId, toolName, args });
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Emit the worker-visible lifecycle around one proxied manager tool. */
|
|
82
|
+
async function emitProxyTool(toolCallId, toolName, args, parentToolCallId) {
|
|
83
|
+
send({ type: "tool_execution_start", toolCallId, toolName, args, ...(parentToolCallId ? { parentToolCallId } : {}) });
|
|
84
|
+
const response = await proxyCall(toolName, args, toolCallId, proxyConfiguration.nonce, (partialResult) => {
|
|
85
|
+
send({ type: "tool_execution_update", toolCallId, toolName, args, partialResult, ...(parentToolCallId ? { parentToolCallId } : {}) });
|
|
86
|
+
});
|
|
87
|
+
send({
|
|
88
|
+
type: "tool_execution_end",
|
|
89
|
+
toolCallId,
|
|
90
|
+
toolName,
|
|
91
|
+
args,
|
|
92
|
+
isError: response.result?.isError === true,
|
|
93
|
+
result: response.result,
|
|
94
|
+
...(parentToolCallId ? { parentToolCallId } : {}),
|
|
95
|
+
});
|
|
96
|
+
return response;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function boundaryAndAbort(toolResults = 1) {
|
|
100
|
+
process.send?.({ type: "task_dispatcher_boundary", turnIndex: 0, toolResults });
|
|
101
|
+
send({ type: "message_end", message: { role: "assistant", stopReason: "error", errorMessage: "This operation was aborted", content: [] } });
|
|
102
|
+
send({ type: "agent_settled" });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function validateFirstPrompt() {
|
|
106
|
+
const toolsIndex = process.argv.indexOf("--tools");
|
|
107
|
+
const selectedTools = toolsIndex >= 0 ? process.argv[toolsIndex + 1]?.split(",") ?? [] : [];
|
|
108
|
+
const extensionValues = process.argv.flatMap((argument, index) => argument === "--extension" ? [process.argv[index + 1]] : []);
|
|
109
|
+
const loaded = await loadProxyConfiguration(extensionValues);
|
|
110
|
+
proxyConfiguration = loaded.configuration;
|
|
111
|
+
const proxyNames = proxyConfiguration.tools.map((tool) => tool.name);
|
|
112
|
+
if (scenario === "normal" && extensionValues.some((value) => value?.endsWith("codemode-only.ts"))) {
|
|
113
|
+
throw new Error("codemode extension was injected without explicit selection");
|
|
114
|
+
}
|
|
115
|
+
if (scenario === "codemode") {
|
|
116
|
+
const nativeCodemodePath = extensionValues.find((value) => value?.endsWith("codemode-only.ts"));
|
|
117
|
+
let nativeCodemodeSource = "";
|
|
118
|
+
try {
|
|
119
|
+
if (nativeCodemodePath) nativeCodemodeSource = await readFile(nativeCodemodePath, "utf8");
|
|
120
|
+
} catch {
|
|
121
|
+
// The deterministic check below reports the source failure.
|
|
122
|
+
}
|
|
123
|
+
if (
|
|
124
|
+
!selectedTools.includes("codemode")
|
|
125
|
+
|| extensionValues.includes("builtin:codemode")
|
|
126
|
+
|| !nativeCodemodePath
|
|
127
|
+
|| !nativeCodemodeSource.includes('createCodemodeExtension({ mode: "only" })')
|
|
128
|
+
|| !nativeCodemodeSource.includes('from "@earendil-works/pi-coding-agent"')
|
|
129
|
+
|| proxyNames.join(",") !== "read"
|
|
130
|
+
) {
|
|
131
|
+
throw new Error("native codemode-only extension or selected proxy visibility was incorrect");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (scenario === "extension" && proxyNames.join(",") !== "extension_tool") {
|
|
135
|
+
throw new Error(`extension tool proxy visibility was not exact: ${proxyNames.join(",")}`);
|
|
136
|
+
}
|
|
137
|
+
if (scenario === "transitive" && proxyNames.join(",") !== "extension_tool,read") {
|
|
138
|
+
throw new Error(`transitive tool proxy visibility was not exact: ${proxyNames.join(",")}`);
|
|
139
|
+
}
|
|
140
|
+
if (scenario === "proxy-rich" && proxyNames.join(",") !== "rich_tool") {
|
|
141
|
+
throw new Error(`rich tool proxy visibility was not exact: ${proxyNames.join(",")}`);
|
|
142
|
+
}
|
|
143
|
+
if (!loaded.source.includes("task_dispatcher_proxy_request") || !loaded.source.includes("Type.Unsafe")) {
|
|
144
|
+
throw new Error("runtime proxy extension omitted its IPC or schema bridge");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function runFirstPrompt() {
|
|
149
|
+
if (scenario === "presentation") {
|
|
150
|
+
send({ type: "message_start", message: { role: "assistant" } });
|
|
151
|
+
send({ type: "message_update", assistantMessageEvent: { type: "text_start", contentIndex: 0 } });
|
|
152
|
+
send({ type: "message_update", assistantMessageEvent: { type: "text_delta", contentIndex: 0, delta: "stream delta that must be replaced" } });
|
|
153
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
154
|
+
send({ type: "message_update", assistantMessageEvent: { type: "text_end", contentIndex: 0, content: "stream block that must be replaced" } });
|
|
155
|
+
send({
|
|
156
|
+
type: "message_end",
|
|
157
|
+
message: { role: "assistant", stopReason: "toolUse", content: [{ type: "text", text: "authoritative first assistant message" }], usage: usage(10, 2) },
|
|
158
|
+
});
|
|
159
|
+
await emitProxyTool("fake-presentation-read", "read", { path: "presentation.txt" });
|
|
160
|
+
boundaryAndAbort();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (scenario === "length") {
|
|
164
|
+
send({
|
|
165
|
+
type: "message_end",
|
|
166
|
+
message: { role: "assistant", stopReason: "length", content: [{ type: "text", text: "truncated worker report" }], usage: usage(7, 4) },
|
|
167
|
+
});
|
|
168
|
+
send({ type: "agent_settled" });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (scenario === "security") {
|
|
172
|
+
send({ type: "message_end", message: { role: "assistant", stopReason: "toolUse", content: [], usage: usage(10, 2) } });
|
|
173
|
+
const cross = await proxyCall("read", { path: "cross-job.txt" }, "security-cross-job", "wrong-job-nonce");
|
|
174
|
+
const unknown = await proxyCall("not_selected", {}, "security-unknown");
|
|
175
|
+
const first = await proxyCall("read", { path: "once.txt" }, "security-replay");
|
|
176
|
+
const replay = await proxyCall("read", { path: "twice.txt" }, "security-replay");
|
|
177
|
+
await observe({
|
|
178
|
+
kind: "security-active",
|
|
179
|
+
cross: textFromResult(cross.result),
|
|
180
|
+
unknown: textFromResult(unknown.result),
|
|
181
|
+
first: textFromResult(first.result),
|
|
182
|
+
replay: textFromResult(replay.result),
|
|
183
|
+
});
|
|
184
|
+
boundaryAndAbort();
|
|
185
|
+
setTimeout(() => {
|
|
186
|
+
void proxyCall("read", { path: "paused.txt" }, "security-paused")
|
|
187
|
+
.then((paused) => observe({ kind: "security-paused", text: textFromResult(paused.result) }))
|
|
188
|
+
.catch((error) => observe({ kind: "security-paused", text: String(error) }));
|
|
189
|
+
}, 40);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (scenario === "concurrent") {
|
|
193
|
+
send({ type: "message_end", message: { role: "assistant", stopReason: "toolUse", content: [], usage: usage(10, 2) } });
|
|
194
|
+
await Promise.all([
|
|
195
|
+
emitProxyTool("fake-concurrent-read", "read", { path: "first.txt" }),
|
|
196
|
+
emitProxyTool("fake-concurrent-extension", "extension_tool", { action: "second" }),
|
|
197
|
+
]);
|
|
198
|
+
boundaryAndAbort(2);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (scenario === "cancel-proxy") {
|
|
202
|
+
send({ type: "message_end", message: { role: "assistant", stopReason: "toolUse", content: [], usage: usage(10, 2) } });
|
|
203
|
+
await emitProxyTool("fake-cancel-read", "read", { path: "wait-for-abort.txt" });
|
|
204
|
+
boundaryAndAbort();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (scenario === "identity-race") {
|
|
208
|
+
send({ type: "message_end", message: { role: "assistant", stopReason: "toolUse", content: [], usage: usage(10, 2) } });
|
|
209
|
+
await observe({ kind: "identity-race-ready" });
|
|
210
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
211
|
+
await emitProxyTool("fake-identity-race-read", "read", { path: "identity-race.txt" });
|
|
212
|
+
boundaryAndAbort();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (scenario === "child-exit") {
|
|
216
|
+
process.exit(17);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (scenario === "extension" || scenario === "transitive") {
|
|
220
|
+
send({ type: "message_end", message: { role: "assistant", stopReason: "toolUse", content: [], usage: usage(10, 2) } });
|
|
221
|
+
await emitProxyTool("fake-extension-1", "extension_tool", { action: "delegated" });
|
|
222
|
+
boundaryAndAbort();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (scenario === "proxy-rich") {
|
|
226
|
+
send({ type: "message_end", message: { role: "assistant", stopReason: "toolUse", content: [], usage: usage(10, 2) } });
|
|
227
|
+
const response = await emitProxyTool("fake-rich-1", "rich_tool", { value: "rich" });
|
|
228
|
+
await observe({
|
|
229
|
+
kind: "rich",
|
|
230
|
+
updates: response.updates.length,
|
|
231
|
+
hasImage: response.result?.content?.some((part) => part?.type === "image") === true,
|
|
232
|
+
structuredContent: response.result?.structuredContent,
|
|
233
|
+
isError: response.result?.isError === true,
|
|
234
|
+
terminate: response.result?.terminate === true,
|
|
235
|
+
hasUsage: Object.prototype.hasOwnProperty.call(response.result ?? {}, "usage"),
|
|
236
|
+
});
|
|
237
|
+
boundaryAndAbort();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
send({ type: "message_end", message: { role: "assistant", stopReason: "toolUse", content: [], usage: usage(10, 2) } });
|
|
242
|
+
if (scenario === "codemode") {
|
|
243
|
+
send({ type: "tool_execution_start", toolCallId: "fake-codemode-1", toolName: "codemode", args: "return image" });
|
|
244
|
+
await emitProxyTool("fake-read-nested", "read", { path: "reference.png" }, "fake-codemode-1");
|
|
245
|
+
send({
|
|
246
|
+
type: "tool_execution_end",
|
|
247
|
+
toolCallId: "fake-codemode-1",
|
|
248
|
+
toolName: "codemode",
|
|
249
|
+
isError: false,
|
|
250
|
+
result: {
|
|
251
|
+
content: [
|
|
252
|
+
{ type: "text", text: "generated image retained in runtime temporary storage" },
|
|
253
|
+
{ type: "image", data: "ZmFrZQ==", mimeType: "image/png" },
|
|
254
|
+
],
|
|
255
|
+
usage: usage(7, 3),
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
} else {
|
|
259
|
+
await emitProxyTool("fake-read-1", "read", { path: "package.json" });
|
|
260
|
+
}
|
|
261
|
+
if (scenario === "missing-boundary") {
|
|
262
|
+
send({
|
|
263
|
+
type: "message_end",
|
|
264
|
+
message: { role: "assistant", stopReason: "stop", content: [{ type: "text", text: "unsafe final response after tool" }], usage: usage(2, 2) },
|
|
265
|
+
});
|
|
266
|
+
} else {
|
|
267
|
+
boundaryAndAbort();
|
|
268
|
+
}
|
|
269
|
+
if (scenario === "missing-boundary") send({ type: "agent_settled" });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function runContinuation() {
|
|
273
|
+
if (scenario === "presentation") {
|
|
274
|
+
const longReport = [
|
|
275
|
+
"presentation opening",
|
|
276
|
+
"A".repeat(2_600),
|
|
277
|
+
"MIDDLE-ONLY-IN-EXPANDED-PRESENTATION",
|
|
278
|
+
"B".repeat(4_000),
|
|
279
|
+
"latest-presentation-marker",
|
|
280
|
+
].join("\n");
|
|
281
|
+
send({ type: "message_start", message: { role: "assistant" } });
|
|
282
|
+
send({ type: "message_update", assistantMessageEvent: { type: "text_start", contentIndex: 0 } });
|
|
283
|
+
send({ type: "message_update", assistantMessageEvent: { type: "text_delta", contentIndex: 0, delta: "partial final assistant text" } });
|
|
284
|
+
send({ type: "message_update", assistantMessageEvent: { type: "text_end", contentIndex: 0, content: "replaced final assistant text" } });
|
|
285
|
+
send({ type: "message_end", message: { role: "assistant", stopReason: "stop", content: [{ type: "text", text: longReport }], usage: usage(5, 3) } });
|
|
286
|
+
send({ type: "agent_settled" });
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
send({
|
|
290
|
+
type: "message_end",
|
|
291
|
+
message: { role: "assistant", stopReason: "stop", content: [{ type: "text", text: "deterministic worker complete" }], usage: usage(5, 3) },
|
|
292
|
+
});
|
|
293
|
+
send({ type: "agent_settled" });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
process.on("SIGTERM", () => {
|
|
297
|
+
if (observationPath) appendFileSync(observationPath, `${JSON.stringify({ kind: "signal", signal: "SIGTERM" })}\n`, "utf8");
|
|
298
|
+
process.exit(0);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Expose the disposable test process identity because Windows terminates it without delivering SIGTERM.
|
|
302
|
+
await observe({ kind: "worker-start", pid: process.pid });
|
|
303
|
+
|
|
304
|
+
// Keep strict LF-framed RPC command handling separate from private bidirectional IPC.
|
|
26
305
|
await readStrictJsonl(process.stdin, async (line) => {
|
|
27
306
|
if (!line.trim()) return;
|
|
28
307
|
const command = JSON.parse(line);
|
|
@@ -54,155 +333,15 @@ await readStrictJsonl(process.stdin, async (line) => {
|
|
|
54
333
|
send({ type: "response", id: command.id, command: command.type, success: true, data: { disposition: "handled" } });
|
|
55
334
|
return;
|
|
56
335
|
}
|
|
57
|
-
|
|
58
336
|
if (promptCount === 1) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
send({ type: "response", id: command.id, command: command.type, success: false, error: "codemode extension was injected without explicit selection" });
|
|
337
|
+
try {
|
|
338
|
+
await validateFirstPrompt();
|
|
339
|
+
} catch (error) {
|
|
340
|
+
send({ type: "response", id: command.id, command: command.type, success: false, error: error instanceof Error ? error.message : String(error) });
|
|
64
341
|
return;
|
|
65
342
|
}
|
|
66
|
-
if (scenario === "codemode") {
|
|
67
|
-
const nativeCodemodePath = extensionValues.find((value) => value?.endsWith("codemode-only.ts"));
|
|
68
|
-
let nativeCodemodeSource = "";
|
|
69
|
-
try {
|
|
70
|
-
if (nativeCodemodePath) nativeCodemodeSource = await readFile(nativeCodemodePath, "utf8");
|
|
71
|
-
} catch {
|
|
72
|
-
// The failed source check below reports a deterministic fake-RPC error.
|
|
73
|
-
}
|
|
74
|
-
if (
|
|
75
|
-
!selectedTools.includes("codemode")
|
|
76
|
-
|| extensionValues.includes("builtin:codemode")
|
|
77
|
-
|| !nativeCodemodePath
|
|
78
|
-
|| !nativeCodemodeSource.includes('createCodemodeExtension({ mode: "only" })')
|
|
79
|
-
|| !nativeCodemodeSource.includes('from "@earendil-works/pi-coding-agent"')
|
|
80
|
-
) {
|
|
81
|
-
send({ type: "response", id: command.id, command: command.type, success: false, error: "native codemode-only extension was not isolated and loaded" });
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
343
|
}
|
|
86
|
-
|
|
87
344
|
send({ type: "response", id: command.id, command: command.type, success: true, data: { disposition: "started" } });
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
send({ type: "message_start", message: { role: "assistant" } });
|
|
91
|
-
send({ type: "message_update", assistantMessageEvent: { type: "text_start", contentIndex: 0 } });
|
|
92
|
-
send({ type: "message_update", assistantMessageEvent: { type: "text_delta", contentIndex: 0, delta: "stream delta that must be replaced" } });
|
|
93
|
-
// Keep the fake stream active long enough to exercise the dispatcher's coalesced live update.
|
|
94
|
-
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
95
|
-
send({ type: "message_update", assistantMessageEvent: { type: "text_end", contentIndex: 0, content: "stream block that must be replaced" } });
|
|
96
|
-
send({
|
|
97
|
-
type: "message_end",
|
|
98
|
-
message: { role: "assistant", stopReason: "toolUse", content: [{ type: "text", text: "authoritative first assistant message" }], usage: usage(10, 2) },
|
|
99
|
-
});
|
|
100
|
-
send({ type: "tool_execution_start", toolCallId: "fake-presentation-read", toolName: "read", args: { path: "presentation.txt" } });
|
|
101
|
-
send({
|
|
102
|
-
type: "tool_execution_update",
|
|
103
|
-
toolCallId: "fake-presentation-read",
|
|
104
|
-
toolName: "read",
|
|
105
|
-
args: { path: "presentation.txt" },
|
|
106
|
-
partialResult: { content: [{ type: "text", text: "partial read result that must be replaced" }] },
|
|
107
|
-
});
|
|
108
|
-
// Keep the partial tool result observable before the authoritative result replaces it.
|
|
109
|
-
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
110
|
-
send({
|
|
111
|
-
type: "tool_execution_end",
|
|
112
|
-
toolCallId: "fake-presentation-read",
|
|
113
|
-
toolName: "read",
|
|
114
|
-
isError: false,
|
|
115
|
-
result: {
|
|
116
|
-
content: [{ type: "text", text: "final read result one\nfinal read result two\nfinal read result three\nfinal read result four\nfinal read result five\nfinal read result six \u001b[31mred\u001b[0m" }],
|
|
117
|
-
usage: usage(3, 1),
|
|
118
|
-
},
|
|
119
|
-
});
|
|
120
|
-
process.send?.({ type: "task_dispatcher_boundary", turnIndex: 0, toolResults: 1 });
|
|
121
|
-
send({ type: "message_end", message: { role: "assistant", stopReason: "error", errorMessage: "This operation was aborted", content: [] } });
|
|
122
|
-
send({ type: "agent_settled" });
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
const longReport = [
|
|
126
|
-
"presentation opening",
|
|
127
|
-
"A".repeat(2_600),
|
|
128
|
-
"MIDDLE-ONLY-IN-EXPANDED-PRESENTATION",
|
|
129
|
-
"B".repeat(4_000),
|
|
130
|
-
"latest-presentation-marker",
|
|
131
|
-
].join("\n");
|
|
132
|
-
send({ type: "message_start", message: { role: "assistant" } });
|
|
133
|
-
send({ type: "message_update", assistantMessageEvent: { type: "text_start", contentIndex: 0 } });
|
|
134
|
-
send({ type: "message_update", assistantMessageEvent: { type: "text_delta", contentIndex: 0, delta: "partial final assistant text" } });
|
|
135
|
-
send({ type: "message_update", assistantMessageEvent: { type: "text_end", contentIndex: 0, content: "replaced final assistant text" } });
|
|
136
|
-
send({
|
|
137
|
-
type: "message_end",
|
|
138
|
-
message: { role: "assistant", stopReason: "stop", content: [{ type: "text", text: longReport }], usage: usage(5, 3) },
|
|
139
|
-
});
|
|
140
|
-
send({ type: "agent_settled" });
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
if (scenario === "length" && promptCount === 1) {
|
|
144
|
-
send({
|
|
145
|
-
type: "message_end",
|
|
146
|
-
message: {
|
|
147
|
-
role: "assistant",
|
|
148
|
-
stopReason: "length",
|
|
149
|
-
content: [{ type: "text", text: "truncated worker report" }],
|
|
150
|
-
usage: usage(7, 4),
|
|
151
|
-
},
|
|
152
|
-
});
|
|
153
|
-
send({ type: "agent_settled" });
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
if (promptCount === 1) {
|
|
157
|
-
send({
|
|
158
|
-
type: "message_end",
|
|
159
|
-
message: { role: "assistant", stopReason: "toolUse", content: [], usage: usage(10, 2) },
|
|
160
|
-
});
|
|
161
|
-
if (scenario === "codemode") {
|
|
162
|
-
send({ type: "tool_execution_start", toolCallId: "fake-codemode-1", toolName: "codemode", args: "return image" });
|
|
163
|
-
send({ type: "tool_execution_start", toolCallId: "fake-read-nested", parentToolCallId: "fake-codemode-1", toolName: "read", args: { path: "reference.png" } });
|
|
164
|
-
send({ type: "tool_execution_end", toolCallId: "fake-read-nested", parentToolCallId: "fake-codemode-1", toolName: "read", isError: false, result: { usage: usage(3, 1) } });
|
|
165
|
-
send({
|
|
166
|
-
type: "tool_execution_end",
|
|
167
|
-
toolCallId: "fake-codemode-1",
|
|
168
|
-
toolName: "codemode",
|
|
169
|
-
isError: false,
|
|
170
|
-
result: {
|
|
171
|
-
content: [
|
|
172
|
-
{ type: "text", text: "generated image retained in runtime temporary storage" },
|
|
173
|
-
{ type: "image", data: "ZmFrZQ==", mimeType: "image/png" },
|
|
174
|
-
],
|
|
175
|
-
usage: usage(7, 3),
|
|
176
|
-
},
|
|
177
|
-
});
|
|
178
|
-
} else {
|
|
179
|
-
send({ type: "tool_execution_start", toolCallId: "fake-read-1", toolName: "read", args: { path: "package.json" } });
|
|
180
|
-
send({ type: "tool_execution_end", toolCallId: "fake-read-1", toolName: "read", isError: false, result: { usage: usage(3, 1) } });
|
|
181
|
-
}
|
|
182
|
-
if (scenario === "missing-boundary") {
|
|
183
|
-
send({
|
|
184
|
-
type: "message_end",
|
|
185
|
-
message: { role: "assistant", stopReason: "stop", content: [{ type: "text", text: "unsafe final response after tool" }], usage: usage(2, 2) },
|
|
186
|
-
});
|
|
187
|
-
} else {
|
|
188
|
-
process.send?.({ type: "task_dispatcher_boundary", turnIndex: 0, toolResults: 1 });
|
|
189
|
-
send({
|
|
190
|
-
type: "message_end",
|
|
191
|
-
message: { role: "assistant", stopReason: "error", errorMessage: "This operation was aborted", content: [] },
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
send({ type: "agent_settled" });
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
send({
|
|
199
|
-
type: "message_end",
|
|
200
|
-
message: {
|
|
201
|
-
role: "assistant",
|
|
202
|
-
stopReason: "stop",
|
|
203
|
-
content: [{ type: "text", text: "deterministic worker complete" }],
|
|
204
|
-
usage: usage(5, 3),
|
|
205
|
-
},
|
|
206
|
-
});
|
|
207
|
-
send({ type: "agent_settled" });
|
|
345
|
+
if (promptCount === 1) await runFirstPrompt();
|
|
346
|
+
else await runContinuation();
|
|
208
347
|
});
|