@springbrand/agent-runtime 0.2.0-alpha.19 → 0.2.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/adapter/cloudflare/universal-agent/preparation.ts +32 -5
- package/src/db/approval.repo.ts +4 -1
- package/src/index.ts +3 -1
- package/src/kernel/approval-lifecycle.ts +1 -0
- package/src/kernel/bindings.ts +22 -13
- package/src/kernel/receipts.ts +2 -0
- package/src/kernel/tool-surface.ts +41 -0
- package/src/layers/context/budget/gate.ts +57 -11
- package/src/lib/prompt.ts +32 -15
- package/src/pi/message/conversion.ts +1 -13
- package/src/pi/runtime-adapter/execution.ts +44 -5
- package/src/pi/tool/compiler.ts +9 -5
- package/src/pi/tool/core-host.ts +102 -13
- package/src/pi/tool/core.ts +209 -61
- package/src/pi/tool/skill.ts +88 -1
- package/src/pi/tool/workspace-sandbox.ts +136 -4
- package/src/runtime-assembler.ts +32 -8
- package/src/runtime.ts +12 -9
- package/src/skills/index.ts +14 -0
- package/src/skills/springbrand-worker-website/index.ts +73 -0
- package/src/tool-registry.ts +15 -32
package/package.json
CHANGED
|
@@ -4,7 +4,6 @@ import type {
|
|
|
4
4
|
RunAgentToolOptions,
|
|
5
5
|
} from "agents";
|
|
6
6
|
import type {
|
|
7
|
-
RuntimeBrowserPort,
|
|
8
7
|
RuntimeMemoryPort,
|
|
9
8
|
RuntimePlatformPort,
|
|
10
9
|
RuntimeSubagentPort,
|
|
@@ -15,7 +14,11 @@ import type { RuntimeDegradation } from "../../../kernel/degradation";
|
|
|
15
14
|
import type { RuntimeMemoryProfile } from "../../../kernel/profile";
|
|
16
15
|
import { AGENT_TYPES } from "../../../layers/orchestration/subagents/agent-types/registry";
|
|
17
16
|
import type { RuntimeAgentConfigContext } from "../../../runtime-agent-context";
|
|
18
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
createBrowserExecutionFactory,
|
|
19
|
+
createWorkspaceCodeExecutionFactory,
|
|
20
|
+
type RuntimeBrowserBinding,
|
|
21
|
+
} from "../../../pi/tool/core-host";
|
|
19
22
|
import {
|
|
20
23
|
createCloudflareSandboxAdapter,
|
|
21
24
|
type SandboxAdmission,
|
|
@@ -33,7 +36,8 @@ export type PlatformLoader = () => Promise<RuntimePlatformPort>;
|
|
|
33
36
|
|
|
34
37
|
export interface CloudflarePlatformBindings {
|
|
35
38
|
LOADER: WorkerLoader;
|
|
36
|
-
|
|
39
|
+
/** Browser Rendering 绑定(可选):CDP Code Mode 的 `browser_execute` 由它开出浏览器会话。 */
|
|
40
|
+
BROWSER?: RuntimeBrowserBinding;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
export interface WorkspaceRequirement {
|
|
@@ -97,7 +101,18 @@ export function createWorkspaceLoader(
|
|
|
97
101
|
|
|
98
102
|
export function createPlatformLoader<
|
|
99
103
|
Env extends Cloudflare.Env & CloudflarePlatformBindings,
|
|
100
|
-
>(
|
|
104
|
+
>(
|
|
105
|
+
context: RuntimeAgentConfigContext<Env>,
|
|
106
|
+
options: {
|
|
107
|
+
/**
|
|
108
|
+
* 本 Session 的 Workspace preview URL(可以带短期签名 query)。
|
|
109
|
+
*
|
|
110
|
+
* 只有 Host 知道公开 origin、这个 Session 的身份和签名方式,所以只能由 Host 传进来。
|
|
111
|
+
* 调用方拿不到就别传 —— 浏览器工具会因此不声称自己能打开产物。
|
|
112
|
+
*/
|
|
113
|
+
previewBaseUrl?: string;
|
|
114
|
+
} = {},
|
|
115
|
+
): PlatformLoader {
|
|
101
116
|
let cached: ReturnType<PlatformLoader> | undefined;
|
|
102
117
|
return () => cached ??= Promise.resolve().then(() => {
|
|
103
118
|
const exports = (context.ctx as unknown as {
|
|
@@ -107,7 +122,19 @@ export function createPlatformLoader<
|
|
|
107
122
|
}).exports;
|
|
108
123
|
return {
|
|
109
124
|
loader: context.env.LOADER,
|
|
110
|
-
|
|
125
|
+
// 绑定缺失时整条浏览器能力不进 Platform Port,Tool Surface 因此注册空集。
|
|
126
|
+
...(context.env.BROWSER
|
|
127
|
+
? {
|
|
128
|
+
browser: createBrowserExecutionFactory({
|
|
129
|
+
ctx: context.ctx,
|
|
130
|
+
loader: context.env.LOADER,
|
|
131
|
+
browser: context.env.BROWSER,
|
|
132
|
+
...(options.previewBaseUrl
|
|
133
|
+
? { previewBaseUrl: options.previewBaseUrl }
|
|
134
|
+
: {}),
|
|
135
|
+
}),
|
|
136
|
+
}
|
|
137
|
+
: {}),
|
|
111
138
|
outbound: () => exports.HttpGateway({}),
|
|
112
139
|
};
|
|
113
140
|
});
|
package/src/db/approval.repo.ts
CHANGED
|
@@ -39,6 +39,7 @@ export interface NewApproval {
|
|
|
39
39
|
|
|
40
40
|
export interface PendingApprovalSummary {
|
|
41
41
|
executionId: string;
|
|
42
|
+
toolCallId: string;
|
|
42
43
|
source: string;
|
|
43
44
|
toolName: string;
|
|
44
45
|
summary: string;
|
|
@@ -189,6 +190,7 @@ export class ApprovalRepository {
|
|
|
189
190
|
listPendingActive(): PendingApprovalSummary[] {
|
|
190
191
|
return this.sql<{
|
|
191
192
|
execution_id: string;
|
|
193
|
+
tool_call_id: string;
|
|
192
194
|
source: string;
|
|
193
195
|
tool_name: string;
|
|
194
196
|
summary: string;
|
|
@@ -197,7 +199,7 @@ export class ApprovalRepository {
|
|
|
197
199
|
input_json: string;
|
|
198
200
|
request_id: string;
|
|
199
201
|
}>`
|
|
200
|
-
SELECT approval.execution_id, approval.source,
|
|
202
|
+
SELECT approval.execution_id, approval.tool_call_id, approval.source,
|
|
201
203
|
approval.tool_name, approval.summary,
|
|
202
204
|
approval.execution_level, approval.required_level,
|
|
203
205
|
approval.input_json, approval.request_id
|
|
@@ -209,6 +211,7 @@ export class ApprovalRepository {
|
|
|
209
211
|
ORDER BY approval.created_at ASC
|
|
210
212
|
`.map((row) => ({
|
|
211
213
|
executionId: row.execution_id,
|
|
214
|
+
toolCallId: row.tool_call_id,
|
|
212
215
|
source: row.source,
|
|
213
216
|
toolName: row.tool_name,
|
|
214
217
|
summary: row.summary,
|
package/src/index.ts
CHANGED
|
@@ -135,10 +135,12 @@ export {
|
|
|
135
135
|
export { skillPiToolCandidates } from "./pi/tool";
|
|
136
136
|
export type { PiSkillBinding } from "./pi/tool";
|
|
137
137
|
export {
|
|
138
|
-
|
|
138
|
+
BROWSER_EXECUTE_TOOL_NAME,
|
|
139
|
+
browserExecutionPiToolCandidate,
|
|
139
140
|
codeExecutionPiToolCandidate,
|
|
140
141
|
} from "./pi/tool";
|
|
141
142
|
export {
|
|
143
|
+
createBrowserExecutionFactory,
|
|
142
144
|
createWorkspaceCodeExecutionFactory,
|
|
143
145
|
} from "./pi/tool";
|
|
144
146
|
export {
|
|
@@ -158,6 +158,7 @@ export class ApprovalLifecycle<
|
|
|
158
158
|
list(): ApprovalReceipt[] {
|
|
159
159
|
return this.options.db.approvals.listPendingActive().map((row) => ({
|
|
160
160
|
executionId: row.executionId,
|
|
161
|
+
toolCallId: row.toolCallId,
|
|
161
162
|
source: row.source as ApprovalReceipt["source"],
|
|
162
163
|
action: row.toolName,
|
|
163
164
|
summary: row.summary,
|
package/src/kernel/bindings.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { SkillSource } from "agents/skills";
|
|
|
2
2
|
import type { ScheduleSpec } from "./receipts";
|
|
3
3
|
import type { RuntimeActivityProjection } from "./state";
|
|
4
4
|
import type { ExecutionLevel } from "../lib/execution-level";
|
|
5
|
-
import type {
|
|
5
|
+
import type { ToolRegistry } from "./tool-surface";
|
|
6
6
|
import type {
|
|
7
7
|
RuntimeEventConfirmation,
|
|
8
8
|
RuntimeLifecycleFact,
|
|
@@ -856,26 +856,23 @@ export interface RuntimeTurnEventsPort {
|
|
|
856
856
|
}
|
|
857
857
|
|
|
858
858
|
/**
|
|
859
|
-
* 向 Runtime
|
|
859
|
+
* 向 Runtime 提供一个真实无头浏览器的 CDP Code Mode 执行能力。
|
|
860
860
|
*
|
|
861
861
|
* @remarks
|
|
862
|
-
* Platform Plugin 在 Browser
|
|
862
|
+
* Platform Plugin 在 Browser 绑定存在时准备它,Tool Surface 决定注册 `browser_execute` 时才调用 `create()`。
|
|
863
863
|
*
|
|
864
|
-
* 该 Port
|
|
864
|
+
* 该 Port 只承载「能不能开出一个浏览器执行端口」,浏览器会话生命周期与 CDP 协议细节都留在 `agents/browser`,
|
|
865
|
+
* 避免本仓复制一份会随上游漂移的协议。旧的 Quick Action `action + options` 单方法协议已于 2026-08-13 退役:
|
|
866
|
+
* 复测确认本地绑定至今不实现 `quickAction()`,该面在本仓从未执行过。
|
|
865
867
|
*/
|
|
866
868
|
export interface RuntimeBrowserPort {
|
|
867
869
|
/**
|
|
868
|
-
*
|
|
870
|
+
* 创建本次装配的浏览器代码执行端口。
|
|
869
871
|
*
|
|
870
872
|
* @remarks
|
|
871
|
-
*
|
|
872
|
-
*
|
|
873
|
-
* Runtime 不复制 Browser Run 的选项联合类型,避免两份协议随上游漂移。
|
|
873
|
+
* Tool Surface 在浏览器工具确实可见时调用一次,延迟到此刻是为了让被 deny 的装配不白建连接器。
|
|
874
874
|
*/
|
|
875
|
-
|
|
876
|
-
action: string,
|
|
877
|
-
options: unknown,
|
|
878
|
-
): Promise<Response>;
|
|
875
|
+
create(): RuntimeCodeExecutionPort;
|
|
879
876
|
}
|
|
880
877
|
|
|
881
878
|
// #endregion
|
|
@@ -942,7 +939,7 @@ export interface RuntimeProviderPort {
|
|
|
942
939
|
export interface RuntimePlatformPort {
|
|
943
940
|
/** Workspace Codemode 在创建 Dynamic Worker 执行器时使用的 Worker Loader。 */
|
|
944
941
|
loader: WorkerLoader;
|
|
945
|
-
/** Platform Plugin 存在 Browser Run
|
|
942
|
+
/** Platform Plugin 存在 Browser Run 绑定时用它生成 `browser_execute`;缺失即整条浏览器 Tool 面不注册。 */
|
|
946
943
|
browser?: RuntimeBrowserPort;
|
|
947
944
|
/**
|
|
948
945
|
* 为一次 Dynamic Worker 组装取得已限定的网络出口。
|
|
@@ -1021,6 +1018,18 @@ export interface RuntimeSkillSourceBinding {
|
|
|
1021
1018
|
*
|
|
1022
1019
|
* 它不携带业务 ID、Repository、数据库 Key、任意能力注册表或凭据配置;术语见 `../index.ts`。
|
|
1023
1020
|
*/
|
|
1021
|
+
/**
|
|
1022
|
+
* 延迟到最终 Tool Surface 完成后再创建 Code Mode 执行能力。
|
|
1023
|
+
*
|
|
1024
|
+
* @remarks
|
|
1025
|
+
* 定义在这里而不是 `tool-registry.ts`:它的返回类型是本文件的
|
|
1026
|
+
* `RuntimeCodeExecutionPort`,而 `RuntimeBindings` 又要引用这个工厂 —— 放在
|
|
1027
|
+
* tool-registry 会让两个模块互相 import。`tool-registry.ts` 继续对外导出它。
|
|
1028
|
+
*/
|
|
1029
|
+
export interface RuntimeCodeExecutionFactory {
|
|
1030
|
+
create(tools: ToolRegistry): RuntimeCodeExecutionPort;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1024
1033
|
export interface RuntimeBindings {
|
|
1025
1034
|
provider: RuntimeProviderPort;
|
|
1026
1035
|
platform: RuntimePlatformPort;
|
package/src/kernel/receipts.ts
CHANGED
|
@@ -24,6 +24,8 @@ export const APPROVAL_DECISIONS = [
|
|
|
24
24
|
*/
|
|
25
25
|
export interface ApprovalReceipt {
|
|
26
26
|
executionId: string;
|
|
27
|
+
/** Tool call that owns this approval; absent for non-tool approval sources. */
|
|
28
|
+
toolCallId?: string;
|
|
27
29
|
source: "action" | "codemode" | "temporary-agent";
|
|
28
30
|
action: string;
|
|
29
31
|
summary: string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ExecutionLevel } from "../lib/execution-level";
|
|
2
|
+
import type { PiToolCandidate } from "../pi/tool/compiler";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tool Surface 的形状类型。
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* `kernel/bindings.ts` 和 `tool-registry.ts` 都要用它们,所以它们不能住在其中任何
|
|
9
|
+
* 一边:`RuntimeBindings.codeExecution` 的工厂签名需要 `ToolRegistry`,而
|
|
10
|
+
* `tool-registry.ts` 又需要 bindings 里的各个 Port —— 两边互相 import 就形成了
|
|
11
|
+
* depcruise 的 no-circular 违规(`bindings → tool-registry → bindings`)。
|
|
12
|
+
*
|
|
13
|
+
* 这里只放形状,不放行为:注册表的合并、筛选和构造仍然留在 `tool-registry.ts`。
|
|
14
|
+
*
|
|
15
|
+
* 术语见 `../index.ts`。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** 模型调用 Tool 时传给 `execute` 的上下文。 */
|
|
19
|
+
export interface ToolContext {
|
|
20
|
+
readonly toolCallId: string;
|
|
21
|
+
readonly signal: AbortSignal;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Definition 作者声明的一个 Tool。 */
|
|
25
|
+
export interface ToolSpec extends Partial<
|
|
26
|
+
Omit<PiToolCandidate, "tool" | "requiredExecutionLevel">
|
|
27
|
+
> {
|
|
28
|
+
readonly label: string;
|
|
29
|
+
readonly description: string;
|
|
30
|
+
readonly parameters: unknown;
|
|
31
|
+
readonly requiredExecutionLevel: ExecutionLevel;
|
|
32
|
+
/** Require a fresh human decision for every call, regardless of execution level. */
|
|
33
|
+
readonly alwaysRequiresApproval?: boolean;
|
|
34
|
+
readonly execute: (
|
|
35
|
+
input: unknown,
|
|
36
|
+
ctx: ToolContext,
|
|
37
|
+
) => Promise<unknown>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 一次装配声明的全部 Tool,键即模型可见的工具名。 */
|
|
41
|
+
export type ToolRegistry = Record<string, ToolSpec>;
|
|
@@ -15,14 +15,48 @@ import {
|
|
|
15
15
|
* must be given an explicit protected path here.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* 预算常量的排序不变量(改任何一个都必须同时复核其余三个):
|
|
20
|
+
*
|
|
21
|
+
* MODEL_LEAF_MAX_CHARS < MODEL_REPLAY_THRESHOLD < SPILL_THRESHOLD
|
|
22
|
+
* ≤ STORAGE_LEAF_MAX_CHARS
|
|
23
|
+
* 4 KB 16 KB 32 KB 64 KB
|
|
24
|
+
*
|
|
25
|
+
* 这四个数回答四个不同问题。曾经其中两个共用一个常量,结果“放宽外置”和“收紧
|
|
26
|
+
* 历史”这两个互相冲突的诉求被绑在同一个旋钮上,只能二选一。
|
|
27
|
+
*/
|
|
20
28
|
|
|
21
|
-
/**
|
|
22
|
-
|
|
29
|
+
/**
|
|
30
|
+
* 持久化单个字符串叶子的上限。
|
|
31
|
+
*
|
|
32
|
+
* MUST ≥ 单页 read 的上限(见 `workspace-sandbox.ts` 的 `READ_PAGE_MAX_CHARS`),
|
|
33
|
+
* 否则持久记录会比模型当轮看到的内容还少,会话恢复后出现凭空缺页。
|
|
34
|
+
*/
|
|
35
|
+
export const STORAGE_LEAF_MAX_CHARS = 64 * 1024;
|
|
23
36
|
|
|
24
|
-
/**
|
|
25
|
-
|
|
37
|
+
/**
|
|
38
|
+
* 一次新的 Tool 结果多大才值得外置成 Workspace 文件。
|
|
39
|
+
*
|
|
40
|
+
* 只影响当轮新产生的结果,不影响历史重放。
|
|
41
|
+
*/
|
|
42
|
+
export const SPILL_THRESHOLD = 32 * 1024;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 历史里一条旧 Tool 结果重放多少。
|
|
46
|
+
*
|
|
47
|
+
* 这条最紧,因为它对每个 Turn 的**全部**历史消息生效(见
|
|
48
|
+
* `pi/runtime-adapter/execution.ts` 的 `projectToolResultsForModel`):
|
|
49
|
+
* 放宽 1 KB 就是 N 条历史 × 1 KB。
|
|
50
|
+
*/
|
|
51
|
+
export const MODEL_REPLAY_THRESHOLD = 16 * 1024;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 重放时单个字符串叶子的上限。
|
|
55
|
+
*
|
|
56
|
+
* 曾经是 500,小到让 Code Mode 这类返回长字符串的结果在**第二轮**就塌成碎片,
|
|
57
|
+
* 模型只能反复换姿势(`Object.values()`、`slice()`)去捞已经不存在的内容。
|
|
58
|
+
*/
|
|
59
|
+
export const MODEL_LEAF_MAX_CHARS = 4 * 1024;
|
|
26
60
|
|
|
27
61
|
const ELISION_RESERVE = 40;
|
|
28
62
|
const PREVIEW_CHARS = 600;
|
|
@@ -118,7 +152,7 @@ export async function spillDurableToolOutput(
|
|
|
118
152
|
},
|
|
119
153
|
): Promise<ArtifactRef | null> {
|
|
120
154
|
const { text, ext } = serializeOutput(value);
|
|
121
|
-
if (text.length <=
|
|
155
|
+
if (text.length <= SPILL_THRESHOLD || !options.workspace) return null;
|
|
122
156
|
try {
|
|
123
157
|
const hash = await sha256Hex(text);
|
|
124
158
|
const path = `${DEFAULT_SPILL_DIR}/${hash.slice(0, 24)}.${ext}`;
|
|
@@ -133,9 +167,19 @@ export async function spillDurableToolOutput(
|
|
|
133
167
|
bytes: new TextEncoder().encode(text).byteLength,
|
|
134
168
|
hash: hash.slice(0, 16),
|
|
135
169
|
preview: text.slice(0, PREVIEW_CHARS),
|
|
170
|
+
// 出口指令必须是可执行的:只说“去 read 这个路径”会诱导模型整读,而整读一个
|
|
171
|
+
// 刚刚因为过大被外置的文件毫无意义。这里明确要求分页,并告知 read 会返回
|
|
172
|
+
// nextOffset / eof 以便继续。
|
|
173
|
+
// 出口指令必须是可执行的,而且必须只承诺模型真的拿得到的东西:Provider 只把
|
|
174
|
+
// Tool 结果的 content 发给模型,所以这里不能引用只存在于 details 的字段。
|
|
175
|
+
// 溢出产物是 JSON,一个大字符串叶子会整块挤在一行上,而按行分页追不回被行宽
|
|
176
|
+
// 截断的内容 —— 那种情况要走 grep / bash,不能让模型以为 read 一定够用。
|
|
136
177
|
note:
|
|
137
178
|
"Output was large and has been saved to the Workspace file above. " +
|
|
138
|
-
"
|
|
179
|
+
"Read it in pages with read(path, offset, limit) — do not read it whole; " +
|
|
180
|
+
"each page ends with a footer telling you the line range and the next offset. " +
|
|
181
|
+
"This file is JSON, so a single large value can sit on one very long line: " +
|
|
182
|
+
"if a page reports that lines were cut short, use grep or bash on the path instead.",
|
|
139
183
|
};
|
|
140
184
|
} catch {
|
|
141
185
|
return null;
|
|
@@ -145,7 +189,9 @@ export async function spillDurableToolOutput(
|
|
|
145
189
|
// Bounds leaves first, then removes oldest array entries until the view fits.
|
|
146
190
|
function structureView(output: unknown): unknown {
|
|
147
191
|
const capped = truncateStringLeaves(output, MODEL_LEAF_MAX_CHARS);
|
|
148
|
-
if (serializeOutput(capped).text.length <=
|
|
192
|
+
if (serializeOutput(capped).text.length <= MODEL_REPLAY_THRESHOLD) {
|
|
193
|
+
return capped;
|
|
194
|
+
}
|
|
149
195
|
if (typeof capped !== "object" || capped === null) return capped;
|
|
150
196
|
|
|
151
197
|
const record = { ...(capped as Record<string, unknown>) };
|
|
@@ -159,7 +205,7 @@ function structureView(output: unknown): unknown {
|
|
|
159
205
|
while (
|
|
160
206
|
items.length > 1 &&
|
|
161
207
|
serializeOutput({ ...record, [arrayKey]: items }).text.length >
|
|
162
|
-
|
|
208
|
+
MODEL_REPLAY_THRESHOLD
|
|
163
209
|
) {
|
|
164
210
|
items.shift();
|
|
165
211
|
dropped += 1;
|
|
@@ -178,7 +224,7 @@ function structureView(output: unknown): unknown {
|
|
|
178
224
|
/** Creates a non-destructive, smaller Tool-result projection for the model. */
|
|
179
225
|
export function projectToolOutputForModel(output: unknown): unknown {
|
|
180
226
|
try {
|
|
181
|
-
return serializeOutput(output).text.length <=
|
|
227
|
+
return serializeOutput(output).text.length <= MODEL_REPLAY_THRESHOLD
|
|
182
228
|
? output
|
|
183
229
|
: structureView(output);
|
|
184
230
|
} catch {
|
package/src/lib/prompt.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stable system-prompt sections shared by every Runtime profile.
|
|
3
3
|
*
|
|
4
|
-
* Only `PERSONA` is replaceable by `RuntimeProfile.systemPrompt`.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* not actually provide.
|
|
4
|
+
* Only `PERSONA` is replaceable by `RuntimeProfile.systemPrompt`. Environment,
|
|
5
|
+
* tool and safety facts remain because a custom Agent persona must not redefine
|
|
6
|
+
* capabilities that the running kernel does not actually provide.
|
|
8
7
|
* Dynamic skills, memory and context blocks are injected by their own modules
|
|
9
8
|
* so this static prefix remains cacheable.
|
|
10
9
|
*/
|
|
@@ -15,13 +14,6 @@ export const PERSONA =
|
|
|
15
14
|
"You can recall user-managed cold memory across sessions and keep working memory within the current Session, " +
|
|
16
15
|
"manage files in your workspace, and use execute Code Mode for network requests and tool composition.";
|
|
17
16
|
|
|
18
|
-
// Base constraint injected alongside every Agent-specific persona.
|
|
19
|
-
export const SYSTEM_PROMPT =
|
|
20
|
-
"Web development: when writing a website or web page, use browser-ready mode: emit browser-ready static files " +
|
|
21
|
-
"that run directly in Workspace preview. Use ./ or ../ for every Workspace asset, never a root-relative path; " +
|
|
22
|
-
"load CSS from HTML instead of importing it from JavaScript. Resolve third-party ESM with a full pinned HTTPS URL " +
|
|
23
|
-
"or import map to a pinned CORS-enabled CDN. Do not require Vite, Node.js, bundling, or Sandbox at preview time.";
|
|
24
|
-
|
|
25
17
|
// Explains how to reach capabilities instead of listing unsupported substitutes.
|
|
26
18
|
export const RUNTIME =
|
|
27
19
|
"Runtime: this agent runs on Cloudflare Workers. When execute is present, its Code Mode Dynamic Worker is your " +
|
|
@@ -56,7 +48,8 @@ export const TOOLS =
|
|
|
56
48
|
"Tools: When execute is present, call a top-level Tool directly only for a single standalone Tool call or when that Tool is unavailable " +
|
|
57
49
|
"inside execute. Any operation that needs the same Tool more than once, two or more related file, Skill, Extension, MCP, or Host Tool calls, " +
|
|
58
50
|
"or branching or repetition MUST use one execute. Do not make repeated top-level calls for that work. Inside execute, use state.* for " +
|
|
59
|
-
"workspace file operations, tools.* for other host capabilities, a loop for repeated calls, and Promise.all for independent
|
|
51
|
+
"workspace file operations, tools.* for other host capabilities, a loop for repeated calls, and Promise.all for independent calls. " +
|
|
52
|
+
"For materialize_skill_resource, materialize every known set of Skill resources in one execute; never start one execute per resource. " +
|
|
60
53
|
"Tools available only at the top level must stay Direct. " +
|
|
61
54
|
"For web tasks, use web_search for web discovery, current facts, cited research, " +
|
|
62
55
|
"and public URL analysis. For web access specifically, use execute Code Mode only for raw or customized network requests, structured " +
|
|
@@ -75,6 +68,30 @@ export const TOOLS =
|
|
|
75
68
|
"only independent top-level-only Direct Tools should be issued together in one turn. When you reference " +
|
|
76
69
|
"code, cite it as file_path:line_number.";
|
|
77
70
|
|
|
71
|
+
// The browser tool is the only way to observe what a page actually does; its
|
|
72
|
+
// failure mode is returning nothing and looking clean, so the shape is fixed here.
|
|
73
|
+
export const BROWSER =
|
|
74
|
+
"Browser: When browser_execute is present, it is for one thing execute cannot do — what a page actually " +
|
|
75
|
+
"does in a real browser: rendered result, console output, runtime exceptions, failed resource loads, CSP " +
|
|
76
|
+
"blocks, and state after an interaction. Everything else stays with execute: computation, files, and " +
|
|
77
|
+
"ordinary HTTP requests (fetching HTML is execute's job, not the browser's). " +
|
|
78
|
+
"After you write or change a web page in the workspace, open it once with browser_execute before you call " +
|
|
79
|
+
"the work done, and say in your reply what you checked and what you saw. " +
|
|
80
|
+
"The cdp connector is request/response only: it delivers no CDP events. Enabling Runtime/Log/Network and " +
|
|
81
|
+
"then reading cdp.getDebugLog() returns method names with no payload — a page full of errors reads as clean. " +
|
|
82
|
+
"So the fixed shape is: create a target and attach for a sessionId; BEFORE Page.navigate, install a page-side " +
|
|
83
|
+
"collector via Page.addScriptToEvaluateOnNewDocument that buffers console.*, a capture-phase window 'error' " +
|
|
84
|
+
"listener (it catches both uncaught exceptions and failed <script>/<img>/<link> loads), 'unhandledrejection' " +
|
|
85
|
+
"and 'securitypolicyviolation' into one page global; then navigate, poll Runtime.evaluate for " +
|
|
86
|
+
"document.readyState === 'complete' (you cannot await an event), then Runtime.evaluate that global back with " +
|
|
87
|
+
"returnByValue. When visual appearance matters, call Page.captureScreenshot and return " +
|
|
88
|
+
"`{ image: { mimeType: 'image/png', data: screenshot.data }, observations }`; the Runtime projects that image " +
|
|
89
|
+
"back to you, so evaluate the visible hierarchy, spacing, clipping and overlap against the request as well as " +
|
|
90
|
+
"the textual diagnostics. Issue CDP calls sequentially — never Promise.all — because call order is recorded for replay. " +
|
|
91
|
+
"Fix what you find and re-check; when the page comes back clean, stop — do not re-verify a page that is " +
|
|
92
|
+
"already clean. If browser_execute is absent, say plainly that you could not verify the page in a browser " +
|
|
93
|
+
"instead of implying you did.";
|
|
94
|
+
|
|
78
95
|
// Uploaded-file routing prevents lossy markdown conversions from becoming data sources.
|
|
79
96
|
export const FILES =
|
|
80
97
|
"Uploaded files live under /uploads/ in your workspace; convertible formats have a companion " +
|
|
@@ -112,7 +129,7 @@ export const MEMORY =
|
|
|
112
129
|
* @remarks
|
|
113
130
|
* Pi 快照装配在生成每次 Runtime Snapshot 时调用;调用方只替换角色文本,不要重复拼接其他段落。
|
|
114
131
|
*
|
|
115
|
-
*
|
|
132
|
+
* Runtime、行为、规划、Tool、浏览器、文件、交互和 Memory 按固定顺序追加,
|
|
116
133
|
* 因为自定义角色不应重新定义实际 Kernel 没有的能力。
|
|
117
134
|
*
|
|
118
135
|
* 各段保持静态,动态 Skill、Context 和 Memory 由各自模块注入,避免这个可缓存前缀每回合改变。
|
|
@@ -121,12 +138,12 @@ export const MEMORY =
|
|
|
121
138
|
*/
|
|
122
139
|
export function assembleSystemPrompt(base?: string | null): string {
|
|
123
140
|
return [
|
|
124
|
-
SYSTEM_PROMPT,
|
|
125
141
|
base ?? PERSONA,
|
|
126
142
|
RUNTIME,
|
|
127
143
|
BEHAVIOR,
|
|
128
144
|
PLANNING,
|
|
129
145
|
TOOLS,
|
|
146
|
+
BROWSER,
|
|
130
147
|
FILES,
|
|
131
148
|
INTERACTION,
|
|
132
149
|
MEMORY,
|
|
@@ -139,7 +156,7 @@ export function assembleSystemPrompt(base?: string | null): string {
|
|
|
139
156
|
* @remarks
|
|
140
157
|
* Pi SubAgent 在启动有界子任务前调用;调用方应传入该子代理类型自己的 persona。
|
|
141
158
|
*
|
|
142
|
-
* 子代理共享真实执行环境和基本行为,但不获得主 Agent
|
|
159
|
+
* 子代理共享真实执行环境和基本行为,但不获得主 Agent 专属的 Skill、规划、Memory 和用户交互指令,因为这些责任留在主循环。
|
|
143
160
|
*
|
|
144
161
|
* 不要直接改成复用 `assembleSystemPrompt`,否则子代理会收到它没有的 Session 记忆和对话交互责任。
|
|
145
162
|
*
|
|
@@ -21,19 +21,7 @@ function attachmentText(
|
|
|
21
21
|
part: Extract<UIMessage["parts"][number], { type: "file" }>,
|
|
22
22
|
): string {
|
|
23
23
|
const name = part.filename?.trim() || "attachment";
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
const path = part.url.startsWith("/")
|
|
27
|
-
? new URL(part.url, "https://attachment.invalid").pathname
|
|
28
|
-
: new URL(part.url).pathname;
|
|
29
|
-
const leaf = path.split("/").filter(Boolean).at(-1);
|
|
30
|
-
if (/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(leaf ?? "")) {
|
|
31
|
-
ref = leaf!;
|
|
32
|
-
}
|
|
33
|
-
} catch {
|
|
34
|
-
// Preserve the existing canonical form for non-platform file URLs.
|
|
35
|
-
}
|
|
36
|
-
return `[Attachment: ${name} (${part.mediaType}) ${ref ? `ref=${ref}` : part.url}]`;
|
|
24
|
+
return `[Attachment: ${name} (${part.mediaType}) path=${part.url}]`;
|
|
37
25
|
}
|
|
38
26
|
|
|
39
27
|
function userContent(
|
|
@@ -70,6 +70,10 @@ const WRAP_UP_MESSAGE = "You have reached this run's model-turn budget. Stop " +
|
|
|
70
70
|
"completed, what you did not finish, and the exact next step you would " +
|
|
71
71
|
"take. Do not start new work.";
|
|
72
72
|
|
|
73
|
+
const TOOL_FAILURE_WRAP_UP_MESSAGE = "This turn reached its tool failure limit. " +
|
|
74
|
+
"Stop calling tools now. Reply with a final message that explains what " +
|
|
75
|
+
"failed and the exact next step the user should take. Do not start new work.";
|
|
76
|
+
|
|
73
77
|
// 收尾开始后最多再放行几个模型回合。工具已经清空,正常一轮就结束了;
|
|
74
78
|
// 留出余量是给 steering 消息,但不能因此把本片的 CPU 上限一起取消。
|
|
75
79
|
const MAX_WRAP_UP_TURNS = 2;
|
|
@@ -159,12 +163,37 @@ interface PiTurnAdapterOptions {
|
|
|
159
163
|
readonly modelTurnsConsumed: number;
|
|
160
164
|
}
|
|
161
165
|
|
|
166
|
+
/**
|
|
167
|
+
* 收集那些结果已经由工具自己收口的 Tool 名字。
|
|
168
|
+
*
|
|
169
|
+
* @remarks
|
|
170
|
+
* `run` 在建 PiCore 前调用,把结果交给 `projectToolResultsForModel`。
|
|
171
|
+
*
|
|
172
|
+
* `outputBudget: "structure"` 的语义就是“这个结果不需要 Runtime 再替它省”:read
|
|
173
|
+
* 按页返回并在页脚写明覆盖的行号,activate_skill 返回的是模型接下来要照做的指令,
|
|
174
|
+
* Code Mode 的结果由它自己收口。对这些再套一次叶子上限,模型看到的正文会和结果里
|
|
175
|
+
* 写明的范围对不上 —— 页脚说 `lines 1-307`,正文却被从中间挖空。
|
|
176
|
+
*
|
|
177
|
+
* 其余工具仍然照常压缩:这道闸按分类放行,不是整体放宽。
|
|
178
|
+
*/
|
|
179
|
+
function selfBoundedToolNames(
|
|
180
|
+
candidates: readonly PiToolCandidate[],
|
|
181
|
+
): ReadonlySet<string> {
|
|
182
|
+
return new Set(
|
|
183
|
+
candidates
|
|
184
|
+
.filter((candidate) => candidate.outputBudget?.kind === "structure")
|
|
185
|
+
.map((candidate) => candidate.tool.name),
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
162
189
|
function projectToolResultsForModel(
|
|
163
190
|
messages: readonly AgentMessage[],
|
|
191
|
+
selfBounded: ReadonlySet<string>,
|
|
164
192
|
): AgentMessage[] {
|
|
165
193
|
let changed = false;
|
|
166
194
|
const projected = messages.map((message) => {
|
|
167
195
|
if (message.role !== "toolResult") return message;
|
|
196
|
+
if (selfBounded.has(message.toolName)) return message;
|
|
168
197
|
const payload = {
|
|
169
198
|
content: message.content,
|
|
170
199
|
details: message.details,
|
|
@@ -252,6 +281,7 @@ class PiTurnAdapter {
|
|
|
252
281
|
signal?.throwIfAborted();
|
|
253
282
|
const canonicalMessages = await this.opts.canonicalMessages();
|
|
254
283
|
const tools = this.compile(opts.tools);
|
|
284
|
+
const selfBounded = selfBoundedToolNames(opts.tools);
|
|
255
285
|
// `modelTurns` 是跨执行片的累计数,`sliceTurns` 只数本片 —— 前者管任务预算,
|
|
256
286
|
// 后者管 CPU 预算,两个边界互不替代。
|
|
257
287
|
let modelTurns = this.opts.modelTurnsConsumed;
|
|
@@ -274,7 +304,7 @@ class PiTurnAdapter {
|
|
|
274
304
|
transformContext: async (messages, signal) => {
|
|
275
305
|
const ctx = await this.opts.transformContext(messages, signal);
|
|
276
306
|
return transformMessages(
|
|
277
|
-
projectToolResultsForModel(ctx) as Message[],
|
|
307
|
+
projectToolResultsForModel(ctx, selfBounded) as Message[],
|
|
278
308
|
this.opts.pi.model,
|
|
279
309
|
) as AgentMessage[];
|
|
280
310
|
},
|
|
@@ -289,9 +319,13 @@ class PiTurnAdapter {
|
|
|
289
319
|
wrapUpTurns += 1;
|
|
290
320
|
return undefined;
|
|
291
321
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
322
|
+
const toolFailureLimitReached = this.governance.wrapUpRequested();
|
|
323
|
+
if (
|
|
324
|
+
!toolFailureLimitReached &&
|
|
325
|
+
modelTurns < MAX_MODEL_TURNS_PER_SUBMISSION - 1
|
|
326
|
+
) return undefined;
|
|
327
|
+
// 预算见底或工具熔断:清空工具表并注入收尾指令,逼出一条真正的
|
|
328
|
+
// 终止助手消息。直接判失败的话用户拿不到任何交代。
|
|
295
329
|
// 这条指令不进 newMessages,因此不会写进 transcript —— 它是控制指令,不是历史。
|
|
296
330
|
wrappingUp = true;
|
|
297
331
|
return {
|
|
@@ -302,7 +336,12 @@ class PiTurnAdapter {
|
|
|
302
336
|
...context.messages,
|
|
303
337
|
{
|
|
304
338
|
role: "user" as const,
|
|
305
|
-
content: [{
|
|
339
|
+
content: [{
|
|
340
|
+
type: "text" as const,
|
|
341
|
+
text: toolFailureLimitReached
|
|
342
|
+
? TOOL_FAILURE_WRAP_UP_MESSAGE
|
|
343
|
+
: WRAP_UP_MESSAGE,
|
|
344
|
+
}],
|
|
306
345
|
timestamp: Date.now(),
|
|
307
346
|
},
|
|
308
347
|
],
|
package/src/pi/tool/compiler.ts
CHANGED
|
@@ -48,6 +48,8 @@ export interface PiToolCandidate {
|
|
|
48
48
|
readonly tool: AgentTool<any, any>;
|
|
49
49
|
/** Keep this Tool Direct-only instead of also offering it through Code Mode. */
|
|
50
50
|
readonly direct?: true;
|
|
51
|
+
/** Offer this Tool only through Code Mode, never as a top-level Tool. */
|
|
52
|
+
readonly codeExecutionOnly?: true;
|
|
51
53
|
/** @internal Tools also callable through this Code Mode candidate. */
|
|
52
54
|
readonly codeExecutionTools?: readonly PiToolCandidate[];
|
|
53
55
|
/** Conservative maximum used in the stable Runtime descriptor. */
|
|
@@ -85,6 +87,7 @@ const governanceState = Symbol("PiToolGovernanceState");
|
|
|
85
87
|
/** 向 Pi Agent 提供工具后置钩子,并为编译器保留同一 Turn 的治理状态。 */
|
|
86
88
|
export interface PiToolGovernance {
|
|
87
89
|
readonly afterToolCall: NonNullable<AgentOptions["afterToolCall"]>;
|
|
90
|
+
readonly wrapUpRequested: () => boolean;
|
|
88
91
|
readonly [governanceState]: PiToolGovernanceState;
|
|
89
92
|
}
|
|
90
93
|
|
|
@@ -222,7 +225,7 @@ function recordFailure(
|
|
|
222
225
|
|
|
223
226
|
// 在执行前判断这次工具调用是否已超过重试或 Turn 上限。
|
|
224
227
|
// 受治理 execute 方法对每个调用首先调用它。
|
|
225
|
-
// 被阻断的 call id 必须记入 terminalCalls,才能让 Pi
|
|
228
|
+
// 被阻断的 call id 必须记入 terminalCalls,才能让 Pi 在返回错误后进入无工具收尾回合。
|
|
226
229
|
function blockReason(
|
|
227
230
|
state: PiToolGovernanceState | undefined,
|
|
228
231
|
toolCallId: string,
|
|
@@ -263,15 +266,16 @@ export function createPiToolGovernance(): PiToolGovernance {
|
|
|
263
266
|
};
|
|
264
267
|
const governance: PiToolGovernance = {
|
|
265
268
|
[governanceState]: state,
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
+
// Pi Agent 在每次工具执行结束后调用,并传入实际 toolCall id。只消费
|
|
270
|
+
// terminalCalls 中的 id,可避免一次失败误触发收尾;不能在这里直接
|
|
271
|
+
// terminate,否则模型没有机会产出权威 assistant 消息。
|
|
269
272
|
async afterToolCall(context) {
|
|
270
273
|
if (!state.terminalCalls.delete(context.toolCall.id)) {
|
|
271
274
|
return;
|
|
272
275
|
}
|
|
273
|
-
|
|
276
|
+
state.aborted = true;
|
|
274
277
|
},
|
|
278
|
+
wrapUpRequested: () => state.aborted,
|
|
275
279
|
};
|
|
276
280
|
return governance;
|
|
277
281
|
}
|