@springbrand/agent-runtime 0.1.3-alpha.4 → 0.1.3-alpha.6
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 +11 -3
- package/src/adapter/cloudflare/index.ts +55 -0
- package/src/adapter/cloudflare/resources/runtime-resources.ts +89 -0
- package/src/adapter/cloudflare/sandbox/adapter.ts +1509 -0
- package/src/adapter/cloudflare/sandbox/id.ts +23 -0
- package/src/adapter/cloudflare/sandbox/policy.ts +15 -0
- package/src/adapter/cloudflare/subagent/definition.ts +574 -0
- package/src/adapter/cloudflare/subagent/runner.ts +175 -0
- package/src/adapter/cloudflare/subagent/tools.ts +254 -0
- package/src/adapter/cloudflare/universal-agent/hooks.ts +35 -0
- package/src/adapter/cloudflare/universal-agent/preparation.ts +273 -0
- package/src/adapter/cloudflare/universal-agent/tools.ts +74 -0
- package/src/adapter/cloudflare/workspace/publisher.ts +31 -0
- package/src/adapter/cloudflare/workspace/scoped-workspace.ts +376 -0
- package/src/agent-tool-runtime.ts +152 -0
- package/src/index.ts +49 -7
- package/src/kernel/bindings.ts +6 -6
- package/src/kernel/recoverable-chat-agent.ts +12 -0
- package/src/kernel/runtime-load.ts +89 -0
- package/src/layers/orchestration/temporary-agent/core.ts +12 -1
- package/src/layers/orchestration/temporary-agent/runner.ts +1 -2
- package/src/lib/mcp.ts +7 -3
- package/src/pi/assembly/context.ts +3 -3
- package/src/pi/assembly/extensions.ts +11 -22
- package/src/pi/assembly/snapshot.ts +1 -1
- package/src/pi/message/contract.ts +7 -0
- package/src/pi/message/conversion.ts +9 -1
- package/src/pi/runtime-adapter/assembly.ts +4 -10
- package/src/pi/runtime-adapter/index.ts +6 -2
- package/src/pi/tool/base.ts +17 -2
- package/src/pi/tool/compiler.ts +0 -1
- package/src/pi/tool/core.ts +13 -3
- package/src/pi/tool/mcp.ts +3 -4
- package/src/pi/tool/schedule.ts +11 -1
- package/src/pi/tool/skill.ts +55 -41
- package/src/pi/tool/subagent.ts +14 -2
- package/src/pi/tool/web-fetch.ts +0 -1
- package/src/pi/tool/web-search/web-search.ts +0 -1
- package/src/pi/tool/workspace-sandbox.ts +15 -7
- package/src/runtime-agent-context.ts +112 -0
- package/src/runtime-agent.ts +442 -328
- package/src/runtime-assembler.ts +255 -103
- package/src/runtime-definition.ts +173 -0
- package/src/runtime.ts +185 -25
- package/src/tool-registry.ts +143 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Agent,
|
|
3
|
+
type AgentEvent,
|
|
4
|
+
type AgentTool,
|
|
5
|
+
type StreamFn,
|
|
6
|
+
} from "@earendil-works/pi-agent-core";
|
|
7
|
+
import type {
|
|
8
|
+
Api,
|
|
9
|
+
AssistantMessage,
|
|
10
|
+
Model,
|
|
11
|
+
} from "@earendil-works/pi-ai";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { assembleSubagentPrompt } from "../../../lib/prompt";
|
|
14
|
+
import type { AgentType } from "../../../layers/orchestration/subagents/agent-types/contract";
|
|
15
|
+
|
|
16
|
+
const RECURSIVE_TOOLS = new Set([
|
|
17
|
+
"run_agent",
|
|
18
|
+
"subagents",
|
|
19
|
+
"fanout",
|
|
20
|
+
"extract",
|
|
21
|
+
"dispatch_background",
|
|
22
|
+
]);
|
|
23
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
24
|
+
|
|
25
|
+
export interface CloudflareSubAgentRun {
|
|
26
|
+
type: AgentType;
|
|
27
|
+
input: unknown;
|
|
28
|
+
model: Model<Api>;
|
|
29
|
+
streamFn: StreamFn;
|
|
30
|
+
tools: AgentTool[];
|
|
31
|
+
getApiKey?: (provider: string) => string | undefined;
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
timeoutMs?: number;
|
|
34
|
+
onEvent?: (event: AgentEvent, signal: AbortSignal) => void | Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function textOf(message: AssistantMessage): string {
|
|
38
|
+
return message.content
|
|
39
|
+
.flatMap((part) => (part.type === "text" ? [part.text] : []))
|
|
40
|
+
.join("")
|
|
41
|
+
.trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function toolErrorText(result: unknown): string {
|
|
45
|
+
if (
|
|
46
|
+
typeof result === "object" &&
|
|
47
|
+
result !== null &&
|
|
48
|
+
Array.isArray((result as { content?: unknown }).content)
|
|
49
|
+
) {
|
|
50
|
+
const text = (result as { content: unknown[] }).content
|
|
51
|
+
.flatMap((part) =>
|
|
52
|
+
typeof part === "object" &&
|
|
53
|
+
part !== null &&
|
|
54
|
+
(part as { type?: unknown }).type === "text" &&
|
|
55
|
+
typeof (part as { text?: unknown }).text === "string"
|
|
56
|
+
? [(part as { text: string }).text]
|
|
57
|
+
: [],
|
|
58
|
+
)
|
|
59
|
+
.join("")
|
|
60
|
+
.trim();
|
|
61
|
+
if (text) return text;
|
|
62
|
+
}
|
|
63
|
+
return "tool execution failed";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function taskPrompt(type: AgentType, input: unknown): string {
|
|
67
|
+
return [
|
|
68
|
+
"Complete this bounded task.",
|
|
69
|
+
`Input:\n${JSON.stringify(input)}`,
|
|
70
|
+
`Return only one JSON object matching this schema:\n${JSON.stringify(z.toJSONSchema(type.outputSchema))}`,
|
|
71
|
+
].join("\n\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function runCloudflareSubAgent(
|
|
75
|
+
run: CloudflareSubAgentRun,
|
|
76
|
+
): Promise<unknown> {
|
|
77
|
+
const input = run.type.inputSchema.safeParse(run.input);
|
|
78
|
+
if (!input.success) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`SubAgent input failed schema validation: ${input.error.message}`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
for (const tool of run.tools) {
|
|
84
|
+
if (RECURSIVE_TOOLS.has(tool.name)) {
|
|
85
|
+
throw new Error(`SubAgent cannot use recursive tool "${tool.name}"`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (run.signal?.aborted) throw new Error("SubAgent run aborted");
|
|
89
|
+
|
|
90
|
+
// 这里没有审批闸,也装不了:到这一步工具已经被 compilePiTools 编译成 AgentTool,
|
|
91
|
+
// requiredExecutionLevel 只挂在编译前的 PiToolCandidate 上,这里读不到。「子 agent 不许挂需审批的工具」
|
|
92
|
+
// 这条不变量由装配处 createCloudflareSubAgentTools 的启动期断言强制执行。
|
|
93
|
+
const agent = new Agent({
|
|
94
|
+
streamFn: run.streamFn,
|
|
95
|
+
getApiKey: run.getApiKey,
|
|
96
|
+
initialState: {
|
|
97
|
+
model: run.model,
|
|
98
|
+
systemPrompt: assembleSubagentPrompt(run.type.persona),
|
|
99
|
+
thinkingLevel: run.type.model === "main" ? "medium" : "off",
|
|
100
|
+
messages: [],
|
|
101
|
+
tools: run.tools,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
let toolFailure: { name: string; error: string } | undefined;
|
|
105
|
+
agent.subscribe(async (event, signal) => {
|
|
106
|
+
if (event.type === "tool_execution_end" && event.isError) {
|
|
107
|
+
toolFailure ??= {
|
|
108
|
+
name: event.toolName,
|
|
109
|
+
error: toolErrorText(event.result),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
await run.onEvent?.(event, signal);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
let timedOut = false;
|
|
116
|
+
const abort = () => agent.abort();
|
|
117
|
+
run.signal?.addEventListener("abort", abort, { once: true });
|
|
118
|
+
const timeout = setTimeout(() => {
|
|
119
|
+
timedOut = true;
|
|
120
|
+
agent.abort();
|
|
121
|
+
}, run.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
await agent.prompt(taskPrompt(run.type, input.data));
|
|
125
|
+
} finally {
|
|
126
|
+
clearTimeout(timeout);
|
|
127
|
+
run.signal?.removeEventListener("abort", abort);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (timedOut) throw new Error("SubAgent run timed out");
|
|
131
|
+
if (run.signal?.aborted) throw new Error("SubAgent run aborted");
|
|
132
|
+
|
|
133
|
+
const assistant = agent.state.messages
|
|
134
|
+
.slice()
|
|
135
|
+
.reverse()
|
|
136
|
+
.find(
|
|
137
|
+
(message): message is AssistantMessage => message.role === "assistant",
|
|
138
|
+
);
|
|
139
|
+
if (!assistant) throw new Error("SubAgent produced no structured output");
|
|
140
|
+
if (assistant.stopReason === "aborted") {
|
|
141
|
+
throw new Error("SubAgent run aborted");
|
|
142
|
+
}
|
|
143
|
+
if (assistant.stopReason === "error") {
|
|
144
|
+
if (toolFailure) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`SubAgent tool "${toolFailure.name}" failed: ${toolFailure.error}`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
throw new Error(
|
|
150
|
+
`SubAgent model failed: ${assistant.errorMessage ?? "unknown provider error"}`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (toolFailure) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`SubAgent tool "${toolFailure.name}" failed: ${toolFailure.error}`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const text = textOf(assistant);
|
|
160
|
+
if (!text) throw new Error("SubAgent produced no structured output");
|
|
161
|
+
|
|
162
|
+
let output: unknown;
|
|
163
|
+
try {
|
|
164
|
+
output = JSON.parse(text);
|
|
165
|
+
} catch {
|
|
166
|
+
throw new Error("SubAgent output is not valid JSON");
|
|
167
|
+
}
|
|
168
|
+
const result = run.type.outputSchema.safeParse(output);
|
|
169
|
+
if (!result.success) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`SubAgent output failed schema validation: ${result.error.message}`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
return result.data;
|
|
175
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DynamicWorkerExecutor,
|
|
3
|
+
resolveProvider,
|
|
4
|
+
runCode,
|
|
5
|
+
} from "@cloudflare/codemode";
|
|
6
|
+
import {
|
|
7
|
+
createWorkspaceStateBackend,
|
|
8
|
+
type WorkspaceFsLike,
|
|
9
|
+
} from "@cloudflare/shell";
|
|
10
|
+
import { stateToolsFromBackend } from "@cloudflare/shell/workers";
|
|
11
|
+
import type {
|
|
12
|
+
AgentTool,
|
|
13
|
+
AgentToolResult,
|
|
14
|
+
} from "@earendil-works/pi-agent-core";
|
|
15
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
16
|
+
import type { WorkspacePort } from "../../../kernel/bindings";
|
|
17
|
+
import type { AgentType } from "../../../layers/orchestration/subagents/agent-types/contract";
|
|
18
|
+
import {
|
|
19
|
+
compilePiTools,
|
|
20
|
+
type CompilePiToolsOptions,
|
|
21
|
+
type PiToolCandidate,
|
|
22
|
+
} from "../../../pi/tool";
|
|
23
|
+
import { workspacePiToolCandidates } from "../../../pi/tool/workspace-sandbox";
|
|
24
|
+
|
|
25
|
+
const READONLY_WORKSPACE_TOOLS = new Set([
|
|
26
|
+
"read",
|
|
27
|
+
"list",
|
|
28
|
+
"find",
|
|
29
|
+
"grep",
|
|
30
|
+
]);
|
|
31
|
+
const DENIED_TOOLS = [
|
|
32
|
+
// SubAgents have no approval channel; keep the restored high-risk Workspace
|
|
33
|
+
// shell on the main Agent surface, matching the pre-Pi behavior.
|
|
34
|
+
"bash",
|
|
35
|
+
"run_agent",
|
|
36
|
+
"subagents",
|
|
37
|
+
"fanout",
|
|
38
|
+
"extract",
|
|
39
|
+
"dispatch_background",
|
|
40
|
+
];
|
|
41
|
+
const MAX_FETCH_BODY_BYTES = 50_000;
|
|
42
|
+
|
|
43
|
+
const executeParameters = Type.Object({
|
|
44
|
+
code: Type.String({
|
|
45
|
+
minLength: 1,
|
|
46
|
+
description:
|
|
47
|
+
"JavaScript to run. Return the final value. The sandbox provides fetch and state.*.",
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const fetchParameters = Type.Object({
|
|
52
|
+
url: Type.String({ minLength: 1, maxLength: 8_192 }),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function result(details: unknown): AgentToolResult<unknown> {
|
|
56
|
+
let text: string;
|
|
57
|
+
try {
|
|
58
|
+
text = JSON.stringify(details);
|
|
59
|
+
} catch {
|
|
60
|
+
text = String(details);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: "text", text }],
|
|
64
|
+
details,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function executeCandidate(options: {
|
|
69
|
+
workspace: WorkspacePort;
|
|
70
|
+
loader: WorkerLoader;
|
|
71
|
+
outbound: Fetcher;
|
|
72
|
+
}): PiToolCandidate {
|
|
73
|
+
const executor = new DynamicWorkerExecutor({
|
|
74
|
+
loader: options.loader,
|
|
75
|
+
globalOutbound: options.outbound,
|
|
76
|
+
});
|
|
77
|
+
const state = resolveProvider(
|
|
78
|
+
stateToolsFromBackend(
|
|
79
|
+
createWorkspaceStateBackend(
|
|
80
|
+
options.workspace as unknown as WorkspaceFsLike,
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
const tool: AgentTool<typeof executeParameters> = {
|
|
85
|
+
name: "execute",
|
|
86
|
+
label: "Execute JavaScript",
|
|
87
|
+
description:
|
|
88
|
+
"Run JavaScript in an isolated Worker with controlled network access and this Chat's scoped " +
|
|
89
|
+
"Workspace exposed as state.*. Return the final value from the script.",
|
|
90
|
+
parameters: executeParameters,
|
|
91
|
+
async execute(_toolCallId, { code }, signal, onUpdate) {
|
|
92
|
+
signal?.throwIfAborted();
|
|
93
|
+
onUpdate?.(
|
|
94
|
+
result({ status: "running", message: "Executing sandbox code." }),
|
|
95
|
+
);
|
|
96
|
+
const output = await runCode({
|
|
97
|
+
code,
|
|
98
|
+
executor,
|
|
99
|
+
providers: [state],
|
|
100
|
+
});
|
|
101
|
+
signal?.throwIfAborted();
|
|
102
|
+
return result(output);
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
owner: "subagent-execute",
|
|
107
|
+
requiredExecutionLevel: "safe",
|
|
108
|
+
tool,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function boundedBody(
|
|
113
|
+
response: Response,
|
|
114
|
+
): Promise<{ body: string; truncated: boolean }> {
|
|
115
|
+
const reader = response.body?.getReader();
|
|
116
|
+
if (!reader) return { body: "", truncated: false };
|
|
117
|
+
|
|
118
|
+
const decoder = new TextDecoder();
|
|
119
|
+
let body = "";
|
|
120
|
+
let bytes = 0;
|
|
121
|
+
let truncated = false;
|
|
122
|
+
try {
|
|
123
|
+
while (bytes < MAX_FETCH_BODY_BYTES) {
|
|
124
|
+
const next = await reader.read();
|
|
125
|
+
if (next.done) {
|
|
126
|
+
body += decoder.decode();
|
|
127
|
+
return { body, truncated };
|
|
128
|
+
}
|
|
129
|
+
const remaining = MAX_FETCH_BODY_BYTES - bytes;
|
|
130
|
+
const value = next.value;
|
|
131
|
+
const accepted =
|
|
132
|
+
value.byteLength > remaining ? value.subarray(0, remaining) : value;
|
|
133
|
+
bytes += accepted.byteLength;
|
|
134
|
+
body += decoder.decode(accepted, { stream: true });
|
|
135
|
+
if (accepted.byteLength < value.byteLength) {
|
|
136
|
+
truncated = true;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (bytes >= MAX_FETCH_BODY_BYTES) truncated = true;
|
|
141
|
+
body += decoder.decode();
|
|
142
|
+
return { body, truncated };
|
|
143
|
+
} finally {
|
|
144
|
+
if (truncated) await reader.cancel().catch(() => {});
|
|
145
|
+
else reader.releaseLock();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function fetchCandidate(options: {
|
|
150
|
+
outbound: Fetcher;
|
|
151
|
+
}): PiToolCandidate {
|
|
152
|
+
const tool: AgentTool<typeof fetchParameters> = {
|
|
153
|
+
name: "fetch_url",
|
|
154
|
+
label: "Fetch URL",
|
|
155
|
+
description:
|
|
156
|
+
"Fetch one HTTPS URL and return its status, headers, and a bounded text body.",
|
|
157
|
+
parameters: fetchParameters,
|
|
158
|
+
async execute(_toolCallId, { url }, signal, onUpdate) {
|
|
159
|
+
const parsed = new URL(url);
|
|
160
|
+
if (parsed.protocol !== "https:") {
|
|
161
|
+
throw new Error("fetch_url only allows HTTPS URLs");
|
|
162
|
+
}
|
|
163
|
+
onUpdate?.(
|
|
164
|
+
result({ status: "running", message: `Fetching ${parsed.host}.` }),
|
|
165
|
+
);
|
|
166
|
+
signal?.throwIfAborted();
|
|
167
|
+
const response = await options.outbound.fetch(
|
|
168
|
+
new Request(parsed.toString(), { signal }),
|
|
169
|
+
);
|
|
170
|
+
const { body, truncated } = await boundedBody(response);
|
|
171
|
+
return result({
|
|
172
|
+
url: response.url,
|
|
173
|
+
status: response.status,
|
|
174
|
+
statusText: response.statusText,
|
|
175
|
+
headers: Object.fromEntries(response.headers),
|
|
176
|
+
body,
|
|
177
|
+
truncated,
|
|
178
|
+
});
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
return {
|
|
182
|
+
owner: "subagent-fetch",
|
|
183
|
+
requiredExecutionLevel: "safe",
|
|
184
|
+
tool,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* SubAgent 链路上没有审批通道:主链的审批闸在 Runtime 的 execution 适配器里
|
|
190
|
+
* (requiresPiToolApproval → park → 等用户裁决),而子 agent 是 `new Agent({ tools })`
|
|
191
|
+
* 直接跑,parked 的请求没有任何界面能落地、也没有人能批。
|
|
192
|
+
*
|
|
193
|
+
* 只有 `safe` Tool 可以挂到这条没有审批通道的链路;更高档位必须在装配时失败关闭。
|
|
194
|
+
*
|
|
195
|
+
* 所以把这个不变量钉在装配处:要的是「装不上」,不是「跑起来才炸」。而且这里是唯一还能
|
|
196
|
+
* 检查的位置 —— 档位挂在 PiToolCandidate 上,compilePiTools 之后只剩 AgentTool。
|
|
197
|
+
*/
|
|
198
|
+
function assertSafeExecutionLevel(
|
|
199
|
+
candidates: readonly PiToolCandidate[],
|
|
200
|
+
): void {
|
|
201
|
+
const risky = candidates.filter(
|
|
202
|
+
(candidate) => candidate.requiredExecutionLevel !== "safe",
|
|
203
|
+
);
|
|
204
|
+
if (risky.length === 0) return;
|
|
205
|
+
throw new Error(
|
|
206
|
+
"SubAgent tools must require safe execution level: " +
|
|
207
|
+
risky
|
|
208
|
+
.map((candidate) =>
|
|
209
|
+
`${candidate.tool.name} (${candidate.requiredExecutionLevel})`
|
|
210
|
+
)
|
|
211
|
+
.join(", ") +
|
|
212
|
+
". The SubAgent chain has no approval channel, so a tool that needs " +
|
|
213
|
+
"approval would execute unreviewed. Wire an approval channel into the " +
|
|
214
|
+
"SubAgent run before mounting it.",
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Assemble the complete Pi-native tool surface for one already-scoped
|
|
220
|
+
* SubAgent run. The caller owns Workspace scoping and network egress.
|
|
221
|
+
*/
|
|
222
|
+
export function createCloudflareSubAgentTools(options: {
|
|
223
|
+
profile: AgentType["tools"];
|
|
224
|
+
workspace: WorkspacePort;
|
|
225
|
+
loader: WorkerLoader;
|
|
226
|
+
outbound: Fetcher;
|
|
227
|
+
settle: CompilePiToolsOptions["settle"];
|
|
228
|
+
}): AgentTool[] {
|
|
229
|
+
const workspace = workspacePiToolCandidates(options.workspace).filter(
|
|
230
|
+
(candidate) =>
|
|
231
|
+
options.profile === "capable" ||
|
|
232
|
+
READONLY_WORKSPACE_TOOLS.has(candidate.tool.name),
|
|
233
|
+
);
|
|
234
|
+
const candidates =
|
|
235
|
+
options.profile === "capable"
|
|
236
|
+
? [
|
|
237
|
+
...workspace,
|
|
238
|
+
executeCandidate(options),
|
|
239
|
+
fetchCandidate(options),
|
|
240
|
+
]
|
|
241
|
+
: [...workspace, fetchCandidate(options)];
|
|
242
|
+
|
|
243
|
+
// 子 Agent 是独立的嵌套 Tool surface:先移除会递归委派的候选,
|
|
244
|
+
// compiler 只负责执行治理和结算。
|
|
245
|
+
const deny = new Set(DENIED_TOOLS);
|
|
246
|
+
const mounted = candidates.filter(
|
|
247
|
+
(candidate) => !deny.has(candidate.tool.name),
|
|
248
|
+
);
|
|
249
|
+
assertSafeExecutionLevel(mounted);
|
|
250
|
+
|
|
251
|
+
return compilePiTools(mounted, {
|
|
252
|
+
settle: options.settle,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { RuntimeTurnEventsPort } from "../../../kernel/bindings";
|
|
2
|
+
import type { RuntimeAgentHooks } from "../../../runtime-definition";
|
|
3
|
+
|
|
4
|
+
export function createUniversalAgentHooks<
|
|
5
|
+
Env extends Cloudflare.Env,
|
|
6
|
+
Config,
|
|
7
|
+
Command = never,
|
|
8
|
+
Change = never,
|
|
9
|
+
>(adapter: {
|
|
10
|
+
guard(config: Config): Promise<void>;
|
|
11
|
+
turnEvents(): RuntimeTurnEventsPort | Promise<RuntimeTurnEventsPort>;
|
|
12
|
+
}): RuntimeAgentHooks<Env, Config, Command, Change> {
|
|
13
|
+
const turnEvents = () => Promise.resolve(adapter.turnEvents());
|
|
14
|
+
return {
|
|
15
|
+
beforeCommit: [async (_context, config) => adapter.guard(config)],
|
|
16
|
+
onTurnEnd: async (_context, messages) => {
|
|
17
|
+
await (await turnEvents()).onResponse(messages);
|
|
18
|
+
},
|
|
19
|
+
onModelUsage: async (event) => {
|
|
20
|
+
await (await turnEvents()).onModelUsage?.(event);
|
|
21
|
+
},
|
|
22
|
+
onToolSettled: async (event) => {
|
|
23
|
+
await (await turnEvents()).onToolSettled?.(event);
|
|
24
|
+
},
|
|
25
|
+
onApproval: async (input) => {
|
|
26
|
+
await (await turnEvents()).onApproval?.(input);
|
|
27
|
+
},
|
|
28
|
+
onActivityChanged: async (projection) => {
|
|
29
|
+
await (await turnEvents()).onActivityChanged?.(projection);
|
|
30
|
+
},
|
|
31
|
+
onSubmissionTerminal: async (input) => {
|
|
32
|
+
await (await turnEvents()).onSubmissionTerminal?.(input);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|