@springbrand/agent-runtime 0.1.3-alpha.0 → 0.1.3-alpha.2
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/db/schema.ts +10 -1
- package/src/db/submission.repo.ts +34 -1
- package/src/index.ts +9 -11
- package/src/kernel/bindings.ts +27 -6
- package/src/kernel/extensions.ts +1 -1
- package/src/kernel/profile.ts +3 -4
- package/src/kernel/recoverable-chat-agent.ts +82 -4
- package/src/kernel/state.ts +4 -0
- package/src/kernel/submission-lifecycle.ts +3 -2
- package/src/layers/orchestration/temporary-agent/runner.ts +0 -65
- package/src/lib/prompt.ts +1 -1
- package/src/pi/assembly/context.ts +1 -3
- package/src/pi/assembly/snapshot.ts +17 -9
- package/src/pi/runtime-adapter/assembly.ts +25 -86
- package/src/pi/runtime-adapter/execution.ts +107 -12
- package/src/pi/runtime-adapter/index.ts +8 -3
- package/src/pi/runtime-adapter/models.ts +221 -29
- package/src/pi/runtime-adapter/transcript.ts +61 -3
- package/src/pi/tool/ai-adapter.ts +58 -1
- package/src/pi/tool/base.ts +112 -2
- package/src/pi/tool/compiler.ts +5 -32
- package/src/pi/tool/core-host.ts +28 -30
- package/src/pi/tool/core.ts +25 -122
- package/src/pi/tool/mcp.ts +2 -2
- package/src/pi/tool/schedule.ts +46 -2
- package/src/pi/tool/skill.ts +112 -420
- package/src/pi/tool/web-fetch.ts +282 -0
- package/src/pi/tool/web-search/api.ts +34 -18
- package/src/pi/tool/workspace-sandbox.ts +92 -258
- package/src/plugins.ts +358 -531
- package/src/runtime.ts +195 -44
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
type FileSystemStateBackend,
|
|
4
|
-
type WorkspaceFsLike,
|
|
5
|
-
} from "@cloudflare/shell";
|
|
6
|
-
import {
|
|
2
|
+
createBashTool,
|
|
7
3
|
createDeleteTool,
|
|
4
|
+
createEditTool,
|
|
8
5
|
createFindTool,
|
|
6
|
+
createGrepTool,
|
|
9
7
|
createListTool,
|
|
8
|
+
createReadTool,
|
|
9
|
+
createWriteTool,
|
|
10
10
|
} from "@cloudflare/think/tools/workspace";
|
|
11
11
|
import type {
|
|
12
12
|
AgentTool,
|
|
@@ -54,15 +54,6 @@ function running(
|
|
|
54
54
|
|
|
55
55
|
// #region Workspace tools
|
|
56
56
|
|
|
57
|
-
const workspaceWriteParameters = Type.Object({
|
|
58
|
-
path: Type.String({
|
|
59
|
-
minLength: 1,
|
|
60
|
-
maxLength: 4_096,
|
|
61
|
-
description: "Absolute Workspace path",
|
|
62
|
-
}),
|
|
63
|
-
content: Type.String({ description: "Content to write" }),
|
|
64
|
-
});
|
|
65
|
-
|
|
66
57
|
const workspaceReadParameters = Type.Object({
|
|
67
58
|
path: Type.String({
|
|
68
59
|
minLength: 1,
|
|
@@ -77,32 +68,6 @@ const workspaceEditParameters = Type.Object({
|
|
|
77
68
|
old_string: Type.String(),
|
|
78
69
|
new_string: Type.String(),
|
|
79
70
|
});
|
|
80
|
-
const workspaceGrepParameters = Type.Object({
|
|
81
|
-
query: Type.String(),
|
|
82
|
-
include: Type.Optional(Type.String({ minLength: 1, maxLength: 4_096 })),
|
|
83
|
-
fixedString: Type.Optional(Type.Boolean()),
|
|
84
|
-
caseSensitive: Type.Optional(Type.Boolean()),
|
|
85
|
-
contextLines: Type.Optional(Type.Integer({ minimum: 0, maximum: 10 })),
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
const MAX_READ_LINES = 2_000;
|
|
89
|
-
const MAX_READ_LINE_CHARS = 2_000;
|
|
90
|
-
const MAX_GREP_MATCHES = 200;
|
|
91
|
-
|
|
92
|
-
// 作用:计算一个非空字符串在文本中出现了多少次。
|
|
93
|
-
// 调用:edit 在精确替换前调用,空 old_string 已由创建文件分支提前处理。
|
|
94
|
-
// 原因:只允许唯一命中可避免模型给出短片段时误改多个位置。
|
|
95
|
-
function countOccurrences(text: string, search: string): number {
|
|
96
|
-
let count = 0;
|
|
97
|
-
let position = 0;
|
|
98
|
-
while (true) {
|
|
99
|
-
const index = text.indexOf(search, position);
|
|
100
|
-
if (index === -1) return count;
|
|
101
|
-
count += 1;
|
|
102
|
-
position = index + 1;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
71
|
/**
|
|
107
72
|
* 把模型给的路径收敛成稳定的闸键。
|
|
108
73
|
*
|
|
@@ -176,164 +141,97 @@ function serializeByPath<T>(
|
|
|
176
141
|
*
|
|
177
142
|
* 本文件中 “Workspace” 指会话作用域的持久文件视图,“Port” 指 Runtime 只依赖
|
|
178
143
|
* 的窄文件接口,“候选工具”指进入 Pi 编译和统一治理前的描述。read、write、
|
|
179
|
-
* edit、
|
|
180
|
-
* 的上游工厂,避免维护第二套同义文件操作。
|
|
144
|
+
* edit、list、find、grep、delete 复用 @cloudflare/think 的上游工厂。
|
|
181
145
|
*
|
|
182
146
|
* `Pi`、`Port` 与“候选工具”等项目核心术语见 `../../index.ts`。
|
|
183
147
|
*/
|
|
184
148
|
export function workspacePiToolCandidates(
|
|
185
149
|
workspace: WorkspacePort,
|
|
186
150
|
): PiToolCandidate[] {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const searchBackend = (): FileSystemStateBackend => {
|
|
193
|
-
backend ??= createWorkspaceStateBackend(
|
|
194
|
-
workspace as unknown as WorkspaceFsLike,
|
|
195
|
-
);
|
|
196
|
-
return backend;
|
|
197
|
-
};
|
|
151
|
+
const readTool = aiToolToPi(
|
|
152
|
+
"read",
|
|
153
|
+
createReadTool({ ops: workspace }),
|
|
154
|
+
{ label: "Read file" },
|
|
155
|
+
);
|
|
198
156
|
const read: AgentTool<typeof workspaceReadParameters> = {
|
|
199
|
-
name:
|
|
200
|
-
label:
|
|
201
|
-
description:
|
|
202
|
-
"Read a Workspace text file with line numbers. Use offset and limit for large files.",
|
|
157
|
+
name: readTool.name,
|
|
158
|
+
label: readTool.label,
|
|
159
|
+
description: readTool.description,
|
|
203
160
|
parameters: workspaceReadParameters,
|
|
204
|
-
|
|
205
|
-
// 调用:Pi 需要查看文件或为后续精确编辑取上下文时调用。
|
|
206
|
-
// 原因:先核对文件类型,再限制行数和单行长度,
|
|
207
|
-
// 避免一次结果挤满模型上下文。
|
|
208
|
-
async execute(_toolCallId, { path, offset, limit }, signal, onUpdate) {
|
|
161
|
+
async execute(toolCallId, params, signal, onUpdate) {
|
|
209
162
|
signal?.throwIfAborted();
|
|
210
163
|
running(onUpdate, "Reading the Workspace file.");
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const lines = content.split("\n");
|
|
221
|
-
const start = (offset ?? 1) - 1;
|
|
222
|
-
const requestedEnd = limit === undefined ? lines.length : start + limit;
|
|
223
|
-
const end = Math.min(requestedEnd, start + MAX_READ_LINES);
|
|
224
|
-
const numbered = lines.slice(start, end).map((line, index) => {
|
|
225
|
-
const visible =
|
|
226
|
-
line.length > MAX_READ_LINE_CHARS
|
|
227
|
-
? `${line.slice(0, MAX_READ_LINE_CHARS)}... (truncated)`
|
|
228
|
-
: line;
|
|
229
|
-
return `${start + index + 1}\t${visible}`;
|
|
230
|
-
});
|
|
231
|
-
return result({
|
|
232
|
-
path,
|
|
233
|
-
content: numbered.join("\n"),
|
|
234
|
-
totalLines: lines.length,
|
|
235
|
-
fromLine: start + 1,
|
|
236
|
-
toLine: Math.min(end, lines.length),
|
|
237
|
-
...(requestedEnd > end ? { truncated: true } : {}),
|
|
238
|
-
});
|
|
164
|
+
const output = await readTool.execute(
|
|
165
|
+
toolCallId,
|
|
166
|
+
params,
|
|
167
|
+
signal,
|
|
168
|
+
onUpdate,
|
|
169
|
+
);
|
|
170
|
+
const error = (output.details as { error?: unknown })?.error;
|
|
171
|
+
if (typeof error === "string") throw new Error(error);
|
|
172
|
+
return output;
|
|
239
173
|
},
|
|
240
174
|
};
|
|
241
175
|
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
176
|
+
const writeTool = aiToolToPi(
|
|
177
|
+
"write",
|
|
178
|
+
createWriteTool({ ops: workspace }),
|
|
179
|
+
{ label: "Write file" },
|
|
180
|
+
);
|
|
181
|
+
const write: AgentTool<any, unknown> = {
|
|
182
|
+
...writeTool,
|
|
183
|
+
async execute(toolCallId, params, signal, onUpdate) {
|
|
184
|
+
const path = (params as { path?: unknown }).path;
|
|
185
|
+
if (typeof path !== "string") {
|
|
186
|
+
return writeTool.execute(toolCallId, params, signal, onUpdate);
|
|
187
|
+
}
|
|
253
188
|
signal?.throwIfAborted();
|
|
254
189
|
running(onUpdate, "Writing the Workspace file.");
|
|
255
190
|
|
|
256
191
|
return serializeByPath(workspace, path, async () => {
|
|
257
192
|
// 排队期间可能已经被取消,拿到闸之后必须重新确认,否则会写一个已放弃的结果。
|
|
258
193
|
signal?.throwIfAborted();
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
return result({
|
|
267
|
-
path,
|
|
268
|
-
bytesWritten: new TextEncoder().encode(content).byteLength,
|
|
269
|
-
lines: content.split("\n").length,
|
|
270
|
-
});
|
|
194
|
+
return writeTool.execute(
|
|
195
|
+
toolCallId,
|
|
196
|
+
params,
|
|
197
|
+
signal,
|
|
198
|
+
onUpdate,
|
|
199
|
+
);
|
|
271
200
|
});
|
|
272
201
|
},
|
|
273
202
|
};
|
|
274
203
|
|
|
204
|
+
const editTool = aiToolToPi(
|
|
205
|
+
"edit",
|
|
206
|
+
createEditTool({ ops: workspace }),
|
|
207
|
+
{ label: "Edit file" },
|
|
208
|
+
);
|
|
275
209
|
const edit: AgentTool<typeof workspaceEditParameters> = {
|
|
276
|
-
name:
|
|
277
|
-
label:
|
|
278
|
-
description:
|
|
279
|
-
"Replace one exact string in a Workspace file. An empty old_string creates a new file.",
|
|
210
|
+
name: editTool.name,
|
|
211
|
+
label: editTool.label,
|
|
212
|
+
description: editTool.description,
|
|
280
213
|
parameters: workspaceEditParameters,
|
|
281
|
-
|
|
282
|
-
// 调用:Pi 已读取文件并能提供足够上下文时调用。
|
|
283
|
-
// 原因:零命中和多命中都拒绝写入,
|
|
284
|
-
// 防止过短片段静默改错位置。
|
|
285
|
-
async execute(
|
|
286
|
-
_toolCallId,
|
|
287
|
-
{ path, old_string, new_string },
|
|
288
|
-
signal,
|
|
289
|
-
onUpdate,
|
|
290
|
-
) {
|
|
214
|
+
async execute(toolCallId, params, signal, onUpdate) {
|
|
291
215
|
signal?.throwIfAborted();
|
|
292
216
|
running(onUpdate, "Editing the Workspace file.");
|
|
293
217
|
// 读-比对-写整段都在闸内:闸只包住最后那次 writeFile 的话,基准内容仍然可能在
|
|
294
218
|
// 比对之后被别的调用换掉,丢写照旧发生。
|
|
295
|
-
return serializeByPath(workspace, path, async () => {
|
|
219
|
+
return serializeByPath(workspace, params.path, async () => {
|
|
296
220
|
signal?.throwIfAborted();
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
path,
|
|
307
|
-
created: true,
|
|
308
|
-
lines: new_string.split("\n").length,
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
if (content === null) {
|
|
312
|
-
throw new Error(`File not found: ${path}`);
|
|
313
|
-
}
|
|
314
|
-
const occurrences = countOccurrences(content, old_string);
|
|
315
|
-
if (occurrences === 0) {
|
|
316
|
-
throw new Error(
|
|
317
|
-
"old_string not found in file. Read the file first and match whitespace exactly.",
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
|
-
if (occurrences > 1) {
|
|
321
|
-
throw new Error(
|
|
322
|
-
`old_string appears ${occurrences} times. Include more surrounding context.`,
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
const next = content.replace(old_string, new_string);
|
|
326
|
-
await workspace.writeFile(path, next);
|
|
327
|
-
return result({
|
|
328
|
-
path,
|
|
329
|
-
replaced: true,
|
|
330
|
-
lines: next.split("\n").length,
|
|
331
|
-
});
|
|
221
|
+
const output = await editTool.execute(
|
|
222
|
+
toolCallId,
|
|
223
|
+
params,
|
|
224
|
+
signal,
|
|
225
|
+
onUpdate,
|
|
226
|
+
);
|
|
227
|
+
const error = (output.details as { error?: unknown })?.error;
|
|
228
|
+
if (typeof error === "string") throw new Error(error);
|
|
229
|
+
return output;
|
|
332
230
|
});
|
|
333
231
|
},
|
|
334
232
|
};
|
|
335
233
|
|
|
336
|
-
// list / find / delete come from the upstream think factories: WorkspacePort
|
|
234
|
+
// list / find / grep / delete come from the upstream think factories: WorkspacePort
|
|
337
235
|
// already satisfies their ops interfaces structurally, and their output shapes
|
|
338
236
|
// match what this file used to build by hand.
|
|
339
237
|
const list = aiToolToPi("list", createListTool({ ops: workspace }), {
|
|
@@ -344,95 +242,9 @@ export function workspacePiToolCandidates(
|
|
|
344
242
|
label: "Find files",
|
|
345
243
|
});
|
|
346
244
|
|
|
347
|
-
const grep:
|
|
348
|
-
name: "grep",
|
|
245
|
+
const grep = aiToolToPi("grep", createGrepTool({ ops: workspace }), {
|
|
349
246
|
label: "Search files",
|
|
350
|
-
|
|
351
|
-
"Search bounded Workspace file contents with a regex or fixed string.",
|
|
352
|
-
parameters: workspaceGrepParameters,
|
|
353
|
-
// 作用:在匹配 glob 的 Workspace 文件中做有上限的文本搜索。
|
|
354
|
-
// 调用:Pi 需要定位定义、调用方或内容片段时调用。
|
|
355
|
-
// 原因:搜索交给 @cloudflare/shell 后端并限制命中数;
|
|
356
|
-
// 正则先在本地检查以保留既有错误契约。
|
|
357
|
-
async execute(
|
|
358
|
-
_toolCallId,
|
|
359
|
-
{
|
|
360
|
-
query,
|
|
361
|
-
include = "**/*",
|
|
362
|
-
fixedString = false,
|
|
363
|
-
caseSensitive = false,
|
|
364
|
-
contextLines = 0,
|
|
365
|
-
},
|
|
366
|
-
signal,
|
|
367
|
-
onUpdate,
|
|
368
|
-
) {
|
|
369
|
-
signal?.throwIfAborted();
|
|
370
|
-
running(onUpdate, "Searching Workspace files.");
|
|
371
|
-
// Reject bad patterns before reaching the backend so the error text stays
|
|
372
|
-
// identical to the previous in-isolate implementation.
|
|
373
|
-
if (!fixedString) {
|
|
374
|
-
try {
|
|
375
|
-
new RegExp(query);
|
|
376
|
-
} catch {
|
|
377
|
-
throw new Error(`Invalid regex: ${query}`);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// TODO(待确认):@cloudflare/shell@0.4.3 的 searchFiles 会先 glob,再逐个
|
|
382
|
-
// 调 readFile;WorkspacePort 跨 DO 时,内容可能仍会经 RPC 回到当前 isolate。
|
|
383
|
-
const hits = await searchBackend().searchFiles(include, query, {
|
|
384
|
-
regex: !fixedString,
|
|
385
|
-
caseSensitive,
|
|
386
|
-
maxMatches: MAX_GREP_MATCHES,
|
|
387
|
-
...(contextLines > 0
|
|
388
|
-
? { contextBefore: contextLines, contextAfter: contextLines }
|
|
389
|
-
: {}),
|
|
390
|
-
});
|
|
391
|
-
signal?.throwIfAborted();
|
|
392
|
-
|
|
393
|
-
const matches: Array<string | {
|
|
394
|
-
file: string;
|
|
395
|
-
line: number;
|
|
396
|
-
context: string;
|
|
397
|
-
}> = [];
|
|
398
|
-
let filesWithMatches = 0;
|
|
399
|
-
|
|
400
|
-
for (const hit of hits) {
|
|
401
|
-
if (matches.length >= MAX_GREP_MATCHES) break;
|
|
402
|
-
if (hit.matches.length > 0) filesWithMatches += 1;
|
|
403
|
-
for (const match of hit.matches) {
|
|
404
|
-
if (matches.length >= MAX_GREP_MATCHES) break;
|
|
405
|
-
if (contextLines === 0) {
|
|
406
|
-
matches.push(`${hit.path}:${match.line}: ${match.lineText}`);
|
|
407
|
-
continue;
|
|
408
|
-
}
|
|
409
|
-
const before = match.beforeLines ?? [];
|
|
410
|
-
const after = match.afterLines ?? [];
|
|
411
|
-
const firstLine = match.line - before.length;
|
|
412
|
-
const context = [...before, match.lineText, ...after]
|
|
413
|
-
.map(
|
|
414
|
-
(line, offset) =>
|
|
415
|
-
`${firstLine + offset === match.line ? ">" : " "} ${
|
|
416
|
-
firstLine + offset
|
|
417
|
-
}\t${line}`,
|
|
418
|
-
)
|
|
419
|
-
.join("\n");
|
|
420
|
-
matches.push({ file: hit.path, line: match.line, context });
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
return result({
|
|
425
|
-
query,
|
|
426
|
-
filesSearched: hits.length,
|
|
427
|
-
filesWithMatches,
|
|
428
|
-
totalMatches: matches.length,
|
|
429
|
-
matches,
|
|
430
|
-
...(matches.length >= MAX_GREP_MATCHES
|
|
431
|
-
? { truncated: true }
|
|
432
|
-
: {}),
|
|
433
|
-
});
|
|
434
|
-
},
|
|
435
|
-
};
|
|
247
|
+
});
|
|
436
248
|
|
|
437
249
|
// delete 来自上游 think 工厂,只能在外面包一层闸。它自己没有读-改-写窗口,但必须与
|
|
438
250
|
// edit 的窗口互斥:否则 edit 读到内容 → delete 删掉文件 → edit 落盘,已删的文件会被
|
|
@@ -456,12 +268,34 @@ export function workspacePiToolCandidates(
|
|
|
456
268
|
},
|
|
457
269
|
};
|
|
458
270
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
271
|
+
const bash = aiToolToPi(
|
|
272
|
+
"bash",
|
|
273
|
+
createBashTool({ ops: workspace }),
|
|
274
|
+
{
|
|
275
|
+
label: "Workspace Bash",
|
|
276
|
+
description:
|
|
277
|
+
"Run a Bash script over Workspace files for shell-style workflows that combine multiple file operations. " +
|
|
278
|
+
"The Workspace is mounted at / and changes are written back. This is a virtual filesystem, not a machine: " +
|
|
279
|
+
"there is no network or system utilities; use execute for fetch and sandbox_exec for system commands.",
|
|
280
|
+
},
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
return [
|
|
284
|
+
...[read, write, edit, list, find, grep, remove].map((tool) => ({
|
|
285
|
+
owner: "workspace",
|
|
286
|
+
authorized: true,
|
|
287
|
+
requiredExecutionLevel: "safe" as const,
|
|
288
|
+
tool,
|
|
289
|
+
})),
|
|
290
|
+
{
|
|
291
|
+
owner: "workspace",
|
|
292
|
+
authorized: true,
|
|
293
|
+
requiredExecutionLevel: "high" as const,
|
|
294
|
+
summary: "Run a Bash script over Workspace files",
|
|
295
|
+
source: "action" as const,
|
|
296
|
+
tool: bash,
|
|
297
|
+
},
|
|
298
|
+
];
|
|
465
299
|
}
|
|
466
300
|
|
|
467
301
|
// #endregion
|