@springbrand/agent-runtime 0.1.3-alpha.8 → 0.2.0-alpha.14
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 +4 -3
- package/src/adapter/cloudflare/index.ts +1 -0
- package/src/adapter/cloudflare/sandbox/adapter.ts +4 -0
- package/src/adapter/cloudflare/subagent/definition.ts +60 -5
- package/src/adapter/cloudflare/universal-agent/hooks.ts +23 -1
- package/src/adapter/cloudflare/universal-agent/preparation.ts +36 -2
- package/src/adapter/cloudflare/universal-agent/tools.ts +9 -12
- package/src/adapter/cloudflare/workspace/git-fs.ts +178 -0
- package/src/adapter/cloudflare/workspace/version-control.ts +374 -0
- package/src/db/index.ts +4 -0
- package/src/db/runtime-event-outbox.repo.ts +34 -1
- package/src/db/schema.ts +42 -0
- package/src/db/submission-admission.repo.ts +127 -0
- package/src/db/submission.repo.ts +54 -3
- package/src/index.ts +5 -2
- package/src/kernel/bindings.ts +52 -0
- package/src/kernel/durable-lifecycle.ts +100 -0
- package/src/kernel/public-contracts.ts +11 -0
- package/src/kernel/receipts.ts +1 -0
- package/src/kernel/recoverable-chat-agent.ts +0 -19
- package/src/kernel/subagent-runtime.ts +137 -0
- package/src/kernel/submission-authority.ts +114 -0
- package/src/kernel/submission-lifecycle.ts +35 -10
- package/src/layers/context/budget/gate.ts +99 -0
- package/src/lib/prompt.ts +7 -3
- package/src/pi/message/projection.ts +2 -2
- package/src/pi/runtime-adapter/assembly.ts +4 -3
- package/src/pi/runtime-adapter/execution.ts +55 -16
- package/src/pi/runtime-adapter/index.ts +34 -0
- package/src/pi/runtime-adapter/models.ts +10 -1
- package/src/pi/runtime-adapter/recovery.ts +17 -17
- package/src/pi/runtime-adapter/transcript.ts +2 -2
- package/src/pi/tool/base.ts +113 -27
- package/src/pi/tool/compiler.ts +66 -6
- package/src/pi/tool/core-host.ts +50 -23
- package/src/pi/tool/core.ts +2 -23
- package/src/pi/tool/index.ts +1 -0
- package/src/pi/tool/mcp.ts +2 -2
- package/src/pi/tool/skill.ts +78 -3
- package/src/pi/tool/subagent.ts +142 -16
- package/src/pi/tool/workspace-revision.ts +64 -0
- package/src/pi/tool/workspace-sandbox.ts +4 -4
- package/src/pi/turn/tool-recovery.ts +7 -7
- package/src/runtime-agent-context.ts +1 -0
- package/src/runtime-agent.ts +8 -3
- package/src/runtime-assembler.ts +61 -9
- package/src/runtime-definition.ts +12 -0
- package/src/runtime.ts +485 -66
- package/src/tool-registry.ts +10 -1
- package/src/workspace-versioning.ts +46 -0
|
@@ -201,7 +201,7 @@ function milestoneMutation(
|
|
|
201
201
|
// 缺少身份时立即失败,避免把无归属的记录混入另一次 Turn 的恢复日志。
|
|
202
202
|
function recoveryIdentity(input: PiRecoveryInput) {
|
|
203
203
|
if (!input.identity) {
|
|
204
|
-
throw new Error("
|
|
204
|
+
throw new Error("SpringBrand recovery command is missing its Turn revision");
|
|
205
205
|
}
|
|
206
206
|
return { version: 1 as const, ...input.identity };
|
|
207
207
|
}
|
|
@@ -255,7 +255,7 @@ function interactionToolResult(
|
|
|
255
255
|
timestamp: number,
|
|
256
256
|
): ToolResultMessage {
|
|
257
257
|
if (!interaction) {
|
|
258
|
-
throw new Error("
|
|
258
|
+
throw new Error("SpringBrand Tool interaction is missing for its response");
|
|
259
259
|
}
|
|
260
260
|
return {
|
|
261
261
|
role: "toolResult",
|
|
@@ -417,7 +417,7 @@ export function decidePiRecovery(
|
|
|
417
417
|
(milestone) => milestone.type === "approval",
|
|
418
418
|
);
|
|
419
419
|
if (!approval || approval.type !== "approval") {
|
|
420
|
-
throw new Error("
|
|
420
|
+
throw new Error("SpringBrand approval decision milestone is missing");
|
|
421
421
|
}
|
|
422
422
|
mutations.push({
|
|
423
423
|
kind: "decide-approval",
|
|
@@ -453,7 +453,7 @@ export function decidePiRecovery(
|
|
|
453
453
|
JSON.stringify(existing.input) !== JSON.stringify(record.input)
|
|
454
454
|
) {
|
|
455
455
|
throw new Error(
|
|
456
|
-
`Conflicting
|
|
456
|
+
`Conflicting SpringBrand Tool input for ${record.toolCallId}`,
|
|
457
457
|
);
|
|
458
458
|
}
|
|
459
459
|
} else {
|
|
@@ -482,7 +482,7 @@ export function decidePiRecovery(
|
|
|
482
482
|
existing.inputJson !== approval.inputJson
|
|
483
483
|
) {
|
|
484
484
|
throw new Error(
|
|
485
|
-
`Conflicting
|
|
485
|
+
`Conflicting SpringBrand Tool approval: ${approval.executionId}`,
|
|
486
486
|
);
|
|
487
487
|
}
|
|
488
488
|
} else {
|
|
@@ -530,7 +530,7 @@ export function decidePiRecovery(
|
|
|
530
530
|
existing.inputJson !== interaction.inputJson
|
|
531
531
|
) {
|
|
532
532
|
throw new Error(
|
|
533
|
-
`Conflicting
|
|
533
|
+
`Conflicting SpringBrand Tool interaction: ${interaction.interactionId}`,
|
|
534
534
|
);
|
|
535
535
|
}
|
|
536
536
|
} else {
|
|
@@ -580,7 +580,7 @@ export function decidePiRecovery(
|
|
|
580
580
|
(milestone) => milestone.type === "interaction",
|
|
581
581
|
);
|
|
582
582
|
if (!record || record.type !== "interaction") {
|
|
583
|
-
throw new Error("
|
|
583
|
+
throw new Error("SpringBrand interaction settlement milestone is missing");
|
|
584
584
|
}
|
|
585
585
|
mutations.push({
|
|
586
586
|
kind: "settle-interaction",
|
|
@@ -621,7 +621,7 @@ export function decidePiRecovery(
|
|
|
621
621
|
if (existing) {
|
|
622
622
|
if (JSON.stringify(existing) !== JSON.stringify(toolResult)) {
|
|
623
623
|
throw new Error(
|
|
624
|
-
`Conflicting
|
|
624
|
+
`Conflicting SpringBrand Tool result for ${command.toolCallId}`,
|
|
625
625
|
);
|
|
626
626
|
}
|
|
627
627
|
} else {
|
|
@@ -676,7 +676,7 @@ export function decidePiRecovery(
|
|
|
676
676
|
plan.continuationKey !== input.command.continuationKey
|
|
677
677
|
) {
|
|
678
678
|
throw new Error(
|
|
679
|
-
`
|
|
679
|
+
`SpringBrand continuation is not ready: ${input.command.continuationKey}`,
|
|
680
680
|
);
|
|
681
681
|
}
|
|
682
682
|
const committed = commitPiRecoveryContinuation(
|
|
@@ -686,7 +686,7 @@ export function decidePiRecovery(
|
|
|
686
686
|
);
|
|
687
687
|
if (!committed) {
|
|
688
688
|
throw new Error(
|
|
689
|
-
`
|
|
689
|
+
`SpringBrand continuation could not be committed: ${plan.continuationKey}`,
|
|
690
690
|
);
|
|
691
691
|
}
|
|
692
692
|
const mutation = milestoneMutation(
|
|
@@ -809,13 +809,13 @@ export class PiRuntimeRecoveryAdapter
|
|
|
809
809
|
if (!durable) {
|
|
810
810
|
return {
|
|
811
811
|
kind: "fail",
|
|
812
|
-
reason: "Recovered
|
|
812
|
+
reason: "Recovered SpringBrand submission is missing",
|
|
813
813
|
} as const;
|
|
814
814
|
}
|
|
815
815
|
if (durable.terminal) {
|
|
816
816
|
return {
|
|
817
817
|
kind: "ignore",
|
|
818
|
-
reason: "
|
|
818
|
+
reason: "SpringBrand submission is already terminal",
|
|
819
819
|
} as const;
|
|
820
820
|
}
|
|
821
821
|
const decision = decidePiRecovery({
|
|
@@ -827,20 +827,20 @@ export class PiRuntimeRecoveryAdapter
|
|
|
827
827
|
if (decision.effect.reason === "approval") {
|
|
828
828
|
return {
|
|
829
829
|
kind: "park",
|
|
830
|
-
reason: "
|
|
830
|
+
reason: "SpringBrand Turn is waiting for Tool approval",
|
|
831
831
|
} as const;
|
|
832
832
|
}
|
|
833
833
|
if (decision.effect.reason === "interaction") {
|
|
834
834
|
return {
|
|
835
835
|
kind: "park",
|
|
836
|
-
reason: "
|
|
836
|
+
reason: "SpringBrand Turn is waiting for a client Tool interaction response",
|
|
837
837
|
} as const;
|
|
838
838
|
}
|
|
839
839
|
if (decision.effect.reason === "uncertain-tool") {
|
|
840
840
|
return {
|
|
841
841
|
kind: "park",
|
|
842
842
|
reason:
|
|
843
|
-
"
|
|
843
|
+
"SpringBrand Turn is parked on an uncertain non-idempotent Tool",
|
|
844
844
|
incidentStatus: "failed",
|
|
845
845
|
} as const;
|
|
846
846
|
}
|
|
@@ -848,12 +848,12 @@ export class PiRuntimeRecoveryAdapter
|
|
|
848
848
|
return {
|
|
849
849
|
kind: "fail",
|
|
850
850
|
reason:
|
|
851
|
-
"
|
|
851
|
+
"SpringBrand recovery is complete but the Submission is not terminal",
|
|
852
852
|
} as const;
|
|
853
853
|
}
|
|
854
854
|
return {
|
|
855
855
|
kind: "park",
|
|
856
|
-
reason: "
|
|
856
|
+
reason: "SpringBrand recovery is waiting for durable input",
|
|
857
857
|
} as const;
|
|
858
858
|
}
|
|
859
859
|
return {
|
|
@@ -526,7 +526,7 @@ export class PiRuntimeTranscript {
|
|
|
526
526
|
*/
|
|
527
527
|
importSnapshot(snapshot: PiCanonicalTranscriptSnapshot): void {
|
|
528
528
|
if (this.hasActiveTurn()) {
|
|
529
|
-
throw new Error("Cannot import messages while a
|
|
529
|
+
throw new Error("Cannot import messages while a SpringBrand Turn is active");
|
|
530
530
|
}
|
|
531
531
|
this.durability.transaction(() => {
|
|
532
532
|
let currentSubmissionId: string | undefined;
|
|
@@ -551,7 +551,7 @@ export class PiRuntimeTranscript {
|
|
|
551
551
|
: {}),
|
|
552
552
|
});
|
|
553
553
|
if (!appended) {
|
|
554
|
-
throw new Error(`
|
|
554
|
+
throw new Error(`SpringBrand Session message "${entry.id}" already exists`);
|
|
555
555
|
}
|
|
556
556
|
}
|
|
557
557
|
});
|
package/src/pi/tool/base.ts
CHANGED
|
@@ -15,47 +15,122 @@ import {
|
|
|
15
15
|
import { webSearchPiToolCandidate } from "./web-search";
|
|
16
16
|
import type { WebSearch } from "./web-search/api";
|
|
17
17
|
|
|
18
|
-
const
|
|
18
|
+
const askUserQuestion = Type.Object({
|
|
19
19
|
question: Type.String({
|
|
20
|
+
minLength: 1,
|
|
20
21
|
description: "The question to put to the user.",
|
|
21
22
|
}),
|
|
22
|
-
options: Type.Array(Type.String(), {
|
|
23
|
+
options: Type.Optional(Type.Array(Type.String({ minLength: 1 }), {
|
|
23
24
|
minItems: 2,
|
|
24
25
|
maxItems: 6,
|
|
26
|
+
uniqueItems: true,
|
|
25
27
|
description: "2-6 mutually distinct options for the user to choose from.",
|
|
26
|
-
}),
|
|
28
|
+
})),
|
|
27
29
|
multiSelect: Type.Optional(Type.Boolean({
|
|
28
30
|
description:
|
|
29
31
|
"Whether the user may select more than one option. Defaults to false.",
|
|
30
32
|
})),
|
|
33
|
+
allowCustom: Type.Optional(Type.Boolean({
|
|
34
|
+
description:
|
|
35
|
+
"Whether the user may add free text. Defaults to false with options and true without options.",
|
|
36
|
+
})),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const askUserParameters = Type.Object({
|
|
40
|
+
questions: Type.Array(askUserQuestion, {
|
|
41
|
+
minItems: 1,
|
|
42
|
+
maxItems: 4,
|
|
43
|
+
description:
|
|
44
|
+
"All user decisions needed to continue. Ask them together in one call.",
|
|
45
|
+
}),
|
|
31
46
|
});
|
|
32
47
|
|
|
33
48
|
/**
|
|
34
49
|
* `ask_user` 的客户端响应体。
|
|
35
50
|
*
|
|
36
|
-
* `
|
|
51
|
+
* `answers` 与 Tool 输入的 `questions` 按下标一一对应。
|
|
37
52
|
* 刻意用结构化数组而不是拼好的字符串 —— 选项本身可能含分隔符,拼了再拆是有损的。
|
|
38
53
|
*/
|
|
39
54
|
const askUserResponse = Type.Object({
|
|
40
|
-
|
|
41
|
-
|
|
55
|
+
answers: Type.Array(Type.Object({
|
|
56
|
+
selections: Type.Array(Type.String()),
|
|
57
|
+
text: Type.Optional(Type.String()),
|
|
58
|
+
})),
|
|
42
59
|
});
|
|
43
60
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
61
|
+
type AskUserInput = Static<typeof askUserParameters>;
|
|
62
|
+
type AskUserResponse = Static<typeof askUserResponse>;
|
|
63
|
+
|
|
64
|
+
function recordOf(value: unknown): Record<string, unknown> | null {
|
|
65
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
66
|
+
? value as Record<string, unknown>
|
|
67
|
+
: null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function questionsOf(value: unknown): AskUserInput["questions"] | null {
|
|
71
|
+
const questions = recordOf(value)?.questions;
|
|
72
|
+
if (!Array.isArray(questions) || questions.length < 1 || questions.length > 4) {
|
|
73
|
+
return null;
|
|
52
74
|
}
|
|
53
|
-
|
|
75
|
+
for (const value of questions) {
|
|
76
|
+
const question = recordOf(value);
|
|
77
|
+
if (!question || typeof question.question !== "string" || !question.question) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
if (
|
|
81
|
+
question.multiSelect !== undefined &&
|
|
82
|
+
typeof question.multiSelect !== "boolean"
|
|
83
|
+
) return null;
|
|
84
|
+
if (
|
|
85
|
+
question.allowCustom !== undefined &&
|
|
86
|
+
typeof question.allowCustom !== "boolean"
|
|
87
|
+
) return null;
|
|
88
|
+
if (question.options !== undefined) {
|
|
89
|
+
if (
|
|
90
|
+
!Array.isArray(question.options) ||
|
|
91
|
+
question.options.length < 2 ||
|
|
92
|
+
question.options.length > 6 ||
|
|
93
|
+
!question.options.every((option) =>
|
|
94
|
+
typeof option === "string" && option.length > 0
|
|
95
|
+
) ||
|
|
96
|
+
new Set(question.options).size !== question.options.length
|
|
97
|
+
) return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return questions as AskUserInput["questions"];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 手写校验而不是拉 TypeBox compiler:这里只需要判真假,
|
|
104
|
+
// 且校验位于 Worker 响应信任边界,不应为每次回答编译 schema。
|
|
105
|
+
function isAskUserResponse(input: unknown, value: unknown): boolean {
|
|
106
|
+
const questions = questionsOf(input);
|
|
107
|
+
const answers = recordOf(value)?.answers;
|
|
108
|
+
if (!questions || !Array.isArray(answers) || answers.length !== questions.length) {
|
|
54
109
|
return false;
|
|
55
110
|
}
|
|
56
|
-
|
|
57
|
-
return
|
|
58
|
-
|
|
111
|
+
|
|
112
|
+
return questions.every((question, index) => {
|
|
113
|
+
const answer = recordOf(answers[index]);
|
|
114
|
+
if (!answer || !Array.isArray(answer.selections)) return false;
|
|
115
|
+
if (!answer.selections.every((item) => typeof item === "string")) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
if (answer.text !== undefined && typeof answer.text !== "string") {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const options = question.options ?? [];
|
|
123
|
+
const text = typeof answer.text === "string" ? answer.text : "";
|
|
124
|
+
if (text.length > 2_000) return false;
|
|
125
|
+
if (answer.selections.some((selection) => !options.includes(selection))) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
if (new Set(answer.selections).size !== answer.selections.length) return false;
|
|
129
|
+
if (!question.multiSelect && answer.selections.length > 1) return false;
|
|
130
|
+
const allowCustom = options.length === 0 || question.allowCustom === true;
|
|
131
|
+
if (!allowCustom && text.trim()) return false;
|
|
132
|
+
return answer.selections.length > 0 || text.trim().length > 0;
|
|
133
|
+
});
|
|
59
134
|
}
|
|
60
135
|
|
|
61
136
|
const suggestFollowupsParameters = Type.Object({
|
|
@@ -162,7 +237,7 @@ export function basePiToolCandidates(
|
|
|
162
237
|
name: "ask_user",
|
|
163
238
|
label: "Ask user",
|
|
164
239
|
description:
|
|
165
|
-
"Ask
|
|
240
|
+
"Ask all 1-4 user questions needed to continue in one call. Each question may offer 2-6 options, allow multiple selections, and/or allow a custom answer. Do not make one call per question or write plain text like 'please pick A / B / C'. The answers return as this tool's result, so continue the same turn after calling it.",
|
|
166
241
|
parameters: askUserParameters,
|
|
167
242
|
// 永远不会被调用:执行链在 interaction 闸就 park 住了。留一个失败关闭的
|
|
168
243
|
// 实现,是为了万一哪次改动绕过了那道闸,能立刻炸出来而不是静默返回空答案。
|
|
@@ -174,18 +249,29 @@ export function basePiToolCandidates(
|
|
|
174
249
|
}),
|
|
175
250
|
interaction: {
|
|
176
251
|
validateResponse: isAskUserResponse,
|
|
177
|
-
settle: (
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
252
|
+
settle: (input, response) => {
|
|
253
|
+
const questions = (input as AskUserInput).questions;
|
|
254
|
+
const responseAnswers = (response as AskUserResponse).answers;
|
|
255
|
+
const answers = questions.map((question, index) => {
|
|
256
|
+
const answer = responseAnswers[index]!;
|
|
257
|
+
return {
|
|
258
|
+
question: question.question,
|
|
259
|
+
selections: answer.selections,
|
|
260
|
+
...(answer.text !== undefined ? { text: answer.text.trim() } : {}),
|
|
261
|
+
};
|
|
262
|
+
});
|
|
183
263
|
return {
|
|
184
264
|
content: [{
|
|
185
265
|
type: "text",
|
|
186
|
-
text: `The user answered
|
|
266
|
+
text: `The user answered:\n${answers.map((answer, index) => {
|
|
267
|
+
const parts = [
|
|
268
|
+
...answer.selections,
|
|
269
|
+
...(answer.text ? [answer.text] : []),
|
|
270
|
+
];
|
|
271
|
+
return `${index + 1}. ${answer.question}\n ${parts.join(" / ")}`;
|
|
272
|
+
}).join("\n")}`,
|
|
187
273
|
}],
|
|
188
|
-
details:
|
|
274
|
+
details: { answers },
|
|
189
275
|
};
|
|
190
276
|
},
|
|
191
277
|
},
|
package/src/pi/tool/compiler.ts
CHANGED
|
@@ -4,7 +4,16 @@ import type {
|
|
|
4
4
|
AgentToolResult,
|
|
5
5
|
} from "@earendil-works/pi-agent-core";
|
|
6
6
|
import type { ApprovalReceipt } from "../../kernel/receipts";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
boundDurableToolOutput,
|
|
9
|
+
type BudgetPolicy,
|
|
10
|
+
spillDurableToolOutput,
|
|
11
|
+
} from "../../layers/context/budget/gate";
|
|
12
|
+
import {
|
|
13
|
+
type ArtifactRef,
|
|
14
|
+
serializeOutput,
|
|
15
|
+
type SpillWorkspace,
|
|
16
|
+
} from "../../lib/artifacts";
|
|
8
17
|
import {
|
|
9
18
|
type ExecutionLevel,
|
|
10
19
|
} from "../../lib/execution-level";
|
|
@@ -24,8 +33,8 @@ import {
|
|
|
24
33
|
* 会让下一次模型调用非法。
|
|
25
34
|
*/
|
|
26
35
|
export interface PiToolInteractionSpec {
|
|
27
|
-
/**
|
|
28
|
-
readonly validateResponse: (response: unknown) => boolean;
|
|
36
|
+
/** 结合 Tool 输入校验客户端响应体;不通过则拒绝投递,park 保持不变。 */
|
|
37
|
+
readonly validateResponse: (input: unknown, response: unknown) => boolean;
|
|
29
38
|
/** 把通过校验的响应映射成 ToolResult;缺省是原样回传。 */
|
|
30
39
|
readonly settle?: (
|
|
31
40
|
input: unknown,
|
|
@@ -54,11 +63,14 @@ export interface PiToolCandidate {
|
|
|
54
63
|
readonly summary?: string;
|
|
55
64
|
readonly source?: ApprovalReceipt["source"];
|
|
56
65
|
readonly interaction?: PiToolInteractionSpec;
|
|
66
|
+
/** Large payloads spill by default; protocol-shaped results opt into structure. */
|
|
67
|
+
readonly outputBudget?: BudgetPolicy;
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
/** 控制候选 Pi 工具如何被编译为可执行工具集。 */
|
|
60
71
|
export interface CompilePiToolsOptions {
|
|
61
72
|
readonly governance?: PiToolGovernance;
|
|
73
|
+
readonly workspace?: SpillWorkspace;
|
|
62
74
|
readonly settle: (
|
|
63
75
|
call: SettledPiToolCall,
|
|
64
76
|
) => void | Promise<void>;
|
|
@@ -200,6 +212,52 @@ function emitToolTelemetry(
|
|
|
200
212
|
}
|
|
201
213
|
}
|
|
202
214
|
|
|
215
|
+
// Most Pi adapters keep the original Tool value in details and its serialized
|
|
216
|
+
// model form in one text block. Spill that original once instead of duplicating
|
|
217
|
+
// it inside an AgentToolResult JSON envelope.
|
|
218
|
+
function spillPayload(result: AgentToolResult<unknown>): unknown {
|
|
219
|
+
const text = result.content.length === 1 && result.content[0]?.type === "text"
|
|
220
|
+
? result.content[0].text
|
|
221
|
+
: undefined;
|
|
222
|
+
if (text !== undefined && serializeOutput(result.details).text === text) {
|
|
223
|
+
return result.details;
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function artifactResult(
|
|
229
|
+
result: AgentToolResult<unknown>,
|
|
230
|
+
ref: ArtifactRef,
|
|
231
|
+
): AgentToolResult<unknown> {
|
|
232
|
+
return {
|
|
233
|
+
...result,
|
|
234
|
+
content: [{
|
|
235
|
+
type: "text",
|
|
236
|
+
text: [
|
|
237
|
+
ref.note,
|
|
238
|
+
`Path: ${ref.path}`,
|
|
239
|
+
`Bytes: ${ref.bytes}`,
|
|
240
|
+
`Preview:\n${ref.preview}`,
|
|
241
|
+
].join("\n"),
|
|
242
|
+
}],
|
|
243
|
+
details: ref,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function durableResult(
|
|
248
|
+
result: AgentToolResult<unknown>,
|
|
249
|
+
candidate: PiToolCandidate,
|
|
250
|
+
workspace: SpillWorkspace | undefined,
|
|
251
|
+
): Promise<AgentToolResult<unknown>> {
|
|
252
|
+
if ((candidate.outputBudget ?? { kind: "spill" }).kind === "spill") {
|
|
253
|
+
const ref = await spillDurableToolOutput(spillPayload(result), {
|
|
254
|
+
workspace,
|
|
255
|
+
});
|
|
256
|
+
if (ref) return artifactResult(result, ref);
|
|
257
|
+
}
|
|
258
|
+
return boundDurableToolOutput(result) as AgentToolResult<unknown>;
|
|
259
|
+
}
|
|
260
|
+
|
|
203
261
|
// 记录一次工具失败并在 Turn 达到上限时打开断路器。
|
|
204
262
|
// 受治理 execute 方法在工具或结算失败后调用。
|
|
205
263
|
// 同时记录单输入次数和 Turn 总数,是为了既阻止原样重试又防止换参数无限失败。
|
|
@@ -341,9 +399,11 @@ function governedTool(
|
|
|
341
399
|
|
|
342
400
|
let result: AgentToolResult<unknown>;
|
|
343
401
|
try {
|
|
344
|
-
result =
|
|
402
|
+
result = await durableResult(
|
|
345
403
|
await execute(toolCallId, args, signal, onUpdate),
|
|
346
|
-
|
|
404
|
+
candidate,
|
|
405
|
+
options.workspace,
|
|
406
|
+
);
|
|
347
407
|
} catch (cause) {
|
|
348
408
|
const message = errorMessage(cause);
|
|
349
409
|
const result = boundDurableToolOutput({
|
|
@@ -442,7 +502,7 @@ export function compilePiTools(
|
|
|
442
502
|
options: CompilePiToolsOptions,
|
|
443
503
|
): AgentTool<any, any>[] {
|
|
444
504
|
if (!options?.settle) {
|
|
445
|
-
throw new Error("
|
|
505
|
+
throw new Error("SpringBrand Tool settlement port is required");
|
|
446
506
|
}
|
|
447
507
|
return candidates.map((candidate) => governedTool(candidate, options));
|
|
448
508
|
}
|
package/src/pi/tool/core-host.ts
CHANGED
|
@@ -3,10 +3,14 @@ import {
|
|
|
3
3
|
type WorkspaceFsLike,
|
|
4
4
|
} from "@cloudflare/shell";
|
|
5
5
|
import { createExecuteRuntime } from "@cloudflare/think/tools/execute";
|
|
6
|
+
import { jsonSchema, tool as aiTool, type ToolSet } from "ai";
|
|
6
7
|
import type {
|
|
7
|
-
RuntimeCodeExecutionPort,
|
|
8
8
|
WorkspacePort,
|
|
9
9
|
} from "../../kernel/bindings";
|
|
10
|
+
import type {
|
|
11
|
+
RuntimeCodeExecutionFactory,
|
|
12
|
+
ToolRegistry,
|
|
13
|
+
} from "../../tool-registry";
|
|
10
14
|
|
|
11
15
|
// 本文件沿用 `../../index.ts` 入口定义的 Workspace、Port 和 Tool Candidate 术语。
|
|
12
16
|
|
|
@@ -19,32 +23,55 @@ const CODEMODE_SANDBOX_TIMEOUT_MS = 55_000;
|
|
|
19
23
|
*
|
|
20
24
|
* Think 的独立 execute factory 接受显式宿主参数,不要求 Agent 继承 Think;这里只借它组装 Codemode Runtime、Dynamic Worker executor 和已限定范围的 Workspace state connector。
|
|
21
25
|
*/
|
|
22
|
-
|
|
26
|
+
function codeExecutionTools(tools: ToolRegistry): ToolSet {
|
|
27
|
+
return Object.fromEntries(Object.entries(tools).map(([name, spec]) => [
|
|
28
|
+
name,
|
|
29
|
+
aiTool({
|
|
30
|
+
description: spec.description,
|
|
31
|
+
inputSchema: jsonSchema(spec.parameters as never),
|
|
32
|
+
execute: (input, options) => spec.execute(input, {
|
|
33
|
+
toolCallId: options?.toolCallId ?? name,
|
|
34
|
+
signal: options?.abortSignal ?? new AbortController().signal,
|
|
35
|
+
}),
|
|
36
|
+
}),
|
|
37
|
+
]));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createWorkspaceCodeExecutionFactory(options: {
|
|
23
41
|
readonly ctx: DurableObjectState;
|
|
24
42
|
readonly loader: WorkerLoader;
|
|
25
43
|
readonly outbound: Fetcher;
|
|
26
44
|
readonly workspace: WorkspacePort;
|
|
27
|
-
}):
|
|
28
|
-
const { tool } = createExecuteRuntime({
|
|
29
|
-
ctx: options.ctx,
|
|
30
|
-
loader: options.loader,
|
|
31
|
-
globalOutbound: options.outbound,
|
|
32
|
-
// 先于外层 60s 截止结束,给 Runtime RPC 结算和 Worker 释放留出时间。
|
|
33
|
-
timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
|
|
34
|
-
state: createWorkspaceStateBackend(
|
|
35
|
-
options.workspace as unknown as WorkspaceFsLike,
|
|
36
|
-
),
|
|
37
|
-
name: "execute",
|
|
38
|
-
});
|
|
39
|
-
const execute = tool.execute;
|
|
40
|
-
if (typeof execute !== "function") {
|
|
41
|
-
throw new Error("Think createExecuteRuntime returned a non-executable tool");
|
|
42
|
-
}
|
|
45
|
+
}): RuntimeCodeExecutionFactory {
|
|
43
46
|
return {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
create(tools) {
|
|
48
|
+
const { tool } = createExecuteRuntime({
|
|
49
|
+
ctx: options.ctx,
|
|
50
|
+
loader: options.loader,
|
|
51
|
+
globalOutbound: options.outbound,
|
|
52
|
+
tools: codeExecutionTools(tools),
|
|
53
|
+
// 先于外层 60s 截止结束,给 Runtime RPC 结算和 Worker 释放留出时间。
|
|
54
|
+
timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
|
|
55
|
+
state: createWorkspaceStateBackend(
|
|
56
|
+
options.workspace as unknown as WorkspaceFsLike,
|
|
57
|
+
),
|
|
58
|
+
name: "execute",
|
|
59
|
+
});
|
|
60
|
+
const { description, execute } = tool;
|
|
61
|
+
if (typeof description !== "string" || description.length === 0) {
|
|
62
|
+
throw new Error("Think createExecuteRuntime returned a tool without a description");
|
|
63
|
+
}
|
|
64
|
+
if (typeof execute !== "function") {
|
|
65
|
+
throw new Error("Think createExecuteRuntime returned a non-executable tool");
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
description,
|
|
69
|
+
execute: (input) => Promise.resolve(execute(input, {
|
|
70
|
+
toolCallId: "execute",
|
|
71
|
+
messages: [],
|
|
72
|
+
context: undefined,
|
|
73
|
+
})),
|
|
74
|
+
};
|
|
75
|
+
},
|
|
49
76
|
};
|
|
50
77
|
}
|
package/src/pi/tool/core.ts
CHANGED
|
@@ -12,10 +12,6 @@ import { serializeOutput } from "../../lib/artifacts";
|
|
|
12
12
|
import type { PiLoadedExtension } from "../assembly/extensions";
|
|
13
13
|
import { aiToolToPi } from "./ai-adapter";
|
|
14
14
|
import type { PiToolCandidate } from "./compiler";
|
|
15
|
-
import {
|
|
16
|
-
toolRegistryFromPiCandidates,
|
|
17
|
-
type ToolRegistry,
|
|
18
|
-
} from "../../tool-registry";
|
|
19
15
|
|
|
20
16
|
// 本文件沿用 `../../index.ts` 入口定义的 Extension、Port 和 Tool Candidate 术语。
|
|
21
17
|
|
|
@@ -125,16 +121,7 @@ export function codeExecutionPiToolCandidate(
|
|
|
125
121
|
const tool: AgentTool<typeof executeParameters> = {
|
|
126
122
|
name: "execute",
|
|
127
123
|
label: "Execute JavaScript",
|
|
128
|
-
description:
|
|
129
|
-
"Run JavaScript in a short-lived Code Mode Dynamic Worker and return a value. This is your " +
|
|
130
|
-
"raw-network and tool-composition instrument, not a general web-search tool. Call fetch directly " +
|
|
131
|
-
"when you need a raw response, custom headers/method/body, a structured API, or content that " +
|
|
132
|
-
"web_search could not retrieve. Check res.status; when several known endpoints are necessary, " +
|
|
133
|
-
"fetch them in one invocation. Also available: " +
|
|
134
|
-
"state.* for your workspace filesystem (readFile/writeFile/glob/searchFiles/replaceInFiles, " +
|
|
135
|
-
"each taking one object argument), and codemode.step(name, fn) to run a side-effecting block " +
|
|
136
|
-
"exactly once so it survives replay. Write plain JavaScript — TypeScript type annotations are " +
|
|
137
|
-
"a syntax error. Use return for the result and console.log for notes.",
|
|
124
|
+
description: runtime.description,
|
|
138
125
|
parameters: executeParameters,
|
|
139
126
|
// 把模型提供的 JavaScript 交给 Codemode Runtime 执行。
|
|
140
127
|
// Pi 工具循环在模型选择 `execute` 时调用,调用前允许 Turn 取消。
|
|
@@ -173,19 +160,11 @@ export function codeExecutionPiToolCandidate(
|
|
|
173
160
|
return {
|
|
174
161
|
owner: "core:codemode",
|
|
175
162
|
requiredExecutionLevel: "high",
|
|
163
|
+
outputBudget: { kind: "structure" },
|
|
176
164
|
source: "codemode",
|
|
177
165
|
summary: "Run JavaScript with network and configured connector access",
|
|
178
166
|
tool,
|
|
179
167
|
};
|
|
180
168
|
}
|
|
181
169
|
|
|
182
|
-
/** 从 Codemode Runtime Port 生成 `execute` Tool。 */
|
|
183
|
-
export function createCodeExecutionTool(
|
|
184
|
-
runtime: RuntimeCodeExecutionPort,
|
|
185
|
-
): ToolRegistry {
|
|
186
|
-
return toolRegistryFromPiCandidates([
|
|
187
|
-
codeExecutionPiToolCandidate(runtime),
|
|
188
|
-
]);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
170
|
// #endregion
|
package/src/pi/tool/index.ts
CHANGED
package/src/pi/tool/mcp.ts
CHANGED
|
@@ -59,7 +59,8 @@ export interface PiMcpHost {
|
|
|
59
59
|
* 参数和中止信号必须原样交给 Agents SDK,避免另造一套传输生命周期。
|
|
60
60
|
*/
|
|
61
61
|
callTool(
|
|
62
|
-
|
|
62
|
+
params: Parameters<MCPClientManager["callTool"]>[0],
|
|
63
|
+
options?: { signal?: AbortSignal },
|
|
63
64
|
): Promise<unknown>;
|
|
64
65
|
};
|
|
65
66
|
}
|
|
@@ -333,7 +334,6 @@ export function createPiMcpToolCandidates(
|
|
|
333
334
|
name: mcpTool.name,
|
|
334
335
|
arguments: args,
|
|
335
336
|
},
|
|
336
|
-
undefined,
|
|
337
337
|
{ signal },
|
|
338
338
|
),
|
|
339
339
|
{
|