@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
|
@@ -64,14 +64,137 @@ const workspaceReadParameters = Type.Object({
|
|
|
64
64
|
maxLength: 4_096,
|
|
65
65
|
description: "Absolute Workspace path",
|
|
66
66
|
}),
|
|
67
|
-
offset: Type.Optional(Type.Integer({
|
|
68
|
-
|
|
67
|
+
offset: Type.Optional(Type.Integer({
|
|
68
|
+
minimum: 1,
|
|
69
|
+
description: "1-indexed line to start from; pass the nextOffset of the previous page",
|
|
70
|
+
})),
|
|
71
|
+
limit: Type.Optional(Type.Integer({
|
|
72
|
+
minimum: 1,
|
|
73
|
+
description: "Maximum lines to return; a page is capped by size regardless",
|
|
74
|
+
})),
|
|
69
75
|
});
|
|
70
76
|
const workspaceEditParameters = Type.Object({
|
|
71
77
|
path: Type.String({ minLength: 1, maxLength: 4_096 }),
|
|
72
78
|
old_string: Type.String(),
|
|
73
79
|
new_string: Type.String(),
|
|
74
80
|
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 单页 read 返回的字符上限。
|
|
84
|
+
*
|
|
85
|
+
* 上游 think 的 read 只卡行数(2000)和行宽(2000 字符),二者相乘意味着单次调用
|
|
86
|
+
* 理论上能返回 4 MB。这里补上缺失的总量闸,让一页的大小可预测。
|
|
87
|
+
*
|
|
88
|
+
* MUST ≤ `budget/gate.ts` 的 `STORAGE_LEAF_MAX_CHARS`,否则持久记录会比模型当轮
|
|
89
|
+
* 看到的内容还少;`gate.test.ts` 有断言守着这条关系。
|
|
90
|
+
*/
|
|
91
|
+
export const READ_PAGE_MAX_CHARS = 64 * 1024;
|
|
92
|
+
|
|
93
|
+
/** think 给每一行加的 `${lineNo}\t` 前缀。 */
|
|
94
|
+
const NUMBERED_LINE = /^(\d+)\t/;
|
|
95
|
+
|
|
96
|
+
/** 页脚占用的字符预留量;页脚和正文同属一个字符串叶子,必须一起受预算约束。 */
|
|
97
|
+
const MARKER_RESERVE_CHARS = 256;
|
|
98
|
+
|
|
99
|
+
interface ReadPage {
|
|
100
|
+
readonly content?: unknown;
|
|
101
|
+
readonly totalLines?: unknown;
|
|
102
|
+
readonly fromLine?: unknown;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 把一次 read 收敛成一页,并明确给出下一页的位置。
|
|
107
|
+
*
|
|
108
|
+
* @remarks
|
|
109
|
+
* read 的包装层在拿到上游结果后调用;图片、PDF 和二进制结果原样透传,因为它们
|
|
110
|
+
* 没有行的概念。
|
|
111
|
+
*
|
|
112
|
+
* 行号从内容里回读而不是复用上游的 `toLine`:上游那个字段是按**请求区间**算的,
|
|
113
|
+
* 一旦它自己触发 2000 行截断就会偏大,照抄会让 `nextOffset` 跳过没读到的行。
|
|
114
|
+
*
|
|
115
|
+
* 返回值同时重建模型可见文本和 details,二者必须来自同一份分页结果,否则模型读到
|
|
116
|
+
* 的内容会比 details 记录的多。
|
|
117
|
+
*/
|
|
118
|
+
function pageReadResult(
|
|
119
|
+
output: AgentToolResult<unknown>,
|
|
120
|
+
): AgentToolResult<unknown> {
|
|
121
|
+
const details = output.details;
|
|
122
|
+
if (details === null || typeof details !== "object") return output;
|
|
123
|
+
const page = details as ReadPage;
|
|
124
|
+
if (typeof page.content !== "string") return output;
|
|
125
|
+
|
|
126
|
+
const totalLines = typeof page.totalLines === "number"
|
|
127
|
+
? page.totalLines
|
|
128
|
+
: undefined;
|
|
129
|
+
const rows = page.content.split("\n").filter((row) => NUMBERED_LINE.test(row));
|
|
130
|
+
if (rows.length === 0) {
|
|
131
|
+
// offset 越过文件末尾时上游返回空内容。空白结果没有任何可操作信息,而模型现在
|
|
132
|
+
// 是被要求自己推进 offset 的,不说清楚它只会换个数再试一次。
|
|
133
|
+
const text = `[no lines at that offset` +
|
|
134
|
+
`${totalLines === undefined ? "" : `; the file has ${totalLines} lines`}]`;
|
|
135
|
+
return {
|
|
136
|
+
...output,
|
|
137
|
+
content: [{ type: "text", text }],
|
|
138
|
+
details: { ...details, eof: true, ...(totalLines === undefined ? {} : { totalLines }) },
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 页脚也要算进预算:它和正文一起构成模型看到的那个字符串叶子,漏算就会正好顶穿
|
|
143
|
+
// STORAGE_LEAF_MAX_CHARS,让持久化再从中间挖掉几十个字符。
|
|
144
|
+
const budget = READ_PAGE_MAX_CHARS - MARKER_RESERVE_CHARS;
|
|
145
|
+
let used = 0;
|
|
146
|
+
let kept = 0;
|
|
147
|
+
for (const row of rows) {
|
|
148
|
+
const next = used + row.length + (kept === 0 ? 0 : 1);
|
|
149
|
+
// 至少留一行,否则单行超限的文件会返回空页,模型无从推进。
|
|
150
|
+
if (kept > 0 && next > budget) break;
|
|
151
|
+
used = next;
|
|
152
|
+
kept += 1;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const lastRow = rows[kept - 1] as string;
|
|
156
|
+
const toLine = Number(NUMBERED_LINE.exec(lastRow)?.[1]);
|
|
157
|
+
const eof = totalLines === undefined
|
|
158
|
+
? kept === rows.length
|
|
159
|
+
: toLine >= totalLines;
|
|
160
|
+
// 上游按 2000 字符截断超长行并就地打标。JSON 溢出产物这类“整块内容挤在一行”的
|
|
161
|
+
// 文件会命中它,此时按行分页永远追不回被砍掉的部分 —— 必须说出来,否则模型会
|
|
162
|
+
// 拿着一个 eof 以为自己读全了。
|
|
163
|
+
const lossy = rows.slice(0, kept).some((row) => row.endsWith("... (truncated)"));
|
|
164
|
+
|
|
165
|
+
const content = rows.slice(0, kept).join("\n");
|
|
166
|
+
const fromLine = typeof page.fromLine === "number"
|
|
167
|
+
? page.fromLine
|
|
168
|
+
: Number(NUMBERED_LINE.exec(rows[0] as string)?.[1]);
|
|
169
|
+
|
|
170
|
+
// 分页信息 MUST 进 content,不能只放 details:Provider 把 Tool 结果转成 tool_result
|
|
171
|
+
// 时只带 content,details 是给应用和 UI 的(见 pi-ai 的 `convertToolResult`)。
|
|
172
|
+
// 放错地方,模型就看不见下一页在哪,只能把半个文件当成整个文件用。
|
|
173
|
+
const span = `lines ${fromLine}-${toLine} of ${totalLines ?? toLine}`;
|
|
174
|
+
const lossyNote = lossy
|
|
175
|
+
? " Some lines were longer than 2000 chars and are cut short; " +
|
|
176
|
+
"paging cannot recover them — use grep or bash on this path instead."
|
|
177
|
+
: "";
|
|
178
|
+
const marker = fromLine === 1 && eof && !lossy
|
|
179
|
+
? ""
|
|
180
|
+
: eof
|
|
181
|
+
? `\n\n[${span} — end of file.${lossyNote}]`
|
|
182
|
+
: `\n\n[${span} — more remains. ` +
|
|
183
|
+
`Continue with read(path, offset: ${toLine + 1}).${lossyNote}]`;
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
...output,
|
|
187
|
+
content: [{ type: "text", text: `${content}${marker}` }],
|
|
188
|
+
details: {
|
|
189
|
+
...details,
|
|
190
|
+
content,
|
|
191
|
+
fromLine,
|
|
192
|
+
toLine,
|
|
193
|
+
eof,
|
|
194
|
+
...(eof ? {} : { nextOffset: toLine + 1 }),
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
75
198
|
/**
|
|
76
199
|
* 把模型给的路径收敛成稳定的闸键。
|
|
77
200
|
*
|
|
@@ -174,7 +297,7 @@ export function workspacePiToolCandidates(
|
|
|
174
297
|
);
|
|
175
298
|
const error = (output.details as { error?: unknown })?.error;
|
|
176
299
|
if (typeof error === "string") throw new Error(error);
|
|
177
|
-
return output;
|
|
300
|
+
return pageReadResult(output);
|
|
178
301
|
},
|
|
179
302
|
};
|
|
180
303
|
|
|
@@ -286,7 +409,16 @@ export function workspacePiToolCandidates(
|
|
|
286
409
|
);
|
|
287
410
|
|
|
288
411
|
return [
|
|
289
|
-
|
|
412
|
+
{
|
|
413
|
+
owner: "workspace",
|
|
414
|
+
requiredExecutionLevel: "safe" as const,
|
|
415
|
+
// read 是大输出外置协议的**出口**:溢出结果的提示语指向它。出口自己再外置,
|
|
416
|
+
// 就会变成 read → 新 artifact → read 的死循环,模型只能靠猜逃出去。
|
|
417
|
+
// 分页上限由 `pageReadResult` 自己保证,不需要外置也不会撑爆一轮。
|
|
418
|
+
outputBudget: { kind: "structure" } as const,
|
|
419
|
+
tool: read,
|
|
420
|
+
},
|
|
421
|
+
...[write, edit, list, find, grep, remove].map((tool) => ({
|
|
290
422
|
owner: "workspace",
|
|
291
423
|
requiredExecutionLevel: "safe" as const,
|
|
292
424
|
tool,
|
package/src/runtime-assembler.ts
CHANGED
|
@@ -54,7 +54,8 @@ import {
|
|
|
54
54
|
memoryPiToolCandidate,
|
|
55
55
|
} from "./pi/tool/base";
|
|
56
56
|
import {
|
|
57
|
-
|
|
57
|
+
BROWSER_EXECUTE_TOOL_NAME,
|
|
58
|
+
browserExecutionPiToolCandidate,
|
|
58
59
|
codeExecutionPiToolCandidate,
|
|
59
60
|
} from "./pi/tool/core";
|
|
60
61
|
import { skillPiToolCandidates } from "./pi/tool/skill";
|
|
@@ -65,6 +66,7 @@ import {
|
|
|
65
66
|
normalizeAgentTelemetryBinding,
|
|
66
67
|
type AgentTelemetryBinding,
|
|
67
68
|
} from "./telemetry/contract";
|
|
69
|
+
import { BUILT_IN_RUNTIME_SKILLS } from "./skills";
|
|
68
70
|
|
|
69
71
|
/** Runtime Assembler:校验一份扁平输入并生成可原子提交的 Snapshot。 */
|
|
70
72
|
|
|
@@ -192,15 +194,23 @@ async function createToolSurface(
|
|
|
192
194
|
const visible = (candidate: PiToolCandidate) =>
|
|
193
195
|
!deny.has(candidate.tool.name) &&
|
|
194
196
|
allowsTool?.(candidate.tool.name) !== false;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
+
// 平台没有浏览器能力时注册空集:宁可没有这个 Tool,也不注册一个必然失败的 Tool 误导模型。
|
|
198
|
+
// `create()` 推迟到确认这个名字真的可见之后,被 deny 的装配不白建一个浏览器连接器。
|
|
199
|
+
// `direct` 使它只走 Direct 调用,不再被并进 `execute` 的工具集——两个 Code Mode 类工具互相嵌套
|
|
200
|
+
// 会让「哪次执行被记录、被重放」变得无法解释,而它们的分工本就由 System Prompt 划清。
|
|
201
|
+
const browserVisible = !deny.has(BROWSER_EXECUTE_TOOL_NAME) &&
|
|
202
|
+
allowsTool?.(BROWSER_EXECUTE_TOOL_NAME) !== false;
|
|
203
|
+
const browserCandidates = input.platform.browser && browserVisible
|
|
204
|
+
? [{
|
|
205
|
+
...browserExecutionPiToolCandidate(input.platform.browser.create()),
|
|
206
|
+
direct: true as const,
|
|
207
|
+
}]
|
|
197
208
|
: [];
|
|
198
209
|
const baseCandidates = basePiToolCandidates(input.webSearch);
|
|
199
210
|
const memoryCandidates = input.memory
|
|
200
211
|
? [memoryPiToolCandidate(input.memory.port, input.memory.profile)]
|
|
201
212
|
: [];
|
|
202
213
|
const scriptCandidates = [
|
|
203
|
-
...browserCandidates,
|
|
204
214
|
...input.hostTools,
|
|
205
215
|
...baseCandidates,
|
|
206
216
|
...memoryCandidates,
|
|
@@ -238,6 +248,9 @@ async function createToolSurface(
|
|
|
238
248
|
}
|
|
239
249
|
|
|
240
250
|
const finalized = [...tools.values()];
|
|
251
|
+
const directVisible = finalized.filter(
|
|
252
|
+
(candidate) => !candidate.codeExecutionOnly,
|
|
253
|
+
);
|
|
241
254
|
if (tools.has("execute")) {
|
|
242
255
|
throw new Error("SpringBrand reserved Runtime Tool name: execute");
|
|
243
256
|
}
|
|
@@ -246,7 +259,7 @@ async function createToolSurface(
|
|
|
246
259
|
deny.has("execute") ||
|
|
247
260
|
allowsTool?.("execute") === false
|
|
248
261
|
) {
|
|
249
|
-
return Object.freeze(
|
|
262
|
+
return Object.freeze(directVisible);
|
|
250
263
|
}
|
|
251
264
|
const mergeable = finalized.filter(
|
|
252
265
|
(candidate) =>
|
|
@@ -264,14 +277,22 @@ async function createToolSurface(
|
|
|
264
277
|
),
|
|
265
278
|
codeExecutionTools,
|
|
266
279
|
}),
|
|
267
|
-
...
|
|
280
|
+
...directVisible,
|
|
268
281
|
]);
|
|
269
282
|
},
|
|
270
283
|
}),
|
|
271
284
|
extensions: input.extensions.filter(
|
|
272
285
|
(extension) => allowsExtension?.(extension) !== false,
|
|
273
286
|
),
|
|
274
|
-
|
|
287
|
+
// 浏览器是可选能力:平台没给就只记诊断,不让整个装配失败。
|
|
288
|
+
// 被 policy deny 不算降级——那是有人主动关的,不是平台缺件。
|
|
289
|
+
degradations: input.platform.browser
|
|
290
|
+
? []
|
|
291
|
+
: [{
|
|
292
|
+
capability: "browser" as const,
|
|
293
|
+
reason: "unavailable" as const,
|
|
294
|
+
detail: "browser_binding_missing",
|
|
295
|
+
}],
|
|
275
296
|
};
|
|
276
297
|
}
|
|
277
298
|
|
|
@@ -827,7 +848,10 @@ export async function assembleRuntimeSnapshot<
|
|
|
827
848
|
? { subagents: toolAssembly.bindings.subagents }
|
|
828
849
|
: {}),
|
|
829
850
|
hostTools,
|
|
830
|
-
skills:
|
|
851
|
+
skills:
|
|
852
|
+
input.ctx.role === "primary" && toolAssembly.bindings?.workspace
|
|
853
|
+
? [...BUILT_IN_RUNTIME_SKILLS, ...resources.skills]
|
|
854
|
+
: resources.skills,
|
|
831
855
|
connectors: resources.connectors.servers,
|
|
832
856
|
...(resources.connectors.gateway
|
|
833
857
|
? { gateway: resources.connectors.gateway }
|
package/src/runtime.ts
CHANGED
|
@@ -1076,11 +1076,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
1076
1076
|
const running = this.db.submissions.findRunning() as StoredSubmission | null;
|
|
1077
1077
|
if (running) {
|
|
1078
1078
|
const effect = this.decidePiRecovery(running).effect;
|
|
1079
|
-
if (
|
|
1080
|
-
effect.kind === "wait" &&
|
|
1081
|
-
(effect.reason === "approval" ||
|
|
1082
|
-
effect.reason === "uncertain-tool")
|
|
1083
|
-
) {
|
|
1079
|
+
if (effect.kind === "wait" && effect.reason === "approval") {
|
|
1084
1080
|
return;
|
|
1085
1081
|
}
|
|
1086
1082
|
}
|
|
@@ -1894,7 +1890,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
1894
1890
|
const submitted = await this.submissions.submit(
|
|
1895
1891
|
this.submissionInput(userMessage, options),
|
|
1896
1892
|
);
|
|
1897
|
-
|
|
1893
|
+
this.ctx.waitUntil(this.drainRuntimeEvents());
|
|
1898
1894
|
await this.broadcastApprovals();
|
|
1899
1895
|
return submitted;
|
|
1900
1896
|
}
|
|
@@ -2496,7 +2492,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
2496
2492
|
// - undefined:续跑里程碑已暂存待派发,`dispatchPendingContinuations`
|
|
2497
2493
|
// 会来接手 —— 判它失败等于杀掉一轮本来有人管的 Turn。
|
|
2498
2494
|
// - uncertain-tool:**没有任何人会来解开**。非幂等工具结果不确定,恢复
|
|
2499
|
-
//
|
|
2495
|
+
// 拒绝重放;停在这里就是永久挂死。
|
|
2500
2496
|
return decision.effect.reason === "uncertain-tool"
|
|
2501
2497
|
? {
|
|
2502
2498
|
kind: "unresumable",
|
|
@@ -2772,9 +2768,11 @@ export abstract class AgentRuntimeKernel<
|
|
|
2772
2768
|
submission = await this.activateQueuedSubmission(submission);
|
|
2773
2769
|
}
|
|
2774
2770
|
const recoveryAdapter = this.createSubmissionExecutionAdapter(submission);
|
|
2771
|
+
let awaitingHumanInput = false;
|
|
2775
2772
|
// 让步续跑跳过整套里程碑重放:这一片是主动让出的,transcript 完好、没有半路的
|
|
2776
2773
|
// Tool 要重建。走恢复通道不只是白跑,重放成本还随让步次数增长 —— 第 N 片要
|
|
2777
|
-
// 重放前 N-1
|
|
2774
|
+
// 重放前 N-1 片积累的全部里程碑。但调度回调可能跨过等人的停靠期,所以下面仍需检查
|
|
2775
|
+
// 恢复决策,不能把尾部 assistant Tool Use 直接交给 Pi 续跑。
|
|
2778
2776
|
if (mode === "recovery") {
|
|
2779
2777
|
const deactivatePreparation = this.submissions.activate(submission, {
|
|
2780
2778
|
submissionId,
|
|
@@ -2817,7 +2815,11 @@ export abstract class AgentRuntimeKernel<
|
|
|
2817
2815
|
);
|
|
2818
2816
|
}
|
|
2819
2817
|
} else {
|
|
2820
|
-
await this.materializeRecoveredToolResults(submission);
|
|
2818
|
+
const decision = await this.materializeRecoveredToolResults(submission);
|
|
2819
|
+
awaitingHumanInput = mode === "continuation" &&
|
|
2820
|
+
decision.effect.kind === "wait" &&
|
|
2821
|
+
(decision.effect.reason === "approval" ||
|
|
2822
|
+
decision.effect.reason === "interaction");
|
|
2821
2823
|
}
|
|
2822
2824
|
|
|
2823
2825
|
const ready = this.readSubmission(submissionId);
|
|
@@ -2832,6 +2834,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
2832
2834
|
ready.abortReason,
|
|
2833
2835
|
);
|
|
2834
2836
|
}
|
|
2837
|
+
if (awaitingHumanInput) return ready;
|
|
2835
2838
|
if (ready.status === "pending") {
|
|
2836
2839
|
if (this.db.submissions.transition(submissionId, "running", ["pending"])) {
|
|
2837
2840
|
this.telemetry.capture("turnStarted", {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { fromManifest } from "agents/skills";
|
|
2
|
+
import { SPRINGBRAND_WORKER_WEBSITE_SKILL } from "./springbrand-worker-website";
|
|
3
|
+
|
|
4
|
+
const source = fromManifest({
|
|
5
|
+
id: "springbrand-runtime-built-ins",
|
|
6
|
+
fingerprint: JSON.stringify(SPRINGBRAND_WORKER_WEBSITE_SKILL),
|
|
7
|
+
skills: [SPRINGBRAND_WORKER_WEBSITE_SKILL],
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const BUILT_IN_RUNTIME_SKILLS = Object.freeze([Object.freeze({
|
|
11
|
+
name: SPRINGBRAND_WORKER_WEBSITE_SKILL.name,
|
|
12
|
+
description: SPRINGBRAND_WORKER_WEBSITE_SKILL.description,
|
|
13
|
+
source,
|
|
14
|
+
})]);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export const SPRINGBRAND_WORKER_WEBSITE_SKILL = {
|
|
2
|
+
name: "springbrand-worker-website",
|
|
3
|
+
description:
|
|
4
|
+
"Use when an Agent creates, restructures, previews, verifies, or deploys a Website in a SpringBrand Workspace; provide the complete Cloudflare Worker project layout, asset conventions, and browser validation workflow.",
|
|
5
|
+
body: [
|
|
6
|
+
"# SpringBrand Worker Website",
|
|
7
|
+
"",
|
|
8
|
+
"Keep the Website in one stable project directory under `/creations/<website-name>/`. Treat that directory as the complete deployable Website project, not as a directory containing only one HTML file.",
|
|
9
|
+
"",
|
|
10
|
+
"## Recommended layout",
|
|
11
|
+
"",
|
|
12
|
+
"When creating a new Website and no existing project structure needs to be preserved, prefer:",
|
|
13
|
+
"",
|
|
14
|
+
"```text",
|
|
15
|
+
"/creations/<website-name>/",
|
|
16
|
+
"├── wrangler.json",
|
|
17
|
+
"├── package.json",
|
|
18
|
+
"├── src/",
|
|
19
|
+
"│ ├── worker.ts",
|
|
20
|
+
"│ ├── routes/",
|
|
21
|
+
"│ ├── lib/",
|
|
22
|
+
"│ └── client/",
|
|
23
|
+
"└── public/",
|
|
24
|
+
" ├── index.html",
|
|
25
|
+
" ├── about/",
|
|
26
|
+
" │ └── index.html",
|
|
27
|
+
" ├── products/",
|
|
28
|
+
" │ └── index.html",
|
|
29
|
+
" ├── assets/",
|
|
30
|
+
" │ ├── css/",
|
|
31
|
+
" │ ├── js/",
|
|
32
|
+
" │ ├── images/",
|
|
33
|
+
" │ ├── fonts/",
|
|
34
|
+
" │ └── icons/",
|
|
35
|
+
" ├── data/",
|
|
36
|
+
" ├── favicon.ico",
|
|
37
|
+
" ├── robots.txt",
|
|
38
|
+
" └── manifest.webmanifest",
|
|
39
|
+
"```",
|
|
40
|
+
"",
|
|
41
|
+
"This is a recommended default, not a required layout. Preserve an existing valid framework or project convention such as `dist/`, `build/`, another Worker entry path, or another static asset directory, and configure Wrangler accordingly.",
|
|
42
|
+
"",
|
|
43
|
+
"## Project rules",
|
|
44
|
+
"",
|
|
45
|
+
"- Keep Worker configuration, package metadata, and build configuration at the Website project root.",
|
|
46
|
+
"- Keep Worker code, routes, application logic, and editable source files under `src/`.",
|
|
47
|
+
"- Keep browser-accessible, deployment-ready static files under `public/`.",
|
|
48
|
+
"- Use `public/index.html` as the recommended home page. Put additional pages under clear routes such as `public/about/index.html` and `public/products/index.html`.",
|
|
49
|
+
"- Put stylesheets, browser scripts, images, fonts, icons, and other static resources under the corresponding `public/assets/` subdirectory.",
|
|
50
|
+
"- Put publicly accessible static data under `public/data/` when needed.",
|
|
51
|
+
"- Keep every file required to build and deploy the Website inside its project directory.",
|
|
52
|
+
"- Do not duplicate the same entry page at both the project root and `public/`.",
|
|
53
|
+
"- Do not place secrets, temporary files, logs, raw tool output, or unprocessed attachments inside the Website project.",
|
|
54
|
+
"",
|
|
55
|
+
"## Worker and browser requirements",
|
|
56
|
+
"",
|
|
57
|
+
"- Configure Wrangler `main` to point to the actual Worker source file.",
|
|
58
|
+
"- Export `class App extends DurableObject` from that module when the Website uses the SpringBrand App Facet.",
|
|
59
|
+
"- Configure `assets.directory` to point to the actual browser asset directory, preferably `public/`.",
|
|
60
|
+
"- Use `./` or `../` for local Website files, never a root-relative Workspace path, so preview and deployed URL prefixes both work.",
|
|
61
|
+
"- Load CSS from HTML instead of importing it from browser JavaScript.",
|
|
62
|
+
"- Without a build step, load browser-native third-party ESM through a full pinned HTTPS URL or an import map to a pinned CORS-enabled CDN.",
|
|
63
|
+
"- With a build step, produce deployment-ready assets before preview instead of depending on a development server.",
|
|
64
|
+
"",
|
|
65
|
+
"## Workflow",
|
|
66
|
+
"",
|
|
67
|
+
"1. Inspect the existing Website directory before changing it and preserve valid conventions.",
|
|
68
|
+
"2. Create or update the complete project rather than only an entry HTML file.",
|
|
69
|
+
"3. Build when the project requires a build step.",
|
|
70
|
+
"4. Preview the Website through the available Workspace or Space capability.",
|
|
71
|
+
"5. When `browser_execute` is available, check runtime errors, failed resources, CSP violations, responsive layout, clipping, overlap, and visual hierarchy. Capture a screenshot when visual appearance matters, fix discovered problems, and verify once more.",
|
|
72
|
+
].join("\n"),
|
|
73
|
+
};
|
package/src/tool-registry.ts
CHANGED
|
@@ -1,51 +1,34 @@
|
|
|
1
|
-
import type { ExecutionLevel } from "./lib/execution-level";
|
|
2
1
|
import type {
|
|
3
|
-
|
|
2
|
+
RuntimeCodeExecutionFactory,
|
|
4
3
|
RuntimeMemoryPort,
|
|
5
4
|
RuntimeSubagentPort,
|
|
6
5
|
WorkspacePort,
|
|
7
6
|
} from "./kernel/bindings";
|
|
8
7
|
import type { RuntimeExtensionConfig } from "./kernel/extensions";
|
|
9
8
|
import type { RuntimeDegradation } from "./kernel/degradation";
|
|
9
|
+
import type {
|
|
10
|
+
ToolContext,
|
|
11
|
+
ToolRegistry,
|
|
12
|
+
ToolSpec,
|
|
13
|
+
} from "./kernel/tool-surface";
|
|
10
14
|
import type { PiToolCandidate } from "./pi/tool/compiler";
|
|
11
15
|
import { validateToolArguments } from "@earendil-works/pi-ai";
|
|
12
16
|
|
|
17
|
+
// Tool Surface 的形状类型住在 `kernel/tool-surface.ts`:bindings 也要用它们,放在这里
|
|
18
|
+
// 会让 `bindings → tool-registry → bindings` 成环。这里继续导出是为了保持调用方不变。
|
|
19
|
+
export type {
|
|
20
|
+
RuntimeCodeExecutionFactory,
|
|
21
|
+
ToolContext,
|
|
22
|
+
ToolRegistry,
|
|
23
|
+
ToolSpec,
|
|
24
|
+
};
|
|
25
|
+
|
|
13
26
|
/** Tool Surface 筛选策略(不含 Manifest deny 列表)。 */
|
|
14
27
|
export interface ToolSurfaceSelectionPolicy {
|
|
15
28
|
readonly allowsTool?: (name: string) => boolean;
|
|
16
29
|
readonly allowsExtension?: (extension: RuntimeExtensionConfig) => boolean;
|
|
17
30
|
}
|
|
18
31
|
|
|
19
|
-
/** 模型调用 Tool 时传给 `execute` 的上下文。 */
|
|
20
|
-
export interface ToolContext {
|
|
21
|
-
readonly toolCallId: string;
|
|
22
|
-
readonly signal: AbortSignal;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Definition 作者声明的一个 Tool。 */
|
|
26
|
-
export interface ToolSpec extends Partial<
|
|
27
|
-
Omit<PiToolCandidate, "tool" | "requiredExecutionLevel">
|
|
28
|
-
> {
|
|
29
|
-
readonly label: string;
|
|
30
|
-
readonly description: string;
|
|
31
|
-
readonly parameters: unknown;
|
|
32
|
-
readonly requiredExecutionLevel: ExecutionLevel;
|
|
33
|
-
/** Require a fresh human decision for every call, regardless of execution level. */
|
|
34
|
-
readonly alwaysRequiresApproval?: boolean;
|
|
35
|
-
readonly execute: (
|
|
36
|
-
input: unknown,
|
|
37
|
-
ctx: ToolContext,
|
|
38
|
-
) => Promise<unknown>;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** 一次装配声明的全部 Tool,键即模型可见的工具名。 */
|
|
42
|
-
export type ToolRegistry = Record<string, ToolSpec>;
|
|
43
|
-
|
|
44
|
-
/** 延迟到最终 Tool Surface 完成后再创建 Code Mode 执行能力。 */
|
|
45
|
-
export interface RuntimeCodeExecutionFactory {
|
|
46
|
-
create(tools: ToolRegistry): RuntimeCodeExecutionPort;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
32
|
export function emptyToolRegistry(): ToolRegistry {
|
|
50
33
|
return {};
|
|
51
34
|
}
|