@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
package/src/pi/tool/skill.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runner as createSkillScriptRunner,
|
|
3
3
|
SkillRegistry,
|
|
4
|
+
type SkillResource,
|
|
4
5
|
type SkillScriptRequest,
|
|
5
6
|
type SkillScriptRunner,
|
|
6
7
|
type SkillSource,
|
|
7
8
|
} from "agents/skills";
|
|
8
|
-
import { jsonSchema, type ToolSet } from "ai";
|
|
9
|
+
import { jsonSchema, tool, type ToolSet } from "ai";
|
|
10
|
+
import { z } from "zod";
|
|
9
11
|
import type {
|
|
10
12
|
RuntimeSkillScriptPolicy,
|
|
11
13
|
WorkspacePort,
|
|
12
14
|
} from "../../kernel/bindings";
|
|
13
15
|
import { aiToolToPi } from "./ai-adapter";
|
|
14
16
|
import type { PiToolCandidate } from "./compiler";
|
|
17
|
+
import { serializeWorkspaceMutation } from "./workspace-sandbox";
|
|
15
18
|
|
|
16
19
|
/** A configured Skill source and its script capabilities. */
|
|
17
20
|
export interface PiSkillBinding {
|
|
@@ -103,11 +106,72 @@ const SKILL_TOOL_LABELS: Readonly<Record<string, string>> = {
|
|
|
103
106
|
activate_skill: "Activate Skill",
|
|
104
107
|
read_skill_resource: "Read Skill resource",
|
|
105
108
|
run_skill_script: "Run Skill script",
|
|
109
|
+
materialize_skill_resource: "Materialize Skill resource",
|
|
106
110
|
};
|
|
107
111
|
|
|
108
112
|
const SKILL_ENTRY_READ_GUIDANCE =
|
|
109
113
|
"SKILL.md contains the Skill instructions; use activate_skill instead.";
|
|
110
114
|
|
|
115
|
+
function resourceBytes(resource: SkillResource): Uint8Array {
|
|
116
|
+
if ((resource.encoding ?? "text") === "text") {
|
|
117
|
+
return new TextEncoder().encode(resource.content);
|
|
118
|
+
}
|
|
119
|
+
const binary = atob(resource.content);
|
|
120
|
+
const bytes = new Uint8Array(binary.length);
|
|
121
|
+
for (let index = 0; index < binary.length; index++) {
|
|
122
|
+
bytes[index] = binary.charCodeAt(index);
|
|
123
|
+
}
|
|
124
|
+
return bytes;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function materializeSkillResourceTool(
|
|
128
|
+
bindings: readonly PiSkillBinding[],
|
|
129
|
+
workspace: WorkspacePort,
|
|
130
|
+
) {
|
|
131
|
+
const names = bindings.map(({ name }) => name) as [string, ...string[]];
|
|
132
|
+
const byName = new Map(bindings.map((binding) => [binding.name, binding]));
|
|
133
|
+
return tool({
|
|
134
|
+
description:
|
|
135
|
+
"Copy a complete bundled Skill resource directly into the Workspace without returning its contents to the model. " +
|
|
136
|
+
"Use this instead of read_skill_resource when a template, script, image, font, or other asset must become a Workspace file. " +
|
|
137
|
+
"The destination is created or overwritten, including parent directories.",
|
|
138
|
+
inputSchema: z.object({
|
|
139
|
+
name: z.enum(names).describe("Activated Skill name"),
|
|
140
|
+
path: z.string().min(1).describe("Bundled resource path listed by activate_skill"),
|
|
141
|
+
destination: z.string().min(1).describe("Absolute destination path in the Workspace"),
|
|
142
|
+
}),
|
|
143
|
+
execute: async ({ name, path, destination }, { abortSignal }) => {
|
|
144
|
+
abortSignal?.throwIfAborted();
|
|
145
|
+
const source = byName.get(name)?.source;
|
|
146
|
+
if (!source?.readResource) {
|
|
147
|
+
throw new Error(`Skill \"${name}\" has no readable resources.`);
|
|
148
|
+
}
|
|
149
|
+
const resource = await source.readResource(name, path);
|
|
150
|
+
if (!resource) throw new Error(`Resource not found: ${name}/${path}`);
|
|
151
|
+
abortSignal?.throwIfAborted();
|
|
152
|
+
const bytes = resourceBytes(resource);
|
|
153
|
+
|
|
154
|
+
await serializeWorkspaceMutation(workspace, destination, async () => {
|
|
155
|
+
abortSignal?.throwIfAborted();
|
|
156
|
+
const parent = destination.replace(/\/[^/]+$/, "");
|
|
157
|
+
if (parent && parent !== "/") {
|
|
158
|
+
await workspace.mkdir(parent, { recursive: true });
|
|
159
|
+
}
|
|
160
|
+
await workspace.writeFileBytes(destination, bytes, resource.mimeType);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
name,
|
|
165
|
+
path: resource.path,
|
|
166
|
+
destination,
|
|
167
|
+
bytesWritten: bytes.byteLength,
|
|
168
|
+
encoding: resource.encoding ?? "text",
|
|
169
|
+
...(resource.mimeType ? { mimeType: resource.mimeType } : {}),
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
111
175
|
/** Create Pi candidates from the official Agents SDK SkillRegistry tools. */
|
|
112
176
|
export async function skillPiToolCandidates(
|
|
113
177
|
bindings: readonly PiSkillBinding[],
|
|
@@ -121,7 +185,15 @@ export async function skillPiToolCandidates(
|
|
|
121
185
|
);
|
|
122
186
|
await registry.load();
|
|
123
187
|
|
|
124
|
-
|
|
188
|
+
const tools = registry.tools();
|
|
189
|
+
if (options.workspace) {
|
|
190
|
+
tools.materialize_skill_resource = materializeSkillResourceTool(
|
|
191
|
+
bindings,
|
|
192
|
+
options.workspace,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return Object.entries(tools).map(([name, tool]) => {
|
|
125
197
|
const adapted = aiToolToPi(name, tool, {
|
|
126
198
|
label: SKILL_TOOL_LABELS[name] ?? name,
|
|
127
199
|
...(name === "read_skill_resource"
|
|
@@ -154,7 +226,10 @@ export async function skillPiToolCandidates(
|
|
|
154
226
|
}
|
|
155
227
|
return {
|
|
156
228
|
owner: "runtime-skill",
|
|
157
|
-
requiredExecutionLevel:
|
|
229
|
+
requiredExecutionLevel:
|
|
230
|
+
name === "run_skill_script" || name === "materialize_skill_resource"
|
|
231
|
+
? "high"
|
|
232
|
+
: "safe",
|
|
158
233
|
tool: adapted,
|
|
159
234
|
};
|
|
160
235
|
});
|
package/src/pi/tool/subagent.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
RuntimeSubagentLifecyclePort,
|
|
5
|
+
RuntimeSubagentPort,
|
|
6
|
+
RuntimeSubagentRunResult,
|
|
7
|
+
RuntimeSubagentUsageEvent,
|
|
8
|
+
} from "../../kernel/bindings";
|
|
4
9
|
import { AGENT_TYPES } from "../../layers/orchestration/subagents/agent-types/registry";
|
|
5
10
|
import { serializeOutput } from "../../lib/artifacts";
|
|
11
|
+
import {
|
|
12
|
+
isZeroSubagentUsage,
|
|
13
|
+
subagentTerminalStatus,
|
|
14
|
+
} from "../../kernel/subagent-runtime";
|
|
6
15
|
import type { PiToolCandidate } from "./compiler";
|
|
7
16
|
import {
|
|
8
17
|
toolRegistryFromPiCandidates,
|
|
@@ -38,6 +47,61 @@ function failure(agentType: string, status: string, error?: string) {
|
|
|
38
47
|
};
|
|
39
48
|
}
|
|
40
49
|
|
|
50
|
+
async function runRegisteredSubagent(
|
|
51
|
+
lifecycle: RuntimeSubagentLifecyclePort,
|
|
52
|
+
subagentRunId: string,
|
|
53
|
+
run: () => Promise<RuntimeSubagentRunResult>,
|
|
54
|
+
): Promise<RuntimeSubagentRunResult> {
|
|
55
|
+
let result: RuntimeSubagentRunResult;
|
|
56
|
+
try {
|
|
57
|
+
result = await run();
|
|
58
|
+
} catch (error) {
|
|
59
|
+
await lifecycle.onTerminal({
|
|
60
|
+
subagentRunId,
|
|
61
|
+
status: "failed",
|
|
62
|
+
zeroUsage: true,
|
|
63
|
+
});
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
if (result.runId !== subagentRunId) {
|
|
67
|
+
await recordSubagentResult(lifecycle, {
|
|
68
|
+
...result,
|
|
69
|
+
runId: subagentRunId,
|
|
70
|
+
status: "error",
|
|
71
|
+
});
|
|
72
|
+
throw new Error("SubAgent run ID changed after durable registration");
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function recordSubagentResult(
|
|
78
|
+
lifecycle: RuntimeSubagentLifecyclePort,
|
|
79
|
+
run: RuntimeSubagentRunResult,
|
|
80
|
+
): Promise<void> {
|
|
81
|
+
const status = subagentTerminalStatus(run.status);
|
|
82
|
+
if (run.usage) {
|
|
83
|
+
const event: RuntimeSubagentUsageEvent = {
|
|
84
|
+
eventId: `${run.runId}:usage`,
|
|
85
|
+
submissionId: lifecycle.submissionId,
|
|
86
|
+
runId: run.runId,
|
|
87
|
+
subagentRunId: run.runId,
|
|
88
|
+
parentRunId: lifecycle.parentRunId,
|
|
89
|
+
...(lifecycle.accountId ? { accountId: lifecycle.accountId } : {}),
|
|
90
|
+
...(lifecycle.rateVersion !== undefined ? { rateVersion: lifecycle.rateVersion } : {}),
|
|
91
|
+
...(lifecycle.slotIdentity ? { slotIdentity: lifecycle.slotIdentity } : {}),
|
|
92
|
+
kind: "subagent",
|
|
93
|
+
status,
|
|
94
|
+
usage: run.usage,
|
|
95
|
+
};
|
|
96
|
+
await lifecycle.onUsage(event);
|
|
97
|
+
}
|
|
98
|
+
await lifecycle.onTerminal({
|
|
99
|
+
subagentRunId: run.runId,
|
|
100
|
+
status,
|
|
101
|
+
zeroUsage: !run.usage || isZeroSubagentUsage(run.usage),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
41
105
|
/**
|
|
42
106
|
* 把已启用的 SubAgent 类型转换成 Pi 候选工具。
|
|
43
107
|
*
|
|
@@ -53,6 +117,7 @@ function failure(agentType: string, status: string, error?: string) {
|
|
|
53
117
|
export function subagentPiToolCandidates(
|
|
54
118
|
subagents: RuntimeSubagentPort | undefined,
|
|
55
119
|
enabledSubagents: readonly string[],
|
|
120
|
+
lifecycle?: RuntimeSubagentLifecyclePort,
|
|
56
121
|
): PiToolCandidate[] {
|
|
57
122
|
if (!subagents) return [];
|
|
58
123
|
const types = [...new Set(enabledSubagents)].flatMap((name) => {
|
|
@@ -79,10 +144,38 @@ export function subagentPiToolCandidates(
|
|
|
79
144
|
// 适配层只固定类型名并统一结果语义。
|
|
80
145
|
async execute(_toolCallId, input, signal) {
|
|
81
146
|
signal?.throwIfAborted();
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
147
|
+
const childInput = input as Record<string, unknown>;
|
|
148
|
+
if (lifecycle && !subagents.reserve) {
|
|
149
|
+
throw new Error("Durable SubAgent execution requires run reservation");
|
|
150
|
+
}
|
|
151
|
+
const reserved = lifecycle
|
|
152
|
+
? await subagents.reserve!(type.name, childInput, {
|
|
153
|
+
parentRunId: lifecycle.parentRunId,
|
|
154
|
+
submissionId: lifecycle.submissionId,
|
|
155
|
+
toolCallId: _toolCallId,
|
|
156
|
+
})
|
|
157
|
+
: undefined;
|
|
158
|
+
if (reserved && lifecycle) await lifecycle.onRegistered(reserved.runId);
|
|
159
|
+
const runOptions = lifecycle
|
|
160
|
+
? {
|
|
161
|
+
parentRunId: lifecycle.parentRunId,
|
|
162
|
+
submissionId: lifecycle.submissionId,
|
|
163
|
+
...(reserved ? { subagentRunId: reserved.runId } : {}),
|
|
164
|
+
...(lifecycle.accountId ? { accountId: lifecycle.accountId } : {}),
|
|
165
|
+
...(lifecycle.rateVersion !== undefined ? { rateVersion: lifecycle.rateVersion } : {}),
|
|
166
|
+
...(lifecycle.slotIdentity ? { slotIdentity: lifecycle.slotIdentity } : {}),
|
|
167
|
+
}
|
|
168
|
+
: undefined;
|
|
169
|
+
const run = lifecycle && reserved
|
|
170
|
+
? await runRegisteredSubagent(
|
|
171
|
+
lifecycle,
|
|
172
|
+
reserved.runId,
|
|
173
|
+
() => subagents.run(type.name, childInput, runOptions),
|
|
174
|
+
)
|
|
175
|
+
: await subagents.run(type.name, childInput);
|
|
176
|
+
if (lifecycle && !run.childStillRunning) {
|
|
177
|
+
await recordSubagentResult(lifecycle, run);
|
|
178
|
+
}
|
|
86
179
|
if (run.status !== "completed" || run.output === undefined) {
|
|
87
180
|
return result(failure(type.name, run.status, run.error));
|
|
88
181
|
}
|
|
@@ -112,7 +205,7 @@ export function subagentPiToolCandidates(
|
|
|
112
205
|
// 调用:Pi 需要子任务异步进行、不应阻塞当前 Turn 时调用。
|
|
113
206
|
// 原因:先用具体 AgentType Schema 复核嵌套 input,
|
|
114
207
|
// 再把 detached 和通知语义交给 Host,避免后台入口绕过类型约束。
|
|
115
|
-
async execute(
|
|
208
|
+
async execute(toolCallId, value, signal) {
|
|
116
209
|
signal?.throwIfAborted();
|
|
117
210
|
const input = value as z.infer<typeof parameters>;
|
|
118
211
|
const parsed = AGENT_TYPES.byName[input.agentType]!.inputSchema.safeParse(
|
|
@@ -123,15 +216,47 @@ export function subagentPiToolCandidates(
|
|
|
123
216
|
`SubAgent input failed schema validation: ${parsed.error.message}`,
|
|
124
217
|
);
|
|
125
218
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
219
|
+
if (lifecycle && !subagents.reserve) {
|
|
220
|
+
throw new Error("Durable SubAgent execution requires run reservation");
|
|
221
|
+
}
|
|
222
|
+
const reserved = lifecycle
|
|
223
|
+
? await subagents.reserve!(
|
|
224
|
+
input.agentType,
|
|
225
|
+
parsed.data as Record<string, unknown>,
|
|
226
|
+
{
|
|
227
|
+
parentRunId: lifecycle.parentRunId,
|
|
228
|
+
submissionId: lifecycle.submissionId,
|
|
229
|
+
toolCallId,
|
|
230
|
+
},
|
|
231
|
+
)
|
|
232
|
+
: undefined;
|
|
233
|
+
if (reserved && lifecycle) await lifecycle.onRegistered(reserved.runId);
|
|
234
|
+
const runInput = parsed.data as Record<string, unknown>;
|
|
235
|
+
const runOptions = {
|
|
236
|
+
detached: true,
|
|
237
|
+
maxBudgetMs: BACKGROUND_MAX_BUDGET_MS,
|
|
238
|
+
notifySource: "dispatch-background",
|
|
239
|
+
...(lifecycle
|
|
240
|
+
? {
|
|
241
|
+
parentRunId: lifecycle.parentRunId,
|
|
242
|
+
submissionId: lifecycle.submissionId,
|
|
243
|
+
...(reserved ? { subagentRunId: reserved.runId } : {}),
|
|
244
|
+
...(lifecycle.accountId ? { accountId: lifecycle.accountId } : {}),
|
|
245
|
+
...(lifecycle.rateVersion !== undefined ? { rateVersion: lifecycle.rateVersion } : {}),
|
|
246
|
+
...(lifecycle.slotIdentity ? { slotIdentity: lifecycle.slotIdentity } : {}),
|
|
247
|
+
}
|
|
248
|
+
: {}),
|
|
249
|
+
};
|
|
250
|
+
const run = lifecycle && reserved
|
|
251
|
+
? await runRegisteredSubagent(
|
|
252
|
+
lifecycle,
|
|
253
|
+
reserved.runId,
|
|
254
|
+
() => subagents.run(input.agentType, runInput, runOptions),
|
|
255
|
+
)
|
|
256
|
+
: await subagents.run(input.agentType, runInput, runOptions);
|
|
257
|
+
if (lifecycle && !run.childStillRunning && run.status !== "running") {
|
|
258
|
+
await recordSubagentResult(lifecycle, run);
|
|
259
|
+
}
|
|
135
260
|
return result({
|
|
136
261
|
runId: run.runId,
|
|
137
262
|
status: run.status,
|
|
@@ -153,8 +278,9 @@ export function subagentPiToolCandidates(
|
|
|
153
278
|
export function createSubagentTools(
|
|
154
279
|
subagents: RuntimeSubagentPort | undefined,
|
|
155
280
|
enabledSubagents: readonly string[],
|
|
281
|
+
lifecycle?: RuntimeSubagentLifecyclePort,
|
|
156
282
|
): ToolRegistry {
|
|
157
283
|
return toolRegistryFromPiCandidates(
|
|
158
|
-
subagentPiToolCandidates(subagents, enabledSubagents),
|
|
284
|
+
subagentPiToolCandidates(subagents, enabledSubagents, lifecycle),
|
|
159
285
|
);
|
|
160
286
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { WorkspaceRevisionRestorePort } from "../../workspace-versioning";
|
|
4
|
+
import { serializeOutput } from "../../lib/artifacts";
|
|
5
|
+
import {
|
|
6
|
+
toolRegistryFromPiCandidates,
|
|
7
|
+
type ToolRegistry,
|
|
8
|
+
} from "../../tool-registry";
|
|
9
|
+
import type { PiToolCandidate } from "./compiler";
|
|
10
|
+
|
|
11
|
+
const parameters = Type.Object({
|
|
12
|
+
target: Type.Union([
|
|
13
|
+
Type.Literal("previous"),
|
|
14
|
+
Type.String({
|
|
15
|
+
pattern: "^[0-9a-f]{40}$",
|
|
16
|
+
description: "A complete revision ID shown in Workspace history.",
|
|
17
|
+
}),
|
|
18
|
+
], {
|
|
19
|
+
description: "Use previous for the revision immediately before the current one.",
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function result<T>(details: T): AgentToolResult<T> {
|
|
24
|
+
return {
|
|
25
|
+
content: [{ type: "text", text: serializeOutput(details).text }],
|
|
26
|
+
details,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function workspaceRevisionPiToolCandidate(
|
|
31
|
+
versions: WorkspaceRevisionRestorePort,
|
|
32
|
+
): PiToolCandidate {
|
|
33
|
+
const tool: AgentTool<typeof parameters> = {
|
|
34
|
+
name: "restore_workspace_revision",
|
|
35
|
+
label: "Restore Workspace revision",
|
|
36
|
+
description:
|
|
37
|
+
"Restore the configured Workspace tree to the previous revision or an exact revision. Use only when the person explicitly asks to roll back. This always asks for approval and changes only the Workspace files managed by the Host.",
|
|
38
|
+
parameters,
|
|
39
|
+
async execute(_toolCallId, input, signal) {
|
|
40
|
+
signal?.throwIfAborted();
|
|
41
|
+
return result({
|
|
42
|
+
status: "restored",
|
|
43
|
+
...await versions.restore(input.target),
|
|
44
|
+
note: "The approved Workspace revision is now current.",
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
owner: "workspace",
|
|
50
|
+
tool,
|
|
51
|
+
requiredExecutionLevel: "safe",
|
|
52
|
+
alwaysRequiresApproval: true,
|
|
53
|
+
source: "action",
|
|
54
|
+
summary: "Restore the Workspace tree",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createWorkspaceRevisionTools(
|
|
59
|
+
versions: WorkspaceRevisionRestorePort,
|
|
60
|
+
): ToolRegistry {
|
|
61
|
+
return toolRegistryFromPiCandidates([
|
|
62
|
+
workspaceRevisionPiToolCandidate(versions),
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
@@ -111,7 +111,7 @@ const mutationQueues = new WeakMap<
|
|
|
111
111
|
Map<string, Promise<void>>
|
|
112
112
|
>();
|
|
113
113
|
|
|
114
|
-
function
|
|
114
|
+
export function serializeWorkspaceMutation<T>(
|
|
115
115
|
workspace: WorkspacePort,
|
|
116
116
|
path: string,
|
|
117
117
|
run: () => Promise<T>,
|
|
@@ -193,7 +193,7 @@ export function workspacePiToolCandidates(
|
|
|
193
193
|
signal?.throwIfAborted();
|
|
194
194
|
running(onUpdate, "Writing the Workspace file.");
|
|
195
195
|
|
|
196
|
-
return
|
|
196
|
+
return serializeWorkspaceMutation(workspace, path, async () => {
|
|
197
197
|
// 排队期间可能已经被取消,拿到闸之后必须重新确认,否则会写一个已放弃的结果。
|
|
198
198
|
signal?.throwIfAborted();
|
|
199
199
|
return writeTool.execute(
|
|
@@ -221,7 +221,7 @@ export function workspacePiToolCandidates(
|
|
|
221
221
|
running(onUpdate, "Editing the Workspace file.");
|
|
222
222
|
// 读-比对-写整段都在闸内:闸只包住最后那次 writeFile 的话,基准内容仍然可能在
|
|
223
223
|
// 比对之后被别的调用换掉,丢写照旧发生。
|
|
224
|
-
return
|
|
224
|
+
return serializeWorkspaceMutation(workspace, params.path, async () => {
|
|
225
225
|
signal?.throwIfAborted();
|
|
226
226
|
const output = await editTool.execute(
|
|
227
227
|
toolCallId,
|
|
@@ -267,7 +267,7 @@ export function workspacePiToolCandidates(
|
|
|
267
267
|
if (typeof path !== "string") {
|
|
268
268
|
return removeTool.execute(toolCallId, params, signal, onUpdate);
|
|
269
269
|
}
|
|
270
|
-
return
|
|
270
|
+
return serializeWorkspaceMutation(workspace, path, () =>
|
|
271
271
|
removeTool.execute(toolCallId, params, signal, onUpdate),
|
|
272
272
|
);
|
|
273
273
|
},
|
|
@@ -429,7 +429,7 @@ function assertIdentity(
|
|
|
429
429
|
state.assemblyRevision !== milestone.assemblyRevision)
|
|
430
430
|
) {
|
|
431
431
|
throw new Error(
|
|
432
|
-
"
|
|
432
|
+
"SpringBrand recovery milestone does not belong to the active Turn revision",
|
|
433
433
|
);
|
|
434
434
|
}
|
|
435
435
|
}
|
|
@@ -496,7 +496,7 @@ export function replayPiToolRecovery(
|
|
|
496
496
|
const previous = tools[milestone.toolCallId];
|
|
497
497
|
if (previous && !sameToolInput(previous, milestone)) {
|
|
498
498
|
throw new Error(
|
|
499
|
-
`Conflicting
|
|
499
|
+
`Conflicting SpringBrand Tool input for ${milestone.toolCallId}`,
|
|
500
500
|
);
|
|
501
501
|
}
|
|
502
502
|
tools[milestone.toolCallId] = previous ?? {
|
|
@@ -891,7 +891,7 @@ export function applyRecoveredPiApprovalDecision(
|
|
|
891
891
|
): RecoveredPiApprovalDecision {
|
|
892
892
|
const approval = state.approvals[input.executionId];
|
|
893
893
|
if (!approval) {
|
|
894
|
-
throw new Error(`Unknown
|
|
894
|
+
throw new Error(`Unknown SpringBrand Tool approval ${input.executionId}`);
|
|
895
895
|
}
|
|
896
896
|
const transition = decidePiToolApproval(
|
|
897
897
|
approval,
|
|
@@ -908,7 +908,7 @@ export function applyRecoveredPiApprovalDecision(
|
|
|
908
908
|
return { milestones: [], outcome: transition.outcome };
|
|
909
909
|
}
|
|
910
910
|
if (!state.turnId || !state.assemblyRevision) {
|
|
911
|
-
throw new Error("
|
|
911
|
+
throw new Error("SpringBrand Tool approval is missing its Turn revision");
|
|
912
912
|
}
|
|
913
913
|
|
|
914
914
|
const identity: RecoveryIdentity = {
|
|
@@ -951,7 +951,7 @@ export function recordRecoveredPiToolInteraction(
|
|
|
951
951
|
interaction: PiToolInteraction,
|
|
952
952
|
): PiToolRecoveryMilestone[] {
|
|
953
953
|
if (!state.turnId || !state.assemblyRevision) {
|
|
954
|
-
throw new Error("
|
|
954
|
+
throw new Error("SpringBrand Tool interaction is missing its Turn revision");
|
|
955
955
|
}
|
|
956
956
|
return [{
|
|
957
957
|
version: 1,
|
|
@@ -989,7 +989,7 @@ export function applyRecoveredPiToolInteractionSettlement(
|
|
|
989
989
|
): RecoveredPiToolInteractionSettlement {
|
|
990
990
|
const interaction = state.interactions[input.interactionId];
|
|
991
991
|
if (!interaction) {
|
|
992
|
-
throw new Error(`Unknown
|
|
992
|
+
throw new Error(`Unknown SpringBrand Tool interaction ${input.interactionId}`);
|
|
993
993
|
}
|
|
994
994
|
const transition = input.kind === "respond"
|
|
995
995
|
? respondPiToolInteraction(interaction, {
|
|
@@ -1008,7 +1008,7 @@ export function applyRecoveredPiToolInteractionSettlement(
|
|
|
1008
1008
|
return { milestones: [], outcome };
|
|
1009
1009
|
}
|
|
1010
1010
|
if (!state.turnId || !state.assemblyRevision) {
|
|
1011
|
-
throw new Error("
|
|
1011
|
+
throw new Error("SpringBrand Tool interaction is missing its Turn revision");
|
|
1012
1012
|
}
|
|
1013
1013
|
|
|
1014
1014
|
const identity: RecoveryIdentity = {
|
package/src/runtime-agent.ts
CHANGED
|
@@ -652,9 +652,14 @@ export function defineRuntimeAgent<
|
|
|
652
652
|
const policy = assembly.surfacePolicy;
|
|
653
653
|
return {
|
|
654
654
|
...assembly,
|
|
655
|
-
bindings:
|
|
656
|
-
|
|
657
|
-
|
|
655
|
+
bindings: {
|
|
656
|
+
...(assembly.bindings?.workspace
|
|
657
|
+
? { workspace: assembly.bindings.workspace }
|
|
658
|
+
: {}),
|
|
659
|
+
...(assembly.bindings?.codeExecution
|
|
660
|
+
? { codeExecution: assembly.bindings.codeExecution }
|
|
661
|
+
: {}),
|
|
662
|
+
},
|
|
658
663
|
memoryProfile: {
|
|
659
664
|
enabled: false,
|
|
660
665
|
memoryTokens: 2_000,
|
package/src/runtime-assembler.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { SkillSource } from "agents/skills";
|
|
2
2
|
import type {
|
|
3
|
-
RuntimeCodeExecutionPort,
|
|
4
3
|
RuntimeGatewayPort,
|
|
5
4
|
RuntimeMemoryPort,
|
|
6
5
|
RuntimePlatformPort,
|
|
@@ -44,7 +43,11 @@ import type {
|
|
|
44
43
|
RuntimeSkillContribution,
|
|
45
44
|
RuntimeToolSurfacePolicy,
|
|
46
45
|
} from "./runtime-definition";
|
|
47
|
-
import
|
|
46
|
+
import {
|
|
47
|
+
toolRegistryFromPiCandidates,
|
|
48
|
+
type RuntimeCodeExecutionFactory,
|
|
49
|
+
type ToolRegistry,
|
|
50
|
+
} from "./tool-registry";
|
|
48
51
|
import { withRuntimeLoadTimeout } from "./kernel/runtime-load";
|
|
49
52
|
import {
|
|
50
53
|
basePiToolCandidates,
|
|
@@ -52,9 +55,11 @@ import {
|
|
|
52
55
|
} from "./pi/tool/base";
|
|
53
56
|
import {
|
|
54
57
|
browserQuickActionPiToolCandidates,
|
|
58
|
+
codeExecutionPiToolCandidate,
|
|
55
59
|
} from "./pi/tool/core";
|
|
56
60
|
import { skillPiToolCandidates } from "./pi/tool/skill";
|
|
57
61
|
import { createWebSearch } from "./pi/tool/web-search";
|
|
62
|
+
import { subagentPiToolCandidates } from "./pi/tool/subagent";
|
|
58
63
|
import { resolvePiModel } from "./pi/runtime-adapter/models";
|
|
59
64
|
|
|
60
65
|
/** Runtime Assembler:校验一份扁平输入并生成可原子提交的 Snapshot。 */
|
|
@@ -76,7 +81,7 @@ export interface RuntimeAssemblyInput {
|
|
|
76
81
|
readonly provider: RuntimeProviderPort;
|
|
77
82
|
readonly platform: RuntimePlatformPort;
|
|
78
83
|
readonly workspace?: WorkspacePort;
|
|
79
|
-
readonly codeExecution?:
|
|
84
|
+
readonly codeExecution?: RuntimeCodeExecutionFactory;
|
|
80
85
|
readonly sandbox?: RuntimeSandboxPort;
|
|
81
86
|
readonly memory?: RuntimeMemoryPort;
|
|
82
87
|
readonly hostTools: readonly PiToolCandidate[];
|
|
@@ -149,6 +154,7 @@ function assertMemoryProfile(profile: RuntimeMemoryProfile): void {
|
|
|
149
154
|
}
|
|
150
155
|
|
|
151
156
|
interface ToolSurfaceInput {
|
|
157
|
+
readonly codeExecution?: RuntimeCodeExecutionFactory;
|
|
152
158
|
readonly platform: RuntimePlatformPort;
|
|
153
159
|
readonly workspace?: WorkspacePort;
|
|
154
160
|
readonly memory?: {
|
|
@@ -214,18 +220,46 @@ async function createToolSurface(
|
|
|
214
220
|
...staticCandidates,
|
|
215
221
|
...candidates,
|
|
216
222
|
]) {
|
|
217
|
-
const name = requiredName(candidate.tool.name, "
|
|
223
|
+
const name = requiredName(candidate.tool.name, "SpringBrand Tool");
|
|
218
224
|
if (!visible(candidate)) continue;
|
|
219
225
|
if (!EXECUTION_LEVELS.includes(candidate.requiredExecutionLevel)) {
|
|
220
|
-
throw new Error(`
|
|
226
|
+
throw new Error(`SpringBrand Tool ${name} execution level is invalid`);
|
|
221
227
|
}
|
|
222
228
|
if (tools.has(name)) {
|
|
223
|
-
throw new Error(`Duplicate Runtime
|
|
229
|
+
throw new Error(`Duplicate Runtime SpringBrand Tool: ${name}`);
|
|
224
230
|
}
|
|
225
231
|
tools.set(name, Object.freeze({ ...candidate }));
|
|
226
232
|
}
|
|
227
233
|
|
|
228
|
-
|
|
234
|
+
const finalized = [...tools.values()];
|
|
235
|
+
if (tools.has("execute")) {
|
|
236
|
+
throw new Error("SpringBrand reserved Runtime Tool name: execute");
|
|
237
|
+
}
|
|
238
|
+
if (
|
|
239
|
+
!input.codeExecution ||
|
|
240
|
+
deny.has("execute") ||
|
|
241
|
+
allowsTool?.("execute") === false
|
|
242
|
+
) {
|
|
243
|
+
return Object.freeze(finalized);
|
|
244
|
+
}
|
|
245
|
+
const mergeable = finalized.filter(
|
|
246
|
+
(candidate) =>
|
|
247
|
+
!candidate.interaction &&
|
|
248
|
+
typeof candidate.tool.execute === "function",
|
|
249
|
+
);
|
|
250
|
+
const direct = finalized.filter(
|
|
251
|
+
(candidate) =>
|
|
252
|
+
candidate.interaction ||
|
|
253
|
+
typeof candidate.tool.execute !== "function",
|
|
254
|
+
);
|
|
255
|
+
return Object.freeze([
|
|
256
|
+
Object.freeze(codeExecutionPiToolCandidate(
|
|
257
|
+
input.codeExecution.create(
|
|
258
|
+
toolRegistryFromPiCandidates(mergeable),
|
|
259
|
+
),
|
|
260
|
+
)),
|
|
261
|
+
...direct,
|
|
262
|
+
]);
|
|
229
263
|
},
|
|
230
264
|
}),
|
|
231
265
|
extensions: input.extensions.filter(
|
|
@@ -245,7 +279,7 @@ class RuntimeBuilder {
|
|
|
245
279
|
private platform?: RuntimePlatformPort;
|
|
246
280
|
private gateway?: RuntimeGatewayPort;
|
|
247
281
|
private workspace?: WorkspacePort;
|
|
248
|
-
private codeExecution?:
|
|
282
|
+
private codeExecution?: RuntimeCodeExecutionFactory;
|
|
249
283
|
private sandbox?: RuntimeSandboxPort;
|
|
250
284
|
private schedule?: RuntimeSchedulePort;
|
|
251
285
|
private memoryProfile: RuntimeMemoryProfile = DISABLED_MEMORY;
|
|
@@ -469,12 +503,19 @@ class RuntimeBuilder {
|
|
|
469
503
|
});
|
|
470
504
|
const skillSources = [...this.skillSources.values()];
|
|
471
505
|
const toolSurface = await createToolSurface({
|
|
506
|
+
...(this.codeExecution ? { codeExecution: this.codeExecution } : {}),
|
|
472
507
|
platform: this.platform,
|
|
473
508
|
...(this.workspace ? { workspace: this.workspace } : {}),
|
|
474
509
|
...(this.memoryProfile.enabled && this.memory
|
|
475
510
|
? { memory: { port: this.memory, profile: this.memoryProfile } }
|
|
476
511
|
: {}),
|
|
477
|
-
hostTools:
|
|
512
|
+
hostTools: [
|
|
513
|
+
...this.hostTools,
|
|
514
|
+
...subagentPiToolCandidates(
|
|
515
|
+
this.subagents,
|
|
516
|
+
[...this.enabledSubagents],
|
|
517
|
+
),
|
|
518
|
+
],
|
|
478
519
|
skills: skillSources,
|
|
479
520
|
enabledSubagents: [...this.enabledSubagents],
|
|
480
521
|
webSearch,
|
|
@@ -492,6 +533,7 @@ class RuntimeBuilder {
|
|
|
492
533
|
...(this.memoryProfile.enabled && this.memory
|
|
493
534
|
? { memory: this.memory }
|
|
494
535
|
: {}),
|
|
536
|
+
...(this.subagents ? { subagents: this.subagents } : {}),
|
|
495
537
|
skills: Object.freeze({
|
|
496
538
|
sources: Object.freeze(skillSources),
|
|
497
539
|
}),
|
|
@@ -632,6 +674,8 @@ function hooksToTurnEventsPort<
|
|
|
632
674
|
if (
|
|
633
675
|
!hooks.onTurnEnd &&
|
|
634
676
|
!hooks.onModelUsage &&
|
|
677
|
+
!hooks.onSubagentUsage &&
|
|
678
|
+
!hooks.onLifecycleFact &&
|
|
635
679
|
!hooks.onToolStart &&
|
|
636
680
|
!hooks.onToolSettled &&
|
|
637
681
|
!hooks.onApproval &&
|
|
@@ -645,6 +689,8 @@ function hooksToTurnEventsPort<
|
|
|
645
689
|
? (messages) => hooks.onTurnEnd!(ctx, messages)
|
|
646
690
|
: async () => undefined,
|
|
647
691
|
...(hooks.onModelUsage ? { onModelUsage: hooks.onModelUsage } : {}),
|
|
692
|
+
...(hooks.onSubagentUsage ? { onSubagentUsage: hooks.onSubagentUsage } : {}),
|
|
693
|
+
...(hooks.onLifecycleFact ? { onLifecycleFact: hooks.onLifecycleFact } : {}),
|
|
648
694
|
...(hooks.onToolStart ? { onToolStart: hooks.onToolStart } : {}),
|
|
649
695
|
...(hooks.onToolSettled ? { onToolSettled: hooks.onToolSettled } : {}),
|
|
650
696
|
...(hooks.onApproval ? { onApproval: hooks.onApproval } : {}),
|
|
@@ -754,9 +800,15 @@ export async function assembleRuntimeSnapshot<
|
|
|
754
800
|
...(toolAssembly.bindings?.workspace
|
|
755
801
|
? { workspace: toolAssembly.bindings.workspace }
|
|
756
802
|
: {}),
|
|
803
|
+
...(toolAssembly.bindings?.codeExecution
|
|
804
|
+
? { codeExecution: toolAssembly.bindings.codeExecution }
|
|
805
|
+
: {}),
|
|
757
806
|
...(memoryProfile.enabled && toolAssembly.bindings?.memory
|
|
758
807
|
? { memory: toolAssembly.bindings.memory }
|
|
759
808
|
: {}),
|
|
809
|
+
...(toolAssembly.bindings?.subagents
|
|
810
|
+
? { subagents: toolAssembly.bindings.subagents }
|
|
811
|
+
: {}),
|
|
760
812
|
hostTools,
|
|
761
813
|
skills: resources.skills,
|
|
762
814
|
connectors: resources.connectors.servers,
|