@springbrand/agent-runtime 0.3.0-alpha.25 → 0.3.0-alpha.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/config/ports.ts +10 -1
- package/src/tools/README.md +24 -0
- package/src/tools/adapters/pi-to-ai.ts +13 -1
- package/src/tools/codemode/adapter.ts +61 -31
- package/src/tools/codemode/browser-connector.ts +28 -0
- package/src/tools/codemode/executor.ts +173 -0
- package/src/tools/codemode/index.ts +25 -32
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@springbrand/agent-runtime",
|
|
3
|
-
"version": "0.3.0-alpha.
|
|
3
|
+
"version": "0.3.0-alpha.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"zod": "^4.4.3",
|
|
37
37
|
"@earendil-works/pi-durable": "1.1.0",
|
|
38
38
|
"@earendil-works/chord": "1.1.0",
|
|
39
|
-
"@springbrand/model-client": "0.1.0-alpha.
|
|
39
|
+
"@springbrand/model-client": "0.1.0-alpha.2"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"agents": ">=0.28.1 <0.29.0",
|
package/src/config/ports.ts
CHANGED
|
@@ -264,7 +264,16 @@ export interface WorkspaceStreamWriteOptions {
|
|
|
264
264
|
*/
|
|
265
265
|
export interface RuntimeCodeExecutionPort {
|
|
266
266
|
readonly description: string;
|
|
267
|
-
|
|
267
|
+
/**
|
|
268
|
+
* Honour a provided signal before dispatch and throughout execution.
|
|
269
|
+
* On abort, stop suspended sandbox continuations and further Host dispatches,
|
|
270
|
+
* await execution finalization, and reject rather than accepting a late result.
|
|
271
|
+
* Already-started Host effects are not rolled back; CPU preemption is not guaranteed.
|
|
272
|
+
*/
|
|
273
|
+
execute(
|
|
274
|
+
input: { code: string },
|
|
275
|
+
options?: { signal?: AbortSignal },
|
|
276
|
+
): Promise<AgentToolResult<unknown>>;
|
|
268
277
|
}
|
|
269
278
|
|
|
270
279
|
/**
|
package/src/tools/README.md
CHANGED
|
@@ -34,6 +34,30 @@ codemode/adapter.ts Cloudflare 执行器组装、描述与结果适
|
|
|
34
34
|
|
|
35
35
|
`pi-to-ai.ts` 也被 Skill 脚本执行使用,因此放在共用 `adapters/` 中。工具声明仍以 Pi 为唯一事实源。
|
|
36
36
|
|
|
37
|
+
### Cancellation And Settlement
|
|
38
|
+
|
|
39
|
+
`RuntimeCodeExecutionPort.execute(input, { signal })` carries cancellation
|
|
40
|
+
through the adapter to the invocation's native sandbox. Parent cancellation
|
|
41
|
+
and the Tool deadline share this path. The Port awaits sandbox finalization
|
|
42
|
+
and connector cleanup before rejecting; it does not abandon a running Promise.
|
|
43
|
+
|
|
44
|
+
`codemode/executor.ts` composes the public Cloudflare executor with a retained,
|
|
45
|
+
per-invocation RPC cancellation handle. It aborts the sandbox invocation, never
|
|
46
|
+
the parent Host context or a sibling invocation. This does not guarantee CPU
|
|
47
|
+
preemption, Worker destruction, quota release or rollback of Host effects.
|
|
48
|
+
|
|
49
|
+
Already-started Host work can finish after sandbox cancellation. Its returned
|
|
50
|
+
facts must still reach the completion callback and the official connector's
|
|
51
|
+
durable result history. That does not permit model continuation or accept a
|
|
52
|
+
cancelled Tool's termination control. Producer-owned usage events remain
|
|
53
|
+
authoritative; an aborted outer Tool must not be interpreted as zero usage.
|
|
54
|
+
|
|
55
|
+
Browser invocations each own an `InvocationBrowserConnector`. At pass end it
|
|
56
|
+
rejects new dispatches and drains its already-started browser calls before the
|
|
57
|
+
public SDK closes the socket and one-shot session. An allocation can complete
|
|
58
|
+
after sandbox abort; the Port waits for that late allocation and its cleanup
|
|
59
|
+
rather than reporting terminal cleanup early. Other Host tools are not drained.
|
|
60
|
+
|
|
37
61
|
## 修改位置
|
|
38
62
|
|
|
39
63
|
- 修改某个工具:进入对应功能目录,测试与实现放在一起。
|
|
@@ -4,7 +4,10 @@ import { tool as aiTool, jsonSchema, type ToolSet } from "ai";
|
|
|
4
4
|
import type { PiToolCandidate } from "../runtime/contracts";
|
|
5
5
|
|
|
6
6
|
interface NestedToolOptions {
|
|
7
|
+
/** Bound by the execution when the upstream connector omits AI SDK call options. */
|
|
8
|
+
readonly signal?: AbortSignal;
|
|
7
9
|
readonly fallbackToolCallId?: (name: string) => string;
|
|
10
|
+
/** Settled Host facts, even after cancellation; not permission for model control. */
|
|
8
11
|
readonly onResult?: (result: AgentToolResult<unknown>) => void;
|
|
9
12
|
}
|
|
10
13
|
|
|
@@ -19,6 +22,11 @@ export function piCandidatesToAiTools(
|
|
|
19
22
|
description: candidate.tool.description,
|
|
20
23
|
inputSchema: jsonSchema(candidate.tool.parameters as never),
|
|
21
24
|
execute: async (input, call) => {
|
|
25
|
+
const callSignal = call?.abortSignal;
|
|
26
|
+
const signal = options.signal && callSignal && options.signal !== callSignal
|
|
27
|
+
? AbortSignal.any([options.signal, callSignal])
|
|
28
|
+
: options.signal ?? callSignal ?? new AbortController().signal;
|
|
29
|
+
signal.throwIfAborted();
|
|
22
30
|
const toolCallId = call?.toolCallId ??
|
|
23
31
|
options.fallbackToolCallId?.(candidate.tool.name) ??
|
|
24
32
|
candidate.tool.name;
|
|
@@ -29,11 +37,15 @@ export function piCandidatesToAiTools(
|
|
|
29
37
|
name: candidate.tool.name,
|
|
30
38
|
arguments: prepared as JsonObject,
|
|
31
39
|
});
|
|
40
|
+
signal.throwIfAborted();
|
|
32
41
|
const result = await candidate.tool.execute(
|
|
33
42
|
toolCallId,
|
|
34
43
|
args,
|
|
35
|
-
|
|
44
|
+
signal,
|
|
36
45
|
);
|
|
46
|
+
// A resolved Host operation is a fact even if cancellation happened
|
|
47
|
+
// while it ran. Let settlement and the connector's durable recordResult
|
|
48
|
+
// finish; the outer Code Mode boundary rejects model continuation.
|
|
37
49
|
options.onResult?.(result);
|
|
38
50
|
if (result.details !== undefined) return result.details;
|
|
39
51
|
const [only] = result.content;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { truncateResponse, truncateResult, type ProxyToolOutput } from "@cloudflare/codemode";
|
|
1
|
+
import { createCodemodeRuntime, truncateResponse, truncateResult, type ProxyToolOutput } from "@cloudflare/codemode";
|
|
2
2
|
import { generateTypes } from "@cloudflare/codemode/ai";
|
|
3
3
|
import {
|
|
4
4
|
STATE_TYPES,
|
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
type StateBackend,
|
|
7
7
|
type WorkspaceFsLike,
|
|
8
8
|
} from "@cloudflare/shell";
|
|
9
|
-
import { createBrowserTools } from "@cloudflare/think/tools/browser";
|
|
10
9
|
import { createExecuteRuntime } from "@cloudflare/think/tools/execute";
|
|
11
10
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
12
11
|
import type { Usage } from "@earendil-works/pi-ai";
|
|
12
|
+
import { DurableBrowserSessionStore } from "agents/browser";
|
|
13
13
|
import type { ToolSet } from "ai";
|
|
14
14
|
import type {
|
|
15
15
|
RuntimeBrowserPort,
|
|
@@ -19,6 +19,8 @@ import type {
|
|
|
19
19
|
} from "../../config/ports";
|
|
20
20
|
import { serializeOutput } from "../../lib/artifacts";
|
|
21
21
|
import { piCandidatesToAiTools } from "../adapters/pi-to-ai";
|
|
22
|
+
import { InvocationBrowserConnector } from "./browser-connector";
|
|
23
|
+
import { createCodeExecutionExecutor } from "./executor";
|
|
22
24
|
import {
|
|
23
25
|
WORKSPACE_PAGING_INSTRUCTIONS,
|
|
24
26
|
WORKSPACE_STATE_TYPES,
|
|
@@ -226,11 +228,23 @@ function toCodeExecutionPort(
|
|
|
226
228
|
}
|
|
227
229
|
return {
|
|
228
230
|
description,
|
|
229
|
-
execute: async (input) =>
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
231
|
+
execute: async (input, options) => {
|
|
232
|
+
const signal = options?.signal;
|
|
233
|
+
signal?.throwIfAborted();
|
|
234
|
+
try {
|
|
235
|
+
const output = await execute(input, {
|
|
236
|
+
toolCallId: name,
|
|
237
|
+
messages: [],
|
|
238
|
+
context: undefined,
|
|
239
|
+
abortSignal: signal,
|
|
240
|
+
});
|
|
241
|
+
signal?.throwIfAborted();
|
|
242
|
+
return project(output);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
signal?.throwIfAborted();
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
},
|
|
234
248
|
};
|
|
235
249
|
}
|
|
236
250
|
|
|
@@ -292,30 +306,38 @@ export function createBrowserExecutionFactory(options: {
|
|
|
292
306
|
}): RuntimeBrowserPort {
|
|
293
307
|
return {
|
|
294
308
|
create() {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
309
|
+
// Compose public seams with per-invocation ownership. No Quick Actions.
|
|
310
|
+
const createPort = (signal?: AbortSignal) => {
|
|
311
|
+
const connector = new InvocationBrowserConnector(options.ctx, {
|
|
312
|
+
browser: options.browser,
|
|
313
|
+
store: new DurableBrowserSessionStore(options.ctx.storage),
|
|
314
|
+
timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
|
|
315
|
+
// Keep the public connector's default one-shot lifecycle.
|
|
316
|
+
});
|
|
317
|
+
const runtime = createCodemodeRuntime({
|
|
318
|
+
ctx: options.ctx,
|
|
319
|
+
executor: createCodeExecutionExecutor({
|
|
320
|
+
loader: options.loader,
|
|
321
|
+
timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
|
|
322
|
+
}, signal),
|
|
323
|
+
connectors: [connector],
|
|
324
|
+
name: "browser",
|
|
325
|
+
// Keep raw screenshots intact for browserResult's Pi image projection.
|
|
326
|
+
});
|
|
327
|
+
return toCodeExecutionPort(runtime.tool({
|
|
328
|
+
connectorHints: {
|
|
329
|
+
cdp: "Browser CDP. Return screenshots as { type: 'browser_screenshot', mediaType: 'image/png', data: screenshot.data }; the UI keeps the image while the model receives a compact summary.",
|
|
330
|
+
},
|
|
331
|
+
}), "browser_execute");
|
|
332
|
+
};
|
|
333
|
+
const description = `${createPort().description}\n\n${browserToolDescription()}`;
|
|
316
334
|
return {
|
|
317
|
-
|
|
318
|
-
description:
|
|
335
|
+
// Keep the Session's exact preview URL ahead of the long generated guidance.
|
|
336
|
+
description: options.previewBaseUrl
|
|
337
|
+
? `${previewNote(options.previewBaseUrl)}\n\n${description}`
|
|
338
|
+
: description,
|
|
339
|
+
execute: (input, executionOptions) =>
|
|
340
|
+
createPort(executionOptions?.signal).execute(input, executionOptions),
|
|
319
341
|
};
|
|
320
342
|
},
|
|
321
343
|
};
|
|
@@ -343,9 +365,11 @@ export function createWorkspaceCodeExecutionFactory(options: {
|
|
|
343
365
|
: undefined;
|
|
344
366
|
const createPort = (
|
|
345
367
|
innerResults: AgentToolResult<unknown>[],
|
|
368
|
+
signal?: AbortSignal,
|
|
346
369
|
) => {
|
|
347
370
|
let nestedCallOrdinal = 0;
|
|
348
371
|
const tools = piCandidatesToAiTools(candidates, {
|
|
372
|
+
signal,
|
|
349
373
|
fallbackToolCallId: (name) =>
|
|
350
374
|
`codemode:${++nestedCallOrdinal}:${name}`,
|
|
351
375
|
onResult: (value) => innerResults.push(value),
|
|
@@ -354,6 +378,11 @@ export function createWorkspaceCodeExecutionFactory(options: {
|
|
|
354
378
|
ctx: options.ctx,
|
|
355
379
|
loader: options.loader,
|
|
356
380
|
globalOutbound: options.outbound,
|
|
381
|
+
executor: createCodeExecutionExecutor({
|
|
382
|
+
loader: options.loader,
|
|
383
|
+
globalOutbound: options.outbound,
|
|
384
|
+
timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
|
|
385
|
+
}, signal),
|
|
357
386
|
tools,
|
|
358
387
|
timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
|
|
359
388
|
connectors: workspaceState ? [workspaceState] : [],
|
|
@@ -392,7 +421,8 @@ export function createWorkspaceCodeExecutionFactory(options: {
|
|
|
392
421
|
...(state && options.workspace.readFilePage ? [WORKSPACE_PAGING_INSTRUCTIONS] : []),
|
|
393
422
|
SPRINGBRAND_CODE_MODE_POLICY,
|
|
394
423
|
].join("\n\n"),
|
|
395
|
-
execute: (input) =>
|
|
424
|
+
execute: (input, executionOptions) =>
|
|
425
|
+
createPort([], executionOptions?.signal).execute(input, executionOptions),
|
|
396
426
|
};
|
|
397
427
|
},
|
|
398
428
|
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { BrowserConnector } from "agents/browser";
|
|
2
|
+
|
|
3
|
+
/** Drains one invocation's browser calls before the upstream pass lifecycle ends. */
|
|
4
|
+
export class InvocationBrowserConnector extends BrowserConnector {
|
|
5
|
+
readonly #pending = new Set<Promise<unknown>>();
|
|
6
|
+
#ending = false;
|
|
7
|
+
|
|
8
|
+
override async executeTool(
|
|
9
|
+
...args: Parameters<BrowserConnector["executeTool"]>
|
|
10
|
+
): Promise<unknown> {
|
|
11
|
+
if (this.#ending) throw new Error("Browser execution has ended");
|
|
12
|
+
const pending = super.executeTool(...args);
|
|
13
|
+
this.#pending.add(pending);
|
|
14
|
+
try {
|
|
15
|
+
return await pending;
|
|
16
|
+
} finally {
|
|
17
|
+
this.#pending.delete(pending);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
override async onPassEnd(
|
|
22
|
+
...args: Parameters<BrowserConnector["onPassEnd"]>
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
this.#ending = true;
|
|
25
|
+
await Promise.allSettled(this.#pending);
|
|
26
|
+
await super.onPassEnd(...args);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/// <reference lib="esnext.disposable" />
|
|
2
|
+
import {
|
|
3
|
+
DynamicWorkerExecutor,
|
|
4
|
+
type ConnectorBinding,
|
|
5
|
+
type DynamicWorkerExecutorOptions,
|
|
6
|
+
type ExecuteResult,
|
|
7
|
+
type Executor,
|
|
8
|
+
type ResolvedProvider,
|
|
9
|
+
} from "@cloudflare/codemode";
|
|
10
|
+
import { RpcTarget } from "cloudflare:workers";
|
|
11
|
+
|
|
12
|
+
const CONTROL = "__springbrand_execution_control";
|
|
13
|
+
const SETUP = "__springbrand_execution_setup";
|
|
14
|
+
const CANCELLED = "Code execution cancelled";
|
|
15
|
+
|
|
16
|
+
interface AbortHandle {
|
|
17
|
+
abort(): Promise<void>;
|
|
18
|
+
dup(): AbortHandle;
|
|
19
|
+
[Symbol.dispose](): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isAbortHandle(value: unknown): value is AbortHandle {
|
|
23
|
+
return (typeof value === "object" && value !== null || typeof value === "function") &&
|
|
24
|
+
typeof Reflect.get(value, "abort") === "function" &&
|
|
25
|
+
typeof Reflect.get(value, "dup") === "function" &&
|
|
26
|
+
typeof Reflect.get(value, Symbol.dispose) === "function";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class ExecutionControl extends RpcTarget {
|
|
30
|
+
#handle?: AbortHandle;
|
|
31
|
+
#registered = false;
|
|
32
|
+
#requested = false;
|
|
33
|
+
#aborting?: Promise<void>;
|
|
34
|
+
failure?: unknown;
|
|
35
|
+
|
|
36
|
+
constructor(readonly abortMessage: string) {
|
|
37
|
+
super();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async callTool(method: string, value: unknown): Promise<void> {
|
|
41
|
+
if (method !== "register" || this.#registered || !isAbortHandle(value)) {
|
|
42
|
+
throw new Error("Invalid Code Mode cancellation registration");
|
|
43
|
+
}
|
|
44
|
+
this.#registered = true;
|
|
45
|
+
this.#handle = value.dup();
|
|
46
|
+
if (this.#requested) {
|
|
47
|
+
this.abort();
|
|
48
|
+
await this.#aborting;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
abort(): void {
|
|
53
|
+
this.#requested = true;
|
|
54
|
+
const handle = this.#handle;
|
|
55
|
+
if (!handle || this.#aborting) return;
|
|
56
|
+
this.#aborting = Promise.resolve().then(() => handle.abort()).then(
|
|
57
|
+
() => undefined,
|
|
58
|
+
(error: unknown) => {
|
|
59
|
+
if (!(error instanceof Error && error.message === this.abortMessage)) {
|
|
60
|
+
this.failure = error;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async close(): Promise<void> {
|
|
67
|
+
this.abort();
|
|
68
|
+
await this.#aborting;
|
|
69
|
+
try {
|
|
70
|
+
this.#handle?.[Symbol.dispose]();
|
|
71
|
+
} catch (error) {
|
|
72
|
+
this.failure = error;
|
|
73
|
+
} finally {
|
|
74
|
+
this.#handle = undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
class GuardedConnector extends RpcTarget {
|
|
80
|
+
constructor(
|
|
81
|
+
readonly binding: ConnectorBinding["binding"],
|
|
82
|
+
readonly check: () => void,
|
|
83
|
+
) {
|
|
84
|
+
super();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async callTool(method: string, args: unknown): Promise<unknown> {
|
|
88
|
+
this.check();
|
|
89
|
+
return await this.binding.callTool(method, args);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function setup(abortMessage: string): string {
|
|
94
|
+
return `await (async () => {
|
|
95
|
+
const { RpcTarget } = await import("cloudflare:workers");
|
|
96
|
+
const context = this.ctx;
|
|
97
|
+
if (typeof context.abort !== "function") throw new Error("Native Code Mode cancellation is unavailable");
|
|
98
|
+
const abort = context.abort.bind(context);
|
|
99
|
+
const reason = new Error(${JSON.stringify(abortMessage)});
|
|
100
|
+
class Cancellation extends RpcTarget {
|
|
101
|
+
abort() { abort(reason); }
|
|
102
|
+
}
|
|
103
|
+
await ${CONTROL}.register(new Cancellation());
|
|
104
|
+
})();`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Cancels a native sandbox pass, not its Host effects or the loaded Worker isolate. */
|
|
108
|
+
export function createCodeExecutionExecutor(
|
|
109
|
+
options: DynamicWorkerExecutorOptions,
|
|
110
|
+
signal?: AbortSignal,
|
|
111
|
+
): Executor {
|
|
112
|
+
const executor = new DynamicWorkerExecutor(options);
|
|
113
|
+
return {
|
|
114
|
+
async execute(code, providersOrFns, executeOptions): Promise<ExecuteResult> {
|
|
115
|
+
if (signal?.aborted) return { result: undefined, error: CANCELLED };
|
|
116
|
+
const providers: ResolvedProvider[] = Array.isArray(providersOrFns)
|
|
117
|
+
? providersOrFns
|
|
118
|
+
: [{ name: "codemode", fns: providersOrFns }];
|
|
119
|
+
const connectors = executeOptions?.connectors ?? [];
|
|
120
|
+
if ([...providers, ...connectors].some(({ name }) => name === CONTROL || name === SETUP)) {
|
|
121
|
+
return { result: undefined, error: "Code execution uses a reserved namespace" };
|
|
122
|
+
}
|
|
123
|
+
const control = new ExecutionControl(`Code Mode pass aborted (${crypto.randomUUID()})`);
|
|
124
|
+
let ended = false;
|
|
125
|
+
const check = () => {
|
|
126
|
+
if (signal?.aborted || ended) throw new Error(CANCELLED);
|
|
127
|
+
};
|
|
128
|
+
const onAbort = () => control.abort();
|
|
129
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
130
|
+
let result: ExecuteResult;
|
|
131
|
+
try {
|
|
132
|
+
if (signal?.aborted) return { result: undefined, error: CANCELLED };
|
|
133
|
+
result = await executor.execute(code, [{
|
|
134
|
+
name: SETUP,
|
|
135
|
+
fns: {},
|
|
136
|
+
prelude: setup(control.abortMessage),
|
|
137
|
+
}, ...providers.map((provider) => ({
|
|
138
|
+
...provider,
|
|
139
|
+
fns: Object.fromEntries(Object.entries(provider.fns).map(([name, fn]) => [
|
|
140
|
+
name,
|
|
141
|
+
async (...args: unknown[]) => {
|
|
142
|
+
check();
|
|
143
|
+
return await fn(...args);
|
|
144
|
+
},
|
|
145
|
+
])),
|
|
146
|
+
}))], {
|
|
147
|
+
...executeOptions,
|
|
148
|
+
connectors: [{
|
|
149
|
+
name: CONTROL,
|
|
150
|
+
binding: control,
|
|
151
|
+
}, ...connectors.map((connector) => ({
|
|
152
|
+
...connector,
|
|
153
|
+
binding: new GuardedConnector(connector.binding, check),
|
|
154
|
+
}))],
|
|
155
|
+
});
|
|
156
|
+
} catch (error) {
|
|
157
|
+
result = {
|
|
158
|
+
result: undefined,
|
|
159
|
+
error: error instanceof Error && error.message === control.abortMessage
|
|
160
|
+
? CANCELLED
|
|
161
|
+
: error instanceof Error ? error.message : String(error),
|
|
162
|
+
};
|
|
163
|
+
} finally {
|
|
164
|
+
ended = true;
|
|
165
|
+
signal?.removeEventListener("abort", onAbort);
|
|
166
|
+
// A returned SDK timeout is only a soft race; stop the native pass too.
|
|
167
|
+
await control.close();
|
|
168
|
+
}
|
|
169
|
+
if (control.failure) return { result: undefined, error: "Failed to stop the Code Mode pass" };
|
|
170
|
+
return signal?.aborted ? { result: undefined, error: CANCELLED } : result;
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { truncateResult } from "@cloudflare/codemode";
|
|
1
2
|
import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
3
|
import { Type } from "@earendil-works/pi-ai";
|
|
3
4
|
import type { RuntimeCodeExecutionPort } from "../../config/ports";
|
|
@@ -86,17 +87,18 @@ function browserResult(details: unknown): AgentToolResult<unknown> {
|
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
const image = record(inner?.image);
|
|
90
|
+
const nativeImage = inner?.type === "browser_screenshot" ? inner : undefined;
|
|
89
91
|
const screenshotArgs = record(screenshotCall?.args);
|
|
90
92
|
const screenshotResult = record(screenshotCall?.result);
|
|
91
93
|
const format = record(screenshotArgs?.params)?.format;
|
|
92
94
|
const data = typeof image?.data === "string"
|
|
93
95
|
? image.data
|
|
94
|
-
: screenshotResult?.data;
|
|
96
|
+
: nativeImage ? nativeImage.data : screenshotResult?.data;
|
|
95
97
|
const mimeType = typeof image?.mimeType === "string"
|
|
96
98
|
? image.mimeType
|
|
97
|
-
: screenshotMimeType(format);
|
|
98
|
-
if (!(inner && "image" in inner) && typeof data !== "string") {
|
|
99
|
-
return result(details);
|
|
99
|
+
: nativeImage ? nativeImage.mediaType : screenshotMimeType(format);
|
|
100
|
+
if (!(inner && "image" in inner) && !nativeImage && typeof data !== "string") {
|
|
101
|
+
return result("result" in outer ? { ...outer, result: truncateResult(rawResult) } : details);
|
|
100
102
|
}
|
|
101
103
|
const projectedImage = browserImageMetadata(data, mimeType);
|
|
102
104
|
|
|
@@ -127,10 +129,9 @@ function browserResult(details: unknown): AgentToolResult<unknown> {
|
|
|
127
129
|
const projectedDetails = {
|
|
128
130
|
...outer,
|
|
129
131
|
result: inner
|
|
130
|
-
?
|
|
131
|
-
...inner,
|
|
132
|
-
image: projectedImage
|
|
133
|
-
}
|
|
132
|
+
? nativeImage
|
|
133
|
+
? { ...inner, data: projectedImage }
|
|
134
|
+
: { ...inner, image: projectedImage }
|
|
134
135
|
: {
|
|
135
136
|
image: projectedImage,
|
|
136
137
|
note: "Screenshot recovered from the Page.captureScreenshot result.",
|
|
@@ -172,30 +173,18 @@ function runCodemode(
|
|
|
172
173
|
): AgentTool<typeof executeParameters>["execute"] {
|
|
173
174
|
return async (_toolCallId, input, signal) => {
|
|
174
175
|
signal?.throwIfAborted();
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}>((resolve) => {
|
|
184
|
-
timeout = setTimeout(() => resolve({
|
|
185
|
-
status: "error",
|
|
186
|
-
code: "timeout",
|
|
187
|
-
error:
|
|
188
|
-
`${label} exceeded ${CODEMODE_EXECUTE_TIMEOUT_MS} ms; ` +
|
|
189
|
-
"use existing results and state the missing evidence.",
|
|
190
|
-
retryable: false,
|
|
191
|
-
outcome: "unknown",
|
|
192
|
-
}), CODEMODE_EXECUTE_TIMEOUT_MS);
|
|
193
|
-
});
|
|
176
|
+
const execution = new AbortController();
|
|
177
|
+
const cancel = () => execution.abort(signal?.reason);
|
|
178
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
179
|
+
// The first abort reason distinguishes a parent cancellation from our deadline.
|
|
180
|
+
const timeout = setTimeout(() => execution.abort(new Error(
|
|
181
|
+
`${label} exceeded ${CODEMODE_EXECUTE_TIMEOUT_MS} ms; ` +
|
|
182
|
+
"use existing results and state the missing evidence.",
|
|
183
|
+
)), CODEMODE_EXECUTE_TIMEOUT_MS);
|
|
194
184
|
try {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
]);
|
|
185
|
+
// The Port settles only after the executor and connector cleanup finish.
|
|
186
|
+
const output = await runtime.execute(input, { signal: execution.signal });
|
|
187
|
+
execution.signal.throwIfAborted();
|
|
199
188
|
const details = output.details as { status?: unknown; error?: unknown } | null;
|
|
200
189
|
if (details?.status === "error") {
|
|
201
190
|
throw new Error(typeof details.error === "string"
|
|
@@ -203,8 +192,12 @@ function runCodemode(
|
|
|
203
192
|
: `${label} failed`);
|
|
204
193
|
}
|
|
205
194
|
return project(output);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
execution.signal.throwIfAborted();
|
|
197
|
+
throw error;
|
|
206
198
|
} finally {
|
|
207
|
-
|
|
199
|
+
clearTimeout(timeout);
|
|
200
|
+
signal?.removeEventListener("abort", cancel);
|
|
208
201
|
}
|
|
209
202
|
};
|
|
210
203
|
}
|