@springbrand/agent-runtime 0.3.0-alpha.25 → 0.3.0-alpha.27
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/adapter/cloudflare/agent.ts +35 -3
- package/src/adapter/cloudflare/database.ts +2 -2
- package/src/adapter/cloudflare/define-agent.ts +10 -6
- package/src/adapter/cloudflare/schema.ts +22 -0
- package/src/config/assembler.ts +4 -0
- package/src/config/harness-runtime.ts +6 -0
- package/src/config/ports.ts +10 -1
- package/src/config/prepared-runtime.ts +1 -0
- package/src/config/public-contracts.ts +7 -0
- package/src/conversation/state.ts +11 -2
- package/src/index.ts +8 -0
- package/src/tools/README.md +24 -0
- package/src/tools/adapters/pi-to-ai.ts +16 -2
- 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/src/tools/runtime/compiler.ts +8 -0
- package/src/tools/runtime/contracts.ts +56 -0
- package/src/tools/runtime/decision-gate.ts +56 -1
- package/src/tools/runtime/human-decisions.ts +68 -2
- package/src/tools/runtime/interaction/repository.ts +97 -8
- package/src/tools/runtime/prerequisite-contracts.ts +31 -0
- package/src/tools/runtime/prerequisite-gate.ts +150 -0
- package/src/tools/runtime/prerequisites.ts +108 -0
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.27",
|
|
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.3"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"agents": ">=0.28.1 <0.29.0",
|
|
@@ -73,6 +73,8 @@ import type {
|
|
|
73
73
|
} from "@earendil-works/pi-ai";
|
|
74
74
|
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
75
75
|
import { openCloudflareSession } from "../../session";
|
|
76
|
+
import type { ToolPrerequisiteResponse } from "../../tools/runtime/prerequisite-contracts";
|
|
77
|
+
import { matchesPrerequisite, parsePrerequisiteResponse, projectPrerequisites } from "../../tools/runtime/prerequisites";
|
|
76
78
|
import { ModelRecovery, openSessionHarness, prepareSubmissionOperation } from "../../harness";
|
|
77
79
|
import type { SessionHarness } from "../../harness";
|
|
78
80
|
import {
|
|
@@ -158,6 +160,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
158
160
|
|
|
159
161
|
initialState: RuntimeState = {
|
|
160
162
|
runtimeLoad: { status: "idle", available: false },
|
|
163
|
+
prerequisites: [],
|
|
161
164
|
};
|
|
162
165
|
|
|
163
166
|
protected abstract ensureRuntimeReady(): Promise<void>;
|
|
@@ -826,6 +829,10 @@ export abstract class AgentRuntimeKernel<
|
|
|
826
829
|
interactions: this.db.interactions,
|
|
827
830
|
transaction: (run) => this.db.transaction(run),
|
|
828
831
|
release: (reason) => runtime.requestHumanRelease(reason),
|
|
832
|
+
isSubmissionActive: () => {
|
|
833
|
+
const current = this.db.submissions.find(submissionId);
|
|
834
|
+
return Boolean(current && current.status === "running" && !current.abortReason && !current.unattendedFailure);
|
|
835
|
+
},
|
|
829
836
|
onChanged: async (kind, id) => {
|
|
830
837
|
await this.broadcastApprovals();
|
|
831
838
|
if (kind === "approval") {
|
|
@@ -1513,11 +1520,12 @@ export abstract class AgentRuntimeKernel<
|
|
|
1513
1520
|
};
|
|
1514
1521
|
}
|
|
1515
1522
|
|
|
1516
|
-
/**
|
|
1523
|
+
/** Answers settle a Tool result; full prerequisite decisions control pre-effect waits. */
|
|
1517
1524
|
async respondToolInteraction(
|
|
1518
|
-
toolCallId: string,
|
|
1519
|
-
response
|
|
1525
|
+
toolCallId: string | ToolPrerequisiteResponse,
|
|
1526
|
+
response?: unknown,
|
|
1520
1527
|
): Promise<{ ok: boolean }> {
|
|
1528
|
+
if (typeof toolCallId !== "string") return this.respondPrerequisite(toolCallId);
|
|
1521
1529
|
const pending = this.db.interactions.findPendingByToolCallId(toolCallId);
|
|
1522
1530
|
if (!pending) return { ok: false };
|
|
1523
1531
|
const submission = this.db.submissions.find(pending.submissionId);
|
|
@@ -1552,6 +1560,29 @@ export abstract class AgentRuntimeKernel<
|
|
|
1552
1560
|
return { ok: true };
|
|
1553
1561
|
}
|
|
1554
1562
|
|
|
1563
|
+
private async respondPrerequisite(response: ToolPrerequisiteResponse): Promise<{ ok: boolean }> {
|
|
1564
|
+
const identity = parsePrerequisiteResponse(response);
|
|
1565
|
+
if (!identity) return { ok: false };
|
|
1566
|
+
const pending = this.db.interactions.find(identity.id);
|
|
1567
|
+
if (!matchesPrerequisite(pending, identity) || pending.status !== "pending") return { ok: false };
|
|
1568
|
+
const submission = this.db.submissions.findRunning();
|
|
1569
|
+
if (!submission || submission.submissionId !== pending.submissionId || submission.abortReason) return { ok: false };
|
|
1570
|
+
const active = this.submissions.currentActive();
|
|
1571
|
+
if (active && active.submissionId !== submission.submissionId) return { ok: false };
|
|
1572
|
+
// Decline is Config-independent, like an ordinary ask_user answer. It can win
|
|
1573
|
+
// while OAuth verification or the previous slice's parallel effects drain.
|
|
1574
|
+
if (identity.decision === "recheck" && !active) await this.ensureRuntimeVersion(submission.runtimeKey, true);
|
|
1575
|
+
const spec = identity.decision === "recheck"
|
|
1576
|
+
? this.pi.prerequisite(this.requireRuntime().pi, pending.toolName) : undefined;
|
|
1577
|
+
const result = await this.humanDecisions.respondPrerequisite(pending, spec, identity);
|
|
1578
|
+
if (!result.ok) return { ok: false };
|
|
1579
|
+
await this.broadcastApprovals();
|
|
1580
|
+
if (result.resume) this.ctx.waitUntil(
|
|
1581
|
+
this.submissions.continueAfterCurrent(submission.submissionId).then(() => undefined),
|
|
1582
|
+
);
|
|
1583
|
+
return { ok: true };
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1555
1586
|
/** Apply one decision; the Host must persist allow_level before resuming. */
|
|
1556
1587
|
async decideApproval(
|
|
1557
1588
|
executionId: string,
|
|
@@ -1619,6 +1650,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
1619
1650
|
try {
|
|
1620
1651
|
const { state, changed } = projectRuntimeState({
|
|
1621
1652
|
approvals: this.db.approvals.listPendingActive(),
|
|
1653
|
+
prerequisites: projectPrerequisites(this.db.interactions.listPendingActive()),
|
|
1622
1654
|
temporaryApprovals: this.temporaryAgentApprovals.list(),
|
|
1623
1655
|
running: this.db.submissions.findRunning(),
|
|
1624
1656
|
pending: this.db.submissions.listPending(),
|
|
@@ -21,8 +21,8 @@ import {
|
|
|
21
21
|
// 数据库术语以这里为准。
|
|
22
22
|
// Submission 是一次可持久化的用户提交,从排队到终态都用同一个 submissionId 跟踪。
|
|
23
23
|
// Approval 是某次工具调用的持久化审批记录,executionId 是对外决策时的主键。
|
|
24
|
-
// Tool Interaction
|
|
25
|
-
//
|
|
24
|
+
// Tool Interaction 保存客户端答案或原工具的前置条件等待,interactionId 是主键;
|
|
25
|
+
// 它保存人的决定和有界核验事实,Pi 保存工具执行位置和最终 ToolResult。
|
|
26
26
|
// Extension Context 是扩展按 label 保存的文本,不随清空聊天记录删除。
|
|
27
27
|
// Message UI 是会话消息的用户界面补充数据,它由 Transcript 而不是 Turn 生命周期管理。
|
|
28
28
|
// Agent Tool Run 是 Agents SDK 派发的子运行,唯一一张不归 Runtime 所有的表:SDK 建它、写它、清它,
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
TemporaryAgentApprovalDecision,
|
|
21
21
|
TemporaryAgentApprovalRequest,
|
|
22
22
|
TemporaryAgentLaunchSelection,
|
|
23
|
+
ToolPrerequisiteResponse,
|
|
23
24
|
} from "../../tools";
|
|
24
25
|
import { TEMPORARY_AGENT_LAUNCH_KEY } from "../../tools";
|
|
25
26
|
import {
|
|
@@ -215,14 +216,15 @@ export interface RuntimeAgentControls {
|
|
|
215
216
|
decision: ApprovalDecision,
|
|
216
217
|
): Promise<{ ok: boolean }>;
|
|
217
218
|
/**
|
|
218
|
-
*
|
|
219
|
+
* Settle an answer Tool, or control a prerequisite using its full wait identity.
|
|
219
220
|
*
|
|
220
221
|
* @remarks
|
|
221
|
-
*
|
|
222
|
+
* Legacy answers use (toolCallId, answer). Prerequisites use one response object;
|
|
223
|
+
* only the backend verifies readiness. Missing/stale/invalid decisions return false.
|
|
222
224
|
*/
|
|
223
225
|
respondToolInteraction(
|
|
224
|
-
toolCallId: string,
|
|
225
|
-
response
|
|
226
|
+
toolCallId: string | ToolPrerequisiteResponse,
|
|
227
|
+
response?: unknown,
|
|
226
228
|
): Promise<{ ok: boolean }>;
|
|
227
229
|
/**
|
|
228
230
|
* 登记一条受管临时 Agent 发起的审批请求。
|
|
@@ -327,6 +329,8 @@ export interface RuntimeAgentConfigControls<Command, Change> {
|
|
|
327
329
|
|
|
328
330
|
/** Type-only Host extension seam implemented by AgentRuntimeKernel. */
|
|
329
331
|
declare class RuntimeAgentHostSeam {
|
|
332
|
+
protected runConfigControl<T>(run: () => Promise<T>): Promise<T>;
|
|
333
|
+
protected ensureRuntimeVersion(runtimeKey: string, force?: boolean): Promise<void>;
|
|
330
334
|
protected prepareToolInteractionResponse(
|
|
331
335
|
input: import("./agent").ToolInteractionResponsePreparation,
|
|
332
336
|
): Promise<unknown | null>;
|
|
@@ -626,8 +630,8 @@ export function defineRuntimeAgent<
|
|
|
626
630
|
}
|
|
627
631
|
|
|
628
632
|
respondToolInteraction(
|
|
629
|
-
toolCallId: string,
|
|
630
|
-
response
|
|
633
|
+
toolCallId: string | ToolPrerequisiteResponse,
|
|
634
|
+
response?: unknown,
|
|
631
635
|
): Promise<{ ok: boolean }> {
|
|
632
636
|
return super.respondToolInteraction(toolCallId, response);
|
|
633
637
|
}
|
|
@@ -209,6 +209,28 @@ export function initializeSchema(sql: SqlTaggedTemplate): void {
|
|
|
209
209
|
responded_at INTEGER,
|
|
210
210
|
UNIQUE (submission_id, tool_call_id)
|
|
211
211
|
)`;
|
|
212
|
+
const interactionColumns = new Set(
|
|
213
|
+
sql<{ name: string }>`PRAGMA table_info(pi_tool_interactions)`.map((column) => column.name),
|
|
214
|
+
);
|
|
215
|
+
// Old client-settled answers keep their original meaning on a rolling upgrade.
|
|
216
|
+
if (!interactionColumns.has("purpose")) {
|
|
217
|
+
sql`ALTER TABLE pi_tool_interactions ADD COLUMN purpose TEXT NOT NULL DEFAULT 'answer'`;
|
|
218
|
+
}
|
|
219
|
+
if (!interactionColumns.has("scope_key")) {
|
|
220
|
+
sql`ALTER TABLE pi_tool_interactions ADD COLUMN scope_key TEXT`;
|
|
221
|
+
}
|
|
222
|
+
if (!interactionColumns.has("version")) {
|
|
223
|
+
sql`ALTER TABLE pi_tool_interactions ADD COLUMN version INTEGER NOT NULL DEFAULT 1`;
|
|
224
|
+
}
|
|
225
|
+
if (!interactionColumns.has("presentation_json")) {
|
|
226
|
+
sql`ALTER TABLE pi_tool_interactions ADD COLUMN presentation_json TEXT`;
|
|
227
|
+
}
|
|
228
|
+
if (!interactionColumns.has("verified_until")) {
|
|
229
|
+
sql`ALTER TABLE pi_tool_interactions ADD COLUMN verified_until INTEGER`;
|
|
230
|
+
}
|
|
231
|
+
sql`CREATE INDEX IF NOT EXISTS pi_tool_interactions_scope
|
|
232
|
+
ON pi_tool_interactions(submission_id, scope_key, status)
|
|
233
|
+
WHERE purpose = 'prerequisite'`;
|
|
212
234
|
sql`CREATE TABLE IF NOT EXISTS pi_extension_context (
|
|
213
235
|
label TEXT PRIMARY KEY,
|
|
214
236
|
content TEXT NOT NULL,
|
package/src/config/assembler.ts
CHANGED
|
@@ -256,6 +256,9 @@ async function createToolSurface(
|
|
|
256
256
|
if (!EXECUTION_LEVELS.includes(candidate.requiredExecutionLevel)) {
|
|
257
257
|
throw new Error(`SpringBrand Tool ${name} execution level is invalid`);
|
|
258
258
|
}
|
|
259
|
+
if (candidate.prerequisite && (candidate.interaction || exposureOf(candidate) === "codemode")) {
|
|
260
|
+
throw new Error(`SpringBrand Tool ${name} prerequisites require a direct, non-answer Tool`);
|
|
261
|
+
}
|
|
259
262
|
if (candidate.unattendedExecution) {
|
|
260
263
|
requiredName(
|
|
261
264
|
candidate.unattendedExecution.capability,
|
|
@@ -293,6 +296,7 @@ async function createToolSurface(
|
|
|
293
296
|
(candidate) =>
|
|
294
297
|
exposureOf(candidate) !== "direct" &&
|
|
295
298
|
!candidate.interaction &&
|
|
299
|
+
!candidate.prerequisite &&
|
|
296
300
|
typeof candidate.tool.execute === "function",
|
|
297
301
|
);
|
|
298
302
|
const codeExecutionCandidates = Object.freeze([...mergeable]);
|
|
@@ -88,6 +88,12 @@ export class HarnessRuntimeConfig {
|
|
|
88
88
|
)?.interaction;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
prerequisite(prepared: PreparedPiRuntime, name: string) {
|
|
92
|
+
return readPreparedPiRuntime(prepared, this.owner).candidates.find(
|
|
93
|
+
(candidate) => candidate.tool.name === name,
|
|
94
|
+
)?.prerequisite;
|
|
95
|
+
}
|
|
96
|
+
|
|
91
97
|
/** Read all executable inputs from the current Prepared Runtime. */
|
|
92
98
|
async harnessInputs(
|
|
93
99
|
prepared: PreparedPiRuntime,
|
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
|
/**
|
|
@@ -197,6 +197,7 @@ function describeTools(candidates: readonly PiToolCandidate[]) {
|
|
|
197
197
|
parameters: candidate.tool.parameters,
|
|
198
198
|
requiredExecutionLevel: candidate.requiredExecutionLevel,
|
|
199
199
|
...(candidate.confirmation ? { confirmation: true } : {}),
|
|
200
|
+
...(candidate.prerequisite ? { prerequisite: true } : {}),
|
|
200
201
|
...(candidate.alwaysRequiresApproval
|
|
201
202
|
? { alwaysRequiresApproval: true }
|
|
202
203
|
: {}),
|
|
@@ -18,6 +18,13 @@ export * from "../conversation/canonical-context";
|
|
|
18
18
|
export * from "../submission/contracts";
|
|
19
19
|
export * from "../conversation/state";
|
|
20
20
|
export * from "../lib/execution-level";
|
|
21
|
+
export type {
|
|
22
|
+
ToolPrerequisiteDecision,
|
|
23
|
+
ToolPrerequisiteIdentity,
|
|
24
|
+
ToolPrerequisitePresentation,
|
|
25
|
+
ToolPrerequisiteRequest,
|
|
26
|
+
ToolPrerequisiteResponse,
|
|
27
|
+
} from "../tools/runtime/prerequisite-contracts";
|
|
21
28
|
export type { WorkspaceRevision, WorkspaceRevisionManifest } from "../tools/workspace/versioning";
|
|
22
29
|
export type {
|
|
23
30
|
RuntimeEventConfirmation,
|
|
@@ -2,6 +2,7 @@ import type { ApprovalReceipt } from "../submission/contracts";
|
|
|
2
2
|
import type { StoredSubmission } from "../submission/repository";
|
|
3
3
|
import type { PendingApprovalSummary } from "../tools/runtime/approval/repository";
|
|
4
4
|
import type { PiCanonicalUserInput } from "./message/contract";
|
|
5
|
+
import type { ToolPrerequisiteRequest } from "../tools/runtime/prerequisite-contracts";
|
|
5
6
|
|
|
6
7
|
export type RuntimeLoadPhase = "config" | "assembly" | "mcp" | "pi";
|
|
7
8
|
|
|
@@ -95,6 +96,8 @@ export interface RuntimeState {
|
|
|
95
96
|
/** 供 Host 列表持久化的粗粒度活动状态;Submission 与 Approval 仍是执行真相。 */
|
|
96
97
|
activity?: RuntimeActivityProjection;
|
|
97
98
|
approvals?: ApprovalReceipt[];
|
|
99
|
+
/** Durable pre-effect waits, separate from canonical messages and answer tools. */
|
|
100
|
+
prerequisites?: ToolPrerequisiteRequest[];
|
|
98
101
|
turn?: RuntimeTurnState;
|
|
99
102
|
/** 有值时普通输入被锁定,直到锁定的 Submission 进入终态。 */
|
|
100
103
|
inputLock?: RuntimeInputLock;
|
|
@@ -109,6 +112,7 @@ type SubmissionProjection = Pick<
|
|
|
109
112
|
/** Rebuild client-visible activity from current facts without retaining lifecycle state. */
|
|
110
113
|
export function projectRuntimeState(input: {
|
|
111
114
|
readonly approvals: readonly PendingApprovalSummary[];
|
|
115
|
+
readonly prerequisites?: readonly ToolPrerequisiteRequest[];
|
|
112
116
|
readonly temporaryApprovals: readonly ApprovalReceipt[];
|
|
113
117
|
readonly running: SubmissionProjection | null | undefined;
|
|
114
118
|
readonly pending: readonly SubmissionProjection[];
|
|
@@ -134,6 +138,9 @@ export function projectRuntimeState(input: {
|
|
|
134
138
|
...input.temporaryApprovals,
|
|
135
139
|
];
|
|
136
140
|
const current = input.running ?? input.pending[0];
|
|
141
|
+
const prerequisites = (input.prerequisites ?? []).filter(
|
|
142
|
+
(request) => request.submissionId === current?.submissionId,
|
|
143
|
+
);
|
|
137
144
|
const queued = (input.running ? input.pending : input.pending.slice(1)).map(
|
|
138
145
|
(submission, index): RuntimeQueuedSubmission => {
|
|
139
146
|
let preview = "Queued message";
|
|
@@ -166,7 +173,7 @@ export function projectRuntimeState(input: {
|
|
|
166
173
|
hasPendingSteer: Boolean(current && input.hasPendingSteer(current.submissionId)),
|
|
167
174
|
queued,
|
|
168
175
|
};
|
|
169
|
-
const activity: RuntimeActivity = approvals.length > 0 ||
|
|
176
|
+
const activity: RuntimeActivity = approvals.length > 0 || prerequisites.length > 0 ||
|
|
170
177
|
(current && input.hasPendingInteraction(current.submissionId))
|
|
171
178
|
? "needs-input"
|
|
172
179
|
: current || queued.length > 0 ? "working" : "idle";
|
|
@@ -183,13 +190,15 @@ export function projectRuntimeState(input: {
|
|
|
183
190
|
? { reason: "scheduled-run" }
|
|
184
191
|
: undefined;
|
|
185
192
|
const changed = input.previousState.approvals === undefined ||
|
|
193
|
+
input.previousState.prerequisites === undefined ||
|
|
186
194
|
JSON.stringify(input.previousState.approvals) !== JSON.stringify(approvals) ||
|
|
195
|
+
JSON.stringify(input.previousState.prerequisites) !== JSON.stringify(prerequisites) ||
|
|
187
196
|
JSON.stringify(input.previousState.turn) !== JSON.stringify(turn) ||
|
|
188
197
|
JSON.stringify(input.previousState.inputLock) !== JSON.stringify(inputLock) ||
|
|
189
198
|
nextActivity !== previousActivity;
|
|
190
199
|
return {
|
|
191
200
|
state: changed
|
|
192
|
-
? { ...input.previousState, activity: nextActivity, approvals, turn, inputLock }
|
|
201
|
+
? { ...input.previousState, activity: nextActivity, approvals, prerequisites, turn, inputLock }
|
|
193
202
|
: input.previousState,
|
|
194
203
|
changed,
|
|
195
204
|
};
|
package/src/index.ts
CHANGED
|
@@ -91,6 +91,14 @@ export type {
|
|
|
91
91
|
} from "./tools/subagent/types/contract";
|
|
92
92
|
export type {
|
|
93
93
|
PiToolCandidate,
|
|
94
|
+
PiToolPrerequisiteCheck,
|
|
95
|
+
PiToolPrerequisiteContext,
|
|
96
|
+
PiToolPrerequisiteSpec,
|
|
97
|
+
ToolPrerequisiteDecision,
|
|
98
|
+
ToolPrerequisiteIdentity,
|
|
99
|
+
ToolPrerequisitePresentation,
|
|
100
|
+
ToolPrerequisiteRequest,
|
|
101
|
+
ToolPrerequisiteResponse,
|
|
94
102
|
ToolExposureMode,
|
|
95
103
|
} from "./tools";
|
|
96
104
|
export { basePiToolCandidates } from "./tools";
|
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
|
|
|
@@ -13,12 +16,19 @@ export function piCandidatesToAiTools(
|
|
|
13
16
|
candidates: readonly PiToolCandidate[],
|
|
14
17
|
options: NestedToolOptions,
|
|
15
18
|
): ToolSet {
|
|
16
|
-
|
|
19
|
+
// Nested sandboxes have no pre-intent durable human wait. Never bypass a
|
|
20
|
+
// prerequisite by exposing its raw execute closure in Code Mode or Skill scripts.
|
|
21
|
+
return Object.fromEntries(candidates.filter((candidate) => !candidate.prerequisite).map((candidate) => [
|
|
17
22
|
candidate.tool.name,
|
|
18
23
|
aiTool({
|
|
19
24
|
description: candidate.tool.description,
|
|
20
25
|
inputSchema: jsonSchema(candidate.tool.parameters as never),
|
|
21
26
|
execute: async (input, call) => {
|
|
27
|
+
const callSignal = call?.abortSignal;
|
|
28
|
+
const signal = options.signal && callSignal && options.signal !== callSignal
|
|
29
|
+
? AbortSignal.any([options.signal, callSignal])
|
|
30
|
+
: options.signal ?? callSignal ?? new AbortController().signal;
|
|
31
|
+
signal.throwIfAborted();
|
|
22
32
|
const toolCallId = call?.toolCallId ??
|
|
23
33
|
options.fallbackToolCallId?.(candidate.tool.name) ??
|
|
24
34
|
candidate.tool.name;
|
|
@@ -29,11 +39,15 @@ export function piCandidatesToAiTools(
|
|
|
29
39
|
name: candidate.tool.name,
|
|
30
40
|
arguments: prepared as JsonObject,
|
|
31
41
|
});
|
|
42
|
+
signal.throwIfAborted();
|
|
32
43
|
const result = await candidate.tool.execute(
|
|
33
44
|
toolCallId,
|
|
34
45
|
args,
|
|
35
|
-
|
|
46
|
+
signal,
|
|
36
47
|
);
|
|
48
|
+
// A resolved Host operation is a fact even if cancellation happened
|
|
49
|
+
// while it ran. Let settlement and the connector's durable recordResult
|
|
50
|
+
// finish; the outer Code Mode boundary rejects model continuation.
|
|
37
51
|
options.onResult?.(result);
|
|
38
52
|
if (result.details !== undefined) return result.details;
|
|
39
53
|
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
|
+
}
|