@springbrand/agent-runtime 0.2.0-alpha.41 → 0.2.0-alpha.42
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 +1 -1
- package/src/adapter/cloudflare/resources/r2-skill-source.ts +215 -0
- package/src/adapter/cloudflare/resources/runtime-resources.ts +1 -18
- package/src/adapter/cloudflare/universal-agent/preparation.ts +20 -1
- package/src/adapter/cloudflare/universal-agent/tools.ts +3 -0
- package/src/index.ts +2 -0
- package/src/kernel/bindings.ts +28 -6
- package/src/lib/prompt.ts +31 -10
- package/src/pi/assembly/snapshot.ts +7 -1
- package/src/pi/runtime-adapter/assembly.ts +8 -3
- package/src/pi/runtime-adapter/execution.ts +106 -12
- package/src/pi/tool/base.ts +29 -5
- package/src/pi/tool/compiler.ts +4 -0
- package/src/pi/tool/core-host.ts +143 -1
- package/src/pi/tool/core.ts +34 -3
- package/src/pi/tool/declared.ts +2 -0
- package/src/pi/tool/schedule.ts +3 -1
- package/src/pi/tool/skill.ts +67 -78
- package/src/runtime-agent.ts +3 -0
- package/src/runtime-assembler.ts +97 -6
- package/src/runtime-definition.ts +1 -0
- package/src/runtime.ts +22 -0
|
@@ -25,6 +25,7 @@ import { PiChunkEncoder } from "../message";
|
|
|
25
25
|
import type { UIMessageChunk } from "ai";
|
|
26
26
|
import { ChatStreamStalledError } from "agents/chat";
|
|
27
27
|
import {
|
|
28
|
+
codeExecutionPiToolCandidate,
|
|
28
29
|
compilePiTools,
|
|
29
30
|
createPiToolGovernance,
|
|
30
31
|
normalizeUpdatePlanArguments,
|
|
@@ -542,6 +543,25 @@ export interface CreatePreparedPiTurnOptions {
|
|
|
542
543
|
readonly durability: PiTurnDurability;
|
|
543
544
|
/** Reports paired model-generation lifecycle facts to the Runtime owner. */
|
|
544
545
|
readonly onGeneration?: PiGenerationLifecycleObserver;
|
|
546
|
+
/** Reports Code Mode child Tool starts without adding Pi recovery state. */
|
|
547
|
+
readonly onNestedToolStarted?: (input: Readonly<{
|
|
548
|
+
parentToolCallId: string;
|
|
549
|
+
toolCallId: string;
|
|
550
|
+
toolName: string;
|
|
551
|
+
input: unknown;
|
|
552
|
+
occurredAt: number;
|
|
553
|
+
}>) => void;
|
|
554
|
+
/** Reports Code Mode child Tool outcomes without adding Pi settlements. */
|
|
555
|
+
readonly onNestedToolFinished?: (input: Readonly<{
|
|
556
|
+
parentToolCallId: string;
|
|
557
|
+
toolCallId: string;
|
|
558
|
+
toolName: string;
|
|
559
|
+
outcome: "completed" | "failed" | "cancelled";
|
|
560
|
+
durationMs: number;
|
|
561
|
+
output?: import("@earendil-works/pi-agent-core").AgentToolResult<unknown>;
|
|
562
|
+
error?: unknown;
|
|
563
|
+
occurredAt: number;
|
|
564
|
+
}>) => void;
|
|
545
565
|
/** Per-Submission executors for tools whose metadata is fixed at assembly time. */
|
|
546
566
|
readonly toolExecutors?: Readonly<Record<
|
|
547
567
|
string,
|
|
@@ -633,6 +653,7 @@ export class PreparedPiTurnAdapter {
|
|
|
633
653
|
private readonly turn: PiTurnAdapter;
|
|
634
654
|
private readonly abortController = new AbortController();
|
|
635
655
|
private readonly candidates: readonly PiToolCandidate[];
|
|
656
|
+
private nestedToolOrdinal = 0;
|
|
636
657
|
private assistantOrdinal: number;
|
|
637
658
|
private readonly encoder: PiChunkEncoder;
|
|
638
659
|
private readonly steerMessageIds: string[] = [];
|
|
@@ -665,6 +686,8 @@ export class PreparedPiTurnAdapter {
|
|
|
665
686
|
});
|
|
666
687
|
const bindCandidate = (
|
|
667
688
|
candidate: PiToolCandidate,
|
|
689
|
+
toolCallIdPrefix?: string,
|
|
690
|
+
recordRecoveryAttempt = true,
|
|
668
691
|
): PiToolCandidate => ({
|
|
669
692
|
...candidate,
|
|
670
693
|
tool: {
|
|
@@ -673,6 +696,9 @@ export class PreparedPiTurnAdapter {
|
|
|
673
696
|
// 实时运行由 PiCore 调用它,恢复或审批续跑则由 retryTool 进入同一路径。
|
|
674
697
|
// 顺序不能随便调整:门禁先于结果复用,审批可能生成结果,不确定的非幂等工作不能重放。
|
|
675
698
|
execute: async (toolCallId, input, signal, onUpdate) => {
|
|
699
|
+
if (toolCallIdPrefix) {
|
|
700
|
+
toolCallId = `${toolCallIdPrefix}:${toolCallId}`;
|
|
701
|
+
}
|
|
676
702
|
const requiredExecutionLevel = candidate.requiredExecutionLevelForInput
|
|
677
703
|
? await candidate.requiredExecutionLevelForInput(input)
|
|
678
704
|
: candidate.requiredExecutionLevel;
|
|
@@ -783,25 +809,93 @@ export class PreparedPiTurnAdapter {
|
|
|
783
809
|
}
|
|
784
810
|
return responded.result;
|
|
785
811
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
812
|
+
if (recordRecoveryAttempt) {
|
|
813
|
+
const retry = piToolRetryPolicy(candidate);
|
|
814
|
+
const firstAttempt = options.durability.appendToolInput({
|
|
815
|
+
toolCallId,
|
|
816
|
+
toolName: candidate.tool.name,
|
|
817
|
+
input,
|
|
818
|
+
retry,
|
|
819
|
+
});
|
|
820
|
+
if (!firstAttempt && retry === "non-idempotent") {
|
|
821
|
+
throw new Error(
|
|
822
|
+
`Non-idempotent Tool outcome is uncertain after recovery: ${candidate.tool.name}`,
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
const execute = options.toolExecutors?.[candidate.tool.name] ??
|
|
827
|
+
candidate.tool.execute;
|
|
828
|
+
if (recordRecoveryAttempt || !toolCallIdPrefix) {
|
|
829
|
+
return execute(toolCallId, input, signal, onUpdate);
|
|
830
|
+
}
|
|
831
|
+
const startedAt = Date.now();
|
|
832
|
+
const telemetryToolCallId = `${toolCallId}:${++this.nestedToolOrdinal}`;
|
|
833
|
+
options.onNestedToolStarted?.({
|
|
834
|
+
parentToolCallId: toolCallIdPrefix,
|
|
835
|
+
toolCallId: telemetryToolCallId,
|
|
789
836
|
toolName: candidate.tool.name,
|
|
790
837
|
input,
|
|
791
|
-
|
|
838
|
+
occurredAt: startedAt,
|
|
792
839
|
});
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
840
|
+
try {
|
|
841
|
+
const output = await execute(toolCallId, input, signal, onUpdate);
|
|
842
|
+
const occurredAt = Date.now();
|
|
843
|
+
options.onNestedToolFinished?.({
|
|
844
|
+
parentToolCallId: toolCallIdPrefix,
|
|
845
|
+
toolCallId: telemetryToolCallId,
|
|
846
|
+
toolName: candidate.tool.name,
|
|
847
|
+
outcome: "completed",
|
|
848
|
+
durationMs: occurredAt - startedAt,
|
|
849
|
+
output,
|
|
850
|
+
occurredAt,
|
|
851
|
+
});
|
|
852
|
+
return output;
|
|
853
|
+
} catch (error) {
|
|
854
|
+
const occurredAt = Date.now();
|
|
855
|
+
options.onNestedToolFinished?.({
|
|
856
|
+
parentToolCallId: toolCallIdPrefix,
|
|
857
|
+
toolCallId: telemetryToolCallId,
|
|
858
|
+
toolName: candidate.tool.name,
|
|
859
|
+
outcome: signal?.aborted ? "cancelled" : "failed",
|
|
860
|
+
durationMs: occurredAt - startedAt,
|
|
861
|
+
error,
|
|
862
|
+
occurredAt,
|
|
863
|
+
});
|
|
864
|
+
throw error;
|
|
797
865
|
}
|
|
798
|
-
const execute = options.toolExecutors?.[candidate.tool.name] ??
|
|
799
|
-
candidate.tool.execute;
|
|
800
|
-
return execute(toolCallId, input, signal, onUpdate);
|
|
801
866
|
},
|
|
802
867
|
},
|
|
803
868
|
});
|
|
804
|
-
this.candidates = state.candidates.map(
|
|
869
|
+
this.candidates = state.candidates.map((candidate) => {
|
|
870
|
+
if (candidate.tool.name !== "execute") return bindCandidate(candidate);
|
|
871
|
+
const factory = state.snapshot.bindings.codeExecution;
|
|
872
|
+
if (!factory) {
|
|
873
|
+
throw new Error("Prepared Code Mode Tool requires a Runtime factory");
|
|
874
|
+
}
|
|
875
|
+
return bindCandidate({
|
|
876
|
+
...candidate,
|
|
877
|
+
tool: {
|
|
878
|
+
...candidate.tool,
|
|
879
|
+
execute: (toolCallId, input, signal, onUpdate) => {
|
|
880
|
+
const runtimeCandidate = codeExecutionPiToolCandidate(
|
|
881
|
+
factory.create(state.codeExecutionCandidates.map((inner) =>
|
|
882
|
+
// Code Mode owns replay of its connector calls. Pi persists
|
|
883
|
+
// only the parent execute attempt/result; recording an inner
|
|
884
|
+
// input without a Pi settlement creates an orphan recovery
|
|
885
|
+
// action that cannot be found on the direct Tool surface.
|
|
886
|
+
bindCandidate(inner, toolCallId, false)
|
|
887
|
+
)),
|
|
888
|
+
);
|
|
889
|
+
return runtimeCandidate.tool.execute(
|
|
890
|
+
toolCallId,
|
|
891
|
+
input,
|
|
892
|
+
signal,
|
|
893
|
+
onUpdate,
|
|
894
|
+
);
|
|
895
|
+
},
|
|
896
|
+
},
|
|
897
|
+
});
|
|
898
|
+
});
|
|
805
899
|
this.turn = new PiTurnAdapter({
|
|
806
900
|
pi: {
|
|
807
901
|
model: state.snapshot.pi.model,
|
package/src/pi/tool/base.ts
CHANGED
|
@@ -183,13 +183,31 @@ export function normalizeUpdatePlanArguments(
|
|
|
183
183
|
return input as UpdatePlanArguments;
|
|
184
184
|
}
|
|
185
185
|
const value = input as Record<string, unknown>;
|
|
186
|
-
|
|
186
|
+
let steps = value.steps;
|
|
187
|
+
if (typeof steps === "string") {
|
|
188
|
+
try {
|
|
189
|
+
steps = JSON.parse(steps);
|
|
190
|
+
} catch {
|
|
191
|
+
return input as UpdatePlanArguments;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (!Array.isArray(steps)) return input as UpdatePlanArguments;
|
|
187
195
|
return {
|
|
188
196
|
...value,
|
|
189
|
-
steps:
|
|
197
|
+
steps: steps.map((step) => {
|
|
190
198
|
if (step === null || typeof step !== "object") return step;
|
|
191
199
|
const s = step as Record<string, unknown>;
|
|
192
|
-
|
|
200
|
+
const { step: alias, ...rest } = s;
|
|
201
|
+
const text = typeof rest.text === "string"
|
|
202
|
+
? rest.text
|
|
203
|
+
: typeof alias === "string"
|
|
204
|
+
? alias
|
|
205
|
+
: undefined;
|
|
206
|
+
return {
|
|
207
|
+
...rest,
|
|
208
|
+
...(text === undefined ? {} : { text }),
|
|
209
|
+
status: normalizeStepStatus(s.status),
|
|
210
|
+
};
|
|
193
211
|
}),
|
|
194
212
|
} as UpdatePlanArguments;
|
|
195
213
|
}
|
|
@@ -278,7 +296,8 @@ export function basePiToolCandidates(
|
|
|
278
296
|
},
|
|
279
297
|
},
|
|
280
298
|
},
|
|
281
|
-
|
|
299
|
+
{
|
|
300
|
+
...candidate({
|
|
282
301
|
name: "suggest_followups",
|
|
283
302
|
label: "Suggest follow-ups",
|
|
284
303
|
description:
|
|
@@ -288,7 +307,10 @@ export function basePiToolCandidates(
|
|
|
288
307
|
return result({ noted: true, count: input.items.length });
|
|
289
308
|
},
|
|
290
309
|
}),
|
|
291
|
-
|
|
310
|
+
direct: true,
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
...candidate({
|
|
292
314
|
name: "update_plan",
|
|
293
315
|
label: "Update plan",
|
|
294
316
|
description:
|
|
@@ -303,6 +325,8 @@ export function basePiToolCandidates(
|
|
|
303
325
|
});
|
|
304
326
|
},
|
|
305
327
|
}),
|
|
328
|
+
direct: true,
|
|
329
|
+
},
|
|
306
330
|
...(webSearch ? [webSearchPiToolCandidate(webSearch)] : []),
|
|
307
331
|
];
|
|
308
332
|
}
|
package/src/pi/tool/compiler.ts
CHANGED
|
@@ -48,6 +48,10 @@ export interface PiToolCandidate {
|
|
|
48
48
|
readonly tool: AgentTool<any, any>;
|
|
49
49
|
/** @internal Send the complete schema but hide it until provider Tool Search finds it. */
|
|
50
50
|
readonly deferLoading?: true;
|
|
51
|
+
/** Keep this Tool Direct-only instead of also offering it through Code Mode. */
|
|
52
|
+
readonly direct?: true;
|
|
53
|
+
/** Offer this Tool only through Code Mode, never as a top-level Tool. */
|
|
54
|
+
readonly codeExecutionOnly?: true;
|
|
51
55
|
/** Conservative maximum used in the stable Runtime descriptor. */
|
|
52
56
|
readonly requiredExecutionLevel: ExecutionLevel;
|
|
53
57
|
/** Trusted parameter-level policy, evaluated before approval or dispatch. */
|
package/src/pi/tool/core-host.ts
CHANGED
|
@@ -1,16 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createWorkspaceStateBackend,
|
|
3
|
+
type WorkspaceFsLike,
|
|
4
|
+
} from "@cloudflare/shell";
|
|
1
5
|
import { createBrowserTools } from "@cloudflare/think/tools/browser";
|
|
6
|
+
import { createExecuteRuntime } from "@cloudflare/think/tools/execute";
|
|
2
7
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
8
|
+
import type { Usage } from "@earendil-works/pi-ai";
|
|
3
9
|
import type { ToolSet } from "ai";
|
|
4
10
|
import type {
|
|
5
11
|
RuntimeBrowserPort,
|
|
6
12
|
RuntimeCodeExecutionPort,
|
|
13
|
+
WorkspacePort,
|
|
7
14
|
} from "../../kernel/bindings";
|
|
15
|
+
import type { RuntimeCodeExecutionFactory } from "../../kernel/bindings";
|
|
8
16
|
import { serializeOutput } from "../../lib/artifacts";
|
|
17
|
+
import type { PiToolCandidate } from "./compiler";
|
|
18
|
+
import { piCandidatesToAiTools } from "./nested-tools";
|
|
9
19
|
|
|
10
|
-
// 本文件沿用 `../../index.ts` 入口定义的
|
|
20
|
+
// 本文件沿用 `../../index.ts` 入口定义的 Workspace、Port 和 Tool Candidate 术语。
|
|
11
21
|
|
|
12
22
|
const CODEMODE_SANDBOX_TIMEOUT_MS = 55_000;
|
|
13
23
|
|
|
24
|
+
function directoryEntry(name: string, label: string): string {
|
|
25
|
+
const singleLineLabel = label.replaceAll(/\s+/g, " ").trim().replaceAll("`", "'");
|
|
26
|
+
return `- \`${name}\` — ${singleLineLabel}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function codeExecutionDescription(
|
|
30
|
+
candidates: readonly PiToolCandidate[],
|
|
31
|
+
): string {
|
|
32
|
+
const list = (
|
|
33
|
+
entries: readonly { readonly name: string; readonly label: string }[],
|
|
34
|
+
) => entries.length > 0
|
|
35
|
+
? entries.map(({ name, label }) => directoryEntry(name, label)).join("\n")
|
|
36
|
+
: "- None.";
|
|
37
|
+
|
|
38
|
+
return [
|
|
39
|
+
"Execute plain JavaScript in a sandbox using the exact Tool directory below.",
|
|
40
|
+
"",
|
|
41
|
+
"## `tools.*` Available",
|
|
42
|
+
list(candidates.map(({ tool }) => ({
|
|
43
|
+
name: tool.name,
|
|
44
|
+
label: tool.label ?? tool.name,
|
|
45
|
+
}))),
|
|
46
|
+
"",
|
|
47
|
+
"Call only the methods listed above through `tools.*`; never guess or construct a method name.",
|
|
48
|
+
"Use `codemode.describe(\"tools.method\")` when you need the exact input type for a listed method.",
|
|
49
|
+
"`codemode.search` cannot add methods to `tools.*` or load top-level Tools; it searches only connector methods and snippets already installed in this Code Mode Runtime.",
|
|
50
|
+
"Use `state.*` for the Workspace filesystem. Every method takes one object argument, for example `state.readFile({ path })` and `state.writeFile({ path, content })`.",
|
|
51
|
+
"Wrap raw fetch, random values, time, and other nondeterministic work in `codemode.step(name, fn)` so replay runs them once.",
|
|
52
|
+
"Some connector methods pause for approval and resume automatically. Do not re-issue paused code.",
|
|
53
|
+
"Keep all code outside connector calls and `codemode.step` deterministic.",
|
|
54
|
+
"Raw `fetch` is available inside `codemode.step(...)`; prefer connector SDKs when one owns the target.",
|
|
55
|
+
"There is no Node.js `require`, `process`, package manager, or Python runtime.",
|
|
56
|
+
].join("\n");
|
|
57
|
+
}
|
|
58
|
+
|
|
14
59
|
/**
|
|
15
60
|
* 宿主的 Browser Rendering 绑定,只取本仓真正用到的那一面。
|
|
16
61
|
*
|
|
@@ -21,6 +66,13 @@ export interface RuntimeBrowserBinding {
|
|
|
21
66
|
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
22
67
|
}
|
|
23
68
|
|
|
69
|
+
/**
|
|
70
|
+
* 把宿主的 Worker Loader、出站网络和 Workspace 组装成代码执行 Port。
|
|
71
|
+
*
|
|
72
|
+
* Worker 宿主在具备 Durable Object state 和完整平台绑定时调用,然后把返回值交给 Runtime 工具组装。
|
|
73
|
+
*
|
|
74
|
+
* Think 的独立 execute factory 接受显式宿主参数,不要求 Agent 继承 Think;这里只借它组装 Codemode Runtime、Dynamic Worker executor 和已限定范围的 Workspace state connector。
|
|
75
|
+
*/
|
|
24
76
|
function result(details: unknown): AgentToolResult<unknown> {
|
|
25
77
|
return {
|
|
26
78
|
content: [{ type: "text", text: serializeOutput(details).text }],
|
|
@@ -28,6 +80,56 @@ function result(details: unknown): AgentToolResult<unknown> {
|
|
|
28
80
|
};
|
|
29
81
|
}
|
|
30
82
|
|
|
83
|
+
function sumUsage(results: readonly AgentToolResult<unknown>[]): Usage | undefined {
|
|
84
|
+
const usages = results.flatMap(({ usage }) => usage ? [usage] : []);
|
|
85
|
+
if (usages.length === 0) return undefined;
|
|
86
|
+
return usages.reduce<Usage>((total, usage) => ({
|
|
87
|
+
input: total.input + usage.input,
|
|
88
|
+
output: total.output + usage.output,
|
|
89
|
+
cacheRead: total.cacheRead + usage.cacheRead,
|
|
90
|
+
cacheWrite: total.cacheWrite + usage.cacheWrite,
|
|
91
|
+
...(total.cacheWrite1h === undefined && usage.cacheWrite1h === undefined
|
|
92
|
+
? {}
|
|
93
|
+
: { cacheWrite1h: (total.cacheWrite1h ?? 0) + (usage.cacheWrite1h ?? 0) }),
|
|
94
|
+
...(total.reasoning === undefined && usage.reasoning === undefined
|
|
95
|
+
? {}
|
|
96
|
+
: { reasoning: (total.reasoning ?? 0) + (usage.reasoning ?? 0) }),
|
|
97
|
+
totalTokens: total.totalTokens + usage.totalTokens,
|
|
98
|
+
cost: {
|
|
99
|
+
input: total.cost.input + usage.cost.input,
|
|
100
|
+
output: total.cost.output + usage.cost.output,
|
|
101
|
+
cacheRead: total.cost.cacheRead + usage.cost.cacheRead,
|
|
102
|
+
cacheWrite: total.cost.cacheWrite + usage.cost.cacheWrite,
|
|
103
|
+
total: total.cost.total + usage.cost.total,
|
|
104
|
+
},
|
|
105
|
+
}), {
|
|
106
|
+
input: 0,
|
|
107
|
+
output: 0,
|
|
108
|
+
cacheRead: 0,
|
|
109
|
+
cacheWrite: 0,
|
|
110
|
+
totalTokens: 0,
|
|
111
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function codeExecutionResult(
|
|
116
|
+
details: unknown,
|
|
117
|
+
innerResults: readonly AgentToolResult<unknown>[],
|
|
118
|
+
): AgentToolResult<unknown> {
|
|
119
|
+
const addedToolNames = [...new Set(
|
|
120
|
+
innerResults.flatMap(({ addedToolNames }) => addedToolNames ?? []),
|
|
121
|
+
)].sort();
|
|
122
|
+
const usage = sumUsage(innerResults);
|
|
123
|
+
return {
|
|
124
|
+
...result(details),
|
|
125
|
+
...(addedToolNames.length > 0 ? { addedToolNames } : {}),
|
|
126
|
+
...(usage ? { usage } : {}),
|
|
127
|
+
...(innerResults.length > 0 && innerResults.every(({ terminate }) => terminate === true)
|
|
128
|
+
? { terminate: true }
|
|
129
|
+
: {}),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
31
133
|
// 上游把 Code Mode 类工具交付为 AI SDK tool;本仓只取 description 和 execute 两件,
|
|
32
134
|
// 并在这里就地校验,使装配期缺件立刻失败,而不是等模型调用时才炸。
|
|
33
135
|
function toCodeExecutionPort(
|
|
@@ -93,6 +195,7 @@ function browserToolDescription(): string {
|
|
|
93
195
|
*
|
|
94
196
|
* Worker 宿主在具备 DO state 与 Browser 绑定时调用,返回值经 Platform Port 交给 Runtime 工具组装。
|
|
95
197
|
*
|
|
198
|
+
* 形状与 `createWorkspaceCodeExecutionFactory` 同构:宿主提供 DO state 与平台绑定,Runtime 只拿到一个可选装配输入。
|
|
96
199
|
* `create()` 延迟到 Tool Surface 真的要注册时才调,被 deny 的装配不会白建连接器。
|
|
97
200
|
*/
|
|
98
201
|
export function createBrowserExecutionFactory(options: {
|
|
@@ -137,3 +240,42 @@ export function createBrowserExecutionFactory(options: {
|
|
|
137
240
|
},
|
|
138
241
|
};
|
|
139
242
|
}
|
|
243
|
+
|
|
244
|
+
export function createWorkspaceCodeExecutionFactory(options: {
|
|
245
|
+
readonly ctx: DurableObjectState;
|
|
246
|
+
readonly loader: WorkerLoader;
|
|
247
|
+
readonly outbound: Fetcher;
|
|
248
|
+
readonly workspace: WorkspacePort;
|
|
249
|
+
}): RuntimeCodeExecutionFactory {
|
|
250
|
+
return {
|
|
251
|
+
create(candidates) {
|
|
252
|
+
const description = codeExecutionDescription(candidates);
|
|
253
|
+
return {
|
|
254
|
+
description,
|
|
255
|
+
async execute(input) {
|
|
256
|
+
const innerResults: AgentToolResult<unknown>[] = [];
|
|
257
|
+
const { tool } = createExecuteRuntime({
|
|
258
|
+
ctx: options.ctx,
|
|
259
|
+
loader: options.loader,
|
|
260
|
+
globalOutbound: options.outbound,
|
|
261
|
+
tools: piCandidatesToAiTools(candidates, {
|
|
262
|
+
onResult: (value) => innerResults.push(value),
|
|
263
|
+
}),
|
|
264
|
+
description,
|
|
265
|
+
// 先于外层 60s 截止结束,给 Runtime RPC 结算和 Worker 释放留出时间。
|
|
266
|
+
timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
|
|
267
|
+
state: createWorkspaceStateBackend(
|
|
268
|
+
options.workspace as unknown as WorkspaceFsLike,
|
|
269
|
+
),
|
|
270
|
+
name: "execute",
|
|
271
|
+
});
|
|
272
|
+
return toCodeExecutionPort(
|
|
273
|
+
tool,
|
|
274
|
+
"execute",
|
|
275
|
+
(details) => codeExecutionResult(details, innerResults),
|
|
276
|
+
).execute(input);
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
package/src/pi/tool/core.ts
CHANGED
|
@@ -202,7 +202,7 @@ export function listExtensionsPiToolCandidate(
|
|
|
202
202
|
|
|
203
203
|
// #endregion
|
|
204
204
|
|
|
205
|
-
// #region
|
|
205
|
+
// #region Code Mode
|
|
206
206
|
|
|
207
207
|
const executeParameters = Type.Object({
|
|
208
208
|
code: Type.String({
|
|
@@ -213,8 +213,9 @@ const executeParameters = Type.Object({
|
|
|
213
213
|
});
|
|
214
214
|
const CODEMODE_EXECUTE_TIMEOUT_MS = 60_000;
|
|
215
215
|
|
|
216
|
-
//
|
|
217
|
-
// `
|
|
216
|
+
// 把模型提供的代码交给 Codemode Runtime 执行,并在外层再压一道截止。
|
|
217
|
+
// 两个 Code Mode 类工具(`execute` 与 `browser_execute`)共用同一段时序,
|
|
218
|
+
// 避免两套心智模型;`label` 只用于超时文案,因为模型看到的名字由候选项决定。
|
|
218
219
|
function runCodemode(
|
|
219
220
|
runtime: RuntimeCodeExecutionPort,
|
|
220
221
|
label: string,
|
|
@@ -253,6 +254,36 @@ function runCodemode(
|
|
|
253
254
|
};
|
|
254
255
|
}
|
|
255
256
|
|
|
257
|
+
/**
|
|
258
|
+
* 把 Cloudflare Codemode Runtime handle 包装为 Pi 代码执行工具候选项。
|
|
259
|
+
*
|
|
260
|
+
* Tool Surface 收到 Host 已组装的 Code Execution Port 后调用,
|
|
261
|
+
* 模型再通过 `execute` 运行代码。
|
|
262
|
+
*
|
|
263
|
+
* Code Mode 作为一个完整的 safe 工具对外暴露,内部能力不再单独提权。
|
|
264
|
+
*/
|
|
265
|
+
export function codeExecutionPiToolCandidate(
|
|
266
|
+
runtime: RuntimeCodeExecutionPort,
|
|
267
|
+
): PiToolCandidate {
|
|
268
|
+
const tool: AgentTool<typeof executeParameters> = {
|
|
269
|
+
name: "execute",
|
|
270
|
+
label: "Execute JavaScript",
|
|
271
|
+
description: runtime.description,
|
|
272
|
+
parameters: executeParameters,
|
|
273
|
+
// Pi 工具循环在模型选择 `execute` 时调用,调用前允许 Turn 取消。
|
|
274
|
+
// 必须通过 Runtime handle 而不是直接调用 executor,因为 Cloudflare Codemode 把重放、审批和执行日志放在持久化 Runtime 层。
|
|
275
|
+
execute: runCodemode(runtime, "Code Mode execute"),
|
|
276
|
+
};
|
|
277
|
+
return {
|
|
278
|
+
owner: "core:codemode",
|
|
279
|
+
requiredExecutionLevel: "safe",
|
|
280
|
+
outputBudget: { kind: "structure" },
|
|
281
|
+
source: "codemode",
|
|
282
|
+
summary: "Run JavaScript with network and configured connector access",
|
|
283
|
+
tool,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
256
287
|
/** 模型可见的浏览器工具名;属外部契约,改名是破坏性变更。 */
|
|
257
288
|
export const BROWSER_EXECUTE_TOOL_NAME = "browser_execute";
|
|
258
289
|
|
package/src/pi/tool/declared.ts
CHANGED
|
@@ -49,6 +49,7 @@ export interface PiDeclaredToolPolicy<Reply = unknown> {
|
|
|
49
49
|
readonly modelName: string;
|
|
50
50
|
readonly requiredExecutionLevel: PiToolCandidate["requiredExecutionLevel"];
|
|
51
51
|
readonly requiredExecutionLevelForInput?: PiToolCandidate["requiredExecutionLevelForInput"];
|
|
52
|
+
readonly direct?: true;
|
|
52
53
|
readonly retry?: PiToolCandidate["retry"];
|
|
53
54
|
readonly source?: PiToolCandidate["source"];
|
|
54
55
|
readonly summary?: string;
|
|
@@ -115,6 +116,7 @@ export function createPiDeclaredToolCandidate<Reply = unknown>(
|
|
|
115
116
|
...(policy.requiredExecutionLevelForInput
|
|
116
117
|
? { requiredExecutionLevelForInput: policy.requiredExecutionLevelForInput }
|
|
117
118
|
: {}),
|
|
119
|
+
...(policy.direct ? { direct: policy.direct } : {}),
|
|
118
120
|
...(policy.retry ? { retry: policy.retry } : {}),
|
|
119
121
|
...(policy.source ? { source: policy.source } : {}),
|
|
120
122
|
};
|
package/src/pi/tool/schedule.ts
CHANGED
|
@@ -92,7 +92,7 @@ function candidate<T extends TSchema>(
|
|
|
92
92
|
options: Partial<
|
|
93
93
|
Pick<
|
|
94
94
|
PiToolCandidate,
|
|
95
|
-
"alwaysRequiresApproval" | "owner" | "requiredExecutionLevel" | "summary"
|
|
95
|
+
"alwaysRequiresApproval" | "direct" | "owner" | "requiredExecutionLevel" | "summary"
|
|
96
96
|
>
|
|
97
97
|
> = {},
|
|
98
98
|
): PiToolCandidate {
|
|
@@ -104,6 +104,7 @@ function candidate<T extends TSchema>(
|
|
|
104
104
|
...(options.alwaysRequiresApproval
|
|
105
105
|
? { alwaysRequiresApproval: true }
|
|
106
106
|
: {}),
|
|
107
|
+
...(options.direct ? { direct: true } : {}),
|
|
107
108
|
...(options.summary ? { summary: options.summary } : {}),
|
|
108
109
|
};
|
|
109
110
|
}
|
|
@@ -133,6 +134,7 @@ export function schedulePiToolCandidates(
|
|
|
133
134
|
},
|
|
134
135
|
{
|
|
135
136
|
alwaysRequiresApproval: true,
|
|
137
|
+
direct: true,
|
|
136
138
|
summary: "Create a scheduled task",
|
|
137
139
|
},
|
|
138
140
|
),
|