@springbrand/agent-runtime 0.1.3-alpha.1 → 0.1.3-alpha.3
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 +1 -0
- package/src/kernel/bindings.ts +1 -3
- 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/lib/prompt.ts +1 -1
- package/src/lib/telemetry-dev.ts +7 -4
- package/src/pi/assembly/snapshot.ts +5 -2
- package/src/pi/runtime-adapter/assembly.ts +9 -20
- package/src/pi/runtime-adapter/execution.ts +89 -6
- package/src/pi/runtime-adapter/index.ts +9 -3
- package/src/pi/runtime-adapter/models.ts +373 -35
- 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/core-host.ts +19 -24
- package/src/pi/tool/core.ts +18 -118
- 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 +79 -42
- package/src/runtime.ts +201 -40
package/src/pi/tool/base.ts
CHANGED
|
@@ -2,7 +2,10 @@ import type {
|
|
|
2
2
|
AgentTool,
|
|
3
3
|
AgentToolResult,
|
|
4
4
|
} from "@earendil-works/pi-agent-core";
|
|
5
|
-
import { Type, type TSchema } from "@earendil-works/pi-ai";
|
|
5
|
+
import { Type, type Static, type TSchema } from "@earendil-works/pi-ai";
|
|
6
|
+
import { estimateStringTokens } from "agents/experimental/memory/utils";
|
|
7
|
+
import type { RuntimeMemoryPort } from "../../kernel/bindings";
|
|
8
|
+
import type { RuntimeMemoryProfile } from "../../kernel/profile";
|
|
6
9
|
import { serializeOutput } from "../../lib/artifacts";
|
|
7
10
|
import type { PiToolCandidate } from "./compiler";
|
|
8
11
|
import { webSearchPiToolCandidate } from "./web-search";
|
|
@@ -42,13 +45,63 @@ const updatePlanParameters = Type.Object({
|
|
|
42
45
|
Type.Literal("pending"),
|
|
43
46
|
Type.Literal("in_progress"),
|
|
44
47
|
Type.Literal("done"),
|
|
45
|
-
], {
|
|
48
|
+
], {
|
|
49
|
+
description:
|
|
50
|
+
'Current status. Use "done" for a finished step; never use "completed".',
|
|
51
|
+
}),
|
|
46
52
|
}), {
|
|
47
53
|
description:
|
|
48
54
|
"The complete, ordered plan. Overwrites any previously reported plan.",
|
|
49
55
|
}),
|
|
50
56
|
});
|
|
51
57
|
|
|
58
|
+
type UpdatePlanArguments = Static<typeof updatePlanParameters>;
|
|
59
|
+
|
|
60
|
+
function normalizeStepStatus(
|
|
61
|
+
status: unknown,
|
|
62
|
+
): UpdatePlanArguments["steps"][number]["status"] {
|
|
63
|
+
switch (status) {
|
|
64
|
+
case "done":
|
|
65
|
+
case "completed": // Anthropic models sometimes output "completed"
|
|
66
|
+
return "done";
|
|
67
|
+
case "in_progress":
|
|
68
|
+
case "inProgress": // camelCase variant
|
|
69
|
+
return "in_progress";
|
|
70
|
+
default: // covers "pending", "todo" (Google), and any other unknown value
|
|
71
|
+
return "pending";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function normalizeUpdatePlanArguments(
|
|
76
|
+
input: unknown,
|
|
77
|
+
): UpdatePlanArguments {
|
|
78
|
+
if (input === null || typeof input !== "object") {
|
|
79
|
+
return input as UpdatePlanArguments;
|
|
80
|
+
}
|
|
81
|
+
const value = input as Record<string, unknown>;
|
|
82
|
+
if (!Array.isArray(value.steps)) return input as UpdatePlanArguments;
|
|
83
|
+
return {
|
|
84
|
+
...value,
|
|
85
|
+
steps: value.steps.map((step) => {
|
|
86
|
+
if (step === null || typeof step !== "object") return step;
|
|
87
|
+
const s = step as Record<string, unknown>;
|
|
88
|
+
return { ...s, status: normalizeStepStatus(s.status) };
|
|
89
|
+
}),
|
|
90
|
+
} as UpdatePlanArguments;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const setContextParameters = Type.Object({
|
|
94
|
+
label: Type.Union([
|
|
95
|
+
Type.Literal("memory"),
|
|
96
|
+
Type.Literal("preferences"),
|
|
97
|
+
]),
|
|
98
|
+
content: Type.String(),
|
|
99
|
+
action: Type.Optional(Type.Union([
|
|
100
|
+
Type.Literal("replace"),
|
|
101
|
+
Type.Literal("append"),
|
|
102
|
+
])),
|
|
103
|
+
});
|
|
104
|
+
|
|
52
105
|
function result<T>(details: T): AgentToolResult<T> {
|
|
53
106
|
return {
|
|
54
107
|
content: [{ type: "text", text: serializeOutput(details).text }],
|
|
@@ -97,6 +150,7 @@ export function basePiToolCandidates(
|
|
|
97
150
|
description:
|
|
98
151
|
"Maintain the user-visible plan for the current task. Call this whenever a task involves 2 or more steps, and again every time the plan or a step's status changes. Always pass the FULL plan — it replaces the previous plan entirely (idempotent overwrite), so omitted steps disappear.",
|
|
99
152
|
parameters: updatePlanParameters,
|
|
153
|
+
prepareArguments: normalizeUpdatePlanArguments,
|
|
100
154
|
async execute(_toolCallId, input) {
|
|
101
155
|
return result({
|
|
102
156
|
ok: true,
|
|
@@ -108,3 +162,59 @@ export function basePiToolCandidates(
|
|
|
108
162
|
...(webSearch ? [webSearchPiToolCandidate(webSearch)] : []),
|
|
109
163
|
];
|
|
110
164
|
}
|
|
165
|
+
|
|
166
|
+
/** Writable hot-memory Tool restored from Think Session context blocks. */
|
|
167
|
+
export function memoryPiToolCandidate(
|
|
168
|
+
memory: RuntimeMemoryPort,
|
|
169
|
+
profile: Pick<
|
|
170
|
+
RuntimeMemoryProfile,
|
|
171
|
+
"memoryTokens" | "preferencesTokens"
|
|
172
|
+
>,
|
|
173
|
+
): PiToolCandidate {
|
|
174
|
+
const queues = new Map<string, Promise<void>>();
|
|
175
|
+
const tool: AgentTool<typeof setContextParameters> = {
|
|
176
|
+
name: "set_context",
|
|
177
|
+
label: "Set context",
|
|
178
|
+
description:
|
|
179
|
+
"Replace or append durable working context. Use memory for facts and active context; use preferences for tone, format, tools, and workflow preferences.",
|
|
180
|
+
parameters: setContextParameters,
|
|
181
|
+
async execute(_toolCallId, { label, content, action = "replace" }) {
|
|
182
|
+
const run = async () => {
|
|
183
|
+
const existing = action === "append"
|
|
184
|
+
? await memory.get(label) ?? ""
|
|
185
|
+
: "";
|
|
186
|
+
const separator = existing && !content.startsWith("\n") ? "\n" : "";
|
|
187
|
+
const updated = `${existing}${separator}${content}`;
|
|
188
|
+
const maxTokens = label === "memory"
|
|
189
|
+
? profile.memoryTokens
|
|
190
|
+
: profile.preferencesTokens;
|
|
191
|
+
const tokens = estimateStringTokens(updated);
|
|
192
|
+
if (tokens > maxTokens) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
`Block "${label}" exceeds maxTokens: ${tokens} > ${maxTokens}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
await memory.set(label, updated);
|
|
198
|
+
return result({ label, action, tokens, maxTokens });
|
|
199
|
+
};
|
|
200
|
+
const tail = queues.get(label) ?? Promise.resolve();
|
|
201
|
+
const output = tail.then(run);
|
|
202
|
+
const next = output.then(
|
|
203
|
+
() => {},
|
|
204
|
+
() => {},
|
|
205
|
+
);
|
|
206
|
+
queues.set(label, next);
|
|
207
|
+
void next.then(() => {
|
|
208
|
+
if (queues.get(label) === next) queues.delete(label);
|
|
209
|
+
});
|
|
210
|
+
return output;
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
return {
|
|
214
|
+
owner: "runtime-memory",
|
|
215
|
+
authorized: true,
|
|
216
|
+
requiredExecutionLevel: "safe",
|
|
217
|
+
source: "action",
|
|
218
|
+
tool,
|
|
219
|
+
};
|
|
220
|
+
}
|
package/src/pi/tool/core-host.ts
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createCodemodeRuntime,
|
|
3
|
-
DynamicWorkerExecutor,
|
|
4
|
-
truncateResult,
|
|
5
|
-
} from "@cloudflare/codemode";
|
|
6
1
|
import {
|
|
7
2
|
createWorkspaceStateBackend,
|
|
8
3
|
type WorkspaceFsLike,
|
|
9
4
|
} from "@cloudflare/shell";
|
|
10
|
-
import {
|
|
5
|
+
import { createExecuteRuntime } from "@cloudflare/think/tools/execute";
|
|
11
6
|
import type {
|
|
12
7
|
RuntimeCodeExecutionPort,
|
|
13
8
|
WorkspacePort,
|
|
@@ -22,7 +17,7 @@ const CODEMODE_SANDBOX_TIMEOUT_MS = 55_000;
|
|
|
22
17
|
*
|
|
23
18
|
* Worker 宿主在具备 Durable Object state 和完整平台绑定时调用,然后把返回值交给 Runtime 工具组装。
|
|
24
19
|
*
|
|
25
|
-
*
|
|
20
|
+
* Think 的独立 execute factory 接受显式宿主参数,不要求 Agent 继承 Think;这里只借它组装 Codemode Runtime、Dynamic Worker executor 和已限定范围的 Workspace state connector。
|
|
26
21
|
*/
|
|
27
22
|
export function createWorkspaceCodeExecutionPort(options: {
|
|
28
23
|
readonly ctx: DurableObjectState;
|
|
@@ -30,26 +25,26 @@ export function createWorkspaceCodeExecutionPort(options: {
|
|
|
30
25
|
readonly outbound: Fetcher;
|
|
31
26
|
readonly workspace: WorkspacePort;
|
|
32
27
|
}): RuntimeCodeExecutionPort {
|
|
33
|
-
const
|
|
28
|
+
const { tool } = createExecuteRuntime({
|
|
34
29
|
ctx: options.ctx,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
new StateConnector(
|
|
43
|
-
options.ctx,
|
|
44
|
-
createWorkspaceStateBackend(
|
|
45
|
-
options.workspace as unknown as WorkspaceFsLike,
|
|
46
|
-
),
|
|
47
|
-
),
|
|
48
|
-
],
|
|
30
|
+
loader: options.loader,
|
|
31
|
+
globalOutbound: options.outbound,
|
|
32
|
+
// 先于外层 60s 截止结束,给 Runtime RPC 结算和 Worker 释放留出时间。
|
|
33
|
+
timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
|
|
34
|
+
state: createWorkspaceStateBackend(
|
|
35
|
+
options.workspace as unknown as WorkspaceFsLike,
|
|
36
|
+
),
|
|
49
37
|
name: "execute",
|
|
50
|
-
transformResult: truncateResult,
|
|
51
38
|
});
|
|
39
|
+
const execute = tool.execute;
|
|
40
|
+
if (typeof execute !== "function") {
|
|
41
|
+
throw new Error("Think createExecuteRuntime returned a non-executable tool");
|
|
42
|
+
}
|
|
52
43
|
return {
|
|
53
|
-
execute: (input) =>
|
|
44
|
+
execute: (input) => Promise.resolve(execute(input, {
|
|
45
|
+
toolCallId: "execute",
|
|
46
|
+
messages: [],
|
|
47
|
+
context: undefined,
|
|
48
|
+
})),
|
|
54
49
|
};
|
|
55
50
|
}
|
package/src/pi/tool/core.ts
CHANGED
|
@@ -1,21 +1,16 @@
|
|
|
1
|
+
import { createQuickActionTools } from "@cloudflare/think/tools/browser";
|
|
1
2
|
import type {
|
|
2
3
|
AgentTool,
|
|
3
4
|
AgentToolResult,
|
|
4
5
|
} from "@earendil-works/pi-agent-core";
|
|
5
6
|
import { Type } from "@earendil-works/pi-ai";
|
|
6
|
-
import {
|
|
7
|
-
browserExtract,
|
|
8
|
-
browserLinks,
|
|
9
|
-
browserMarkdown,
|
|
10
|
-
browserScrape,
|
|
11
|
-
type QuickActionPage,
|
|
12
|
-
} from "agents/browser";
|
|
13
7
|
import type {
|
|
14
8
|
RuntimeBrowserPort,
|
|
15
9
|
RuntimeCodeExecutionPort,
|
|
16
10
|
} from "../../kernel/bindings";
|
|
17
11
|
import { serializeOutput } from "../../lib/artifacts";
|
|
18
12
|
import type { PiLoadedExtension } from "../assembly/extensions";
|
|
13
|
+
import { aiToolToPi } from "./ai-adapter";
|
|
19
14
|
import type { PiToolCandidate } from "./compiler";
|
|
20
15
|
|
|
21
16
|
// 本文件沿用 `../../index.ts` 入口定义的 Extension、Port 和 Tool Candidate 术语。
|
|
@@ -32,128 +27,33 @@ function result(details: unknown): AgentToolResult<unknown> {
|
|
|
32
27
|
|
|
33
28
|
// #region Browser Quick Actions
|
|
34
29
|
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
const BROWSER_TOOL_LABELS: Readonly<Record<string, string>> = {
|
|
31
|
+
browser_markdown: "Read web page",
|
|
32
|
+
browser_extract: "Extract web data",
|
|
33
|
+
browser_links: "List web links",
|
|
34
|
+
browser_scrape: "Scrape web elements",
|
|
38
35
|
};
|
|
39
|
-
const browserPageParameters = Type.Object(pageParameters);
|
|
40
|
-
const browserExtractParameters = Type.Object({
|
|
41
|
-
...pageParameters,
|
|
42
|
-
prompt: Type.Optional(Type.String({ minLength: 1 })),
|
|
43
|
-
schema: Type.Optional(Type.Unknown()),
|
|
44
|
-
});
|
|
45
|
-
const browserScrapeParameters = Type.Object({
|
|
46
|
-
...pageParameters,
|
|
47
|
-
selectors: Type.Array(Type.String({ minLength: 1 }), {
|
|
48
|
-
minItems: 1,
|
|
49
|
-
}),
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// 把工具输入收窄为 Browser Quick Action 接受的页面来源。
|
|
53
|
-
// 四个 Browser Quick Action execute 方法在调用 Agents SDK 前共用它。
|
|
54
|
-
// 实际运行时再拒绝空输入,而同时给出 url 和 html 时优先 url,不能把不完整来源传给宿主绑定。
|
|
55
|
-
function page(input: {
|
|
56
|
-
url?: string;
|
|
57
|
-
html?: string;
|
|
58
|
-
}): QuickActionPage {
|
|
59
|
-
if (input.url) return { url: input.url };
|
|
60
|
-
if (input.html) return { html: input.html };
|
|
61
|
-
throw new Error("Provide either 'url' or 'html'");
|
|
62
|
-
}
|
|
63
36
|
|
|
64
37
|
/**
|
|
65
38
|
* 为 Cloudflare Browser Run 的四个一次性 Quick Action 创建 Pi 工具候选项。
|
|
66
39
|
*
|
|
67
40
|
* Runtime 在宿主提供 Browser port 时调用,模型分别用它读取 Markdown、抽取数据、列出链接或按选择器抓取。
|
|
68
41
|
*
|
|
69
|
-
* Cloudflare 官方将 Quick Actions 定位为只需 browser binding
|
|
42
|
+
* Cloudflare 官方将 Quick Actions 定位为只需 browser binding 的无状态单次操作,这里直接复用官方工厂。
|
|
70
43
|
*/
|
|
71
44
|
export function browserQuickActionPiToolCandidates(
|
|
72
45
|
browser: RuntimeBrowserPort,
|
|
73
46
|
): PiToolCandidate[] {
|
|
74
|
-
|
|
75
|
-
name
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
signal?.throwIfAborted();
|
|
85
|
-
return result(await browserMarkdown(browser, page(input)));
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
const extract: AgentTool<typeof browserExtractParameters> = {
|
|
89
|
-
name: "browser_extract",
|
|
90
|
-
label: "Extract web data",
|
|
91
|
-
description:
|
|
92
|
-
"Extract structured data from a web page using AI. Describe what you want in 'prompt'. Passing a JSON Schema in 'schema' is strongly recommended — without one the extractor often fails to produce JSON.",
|
|
93
|
-
parameters: browserExtractParameters,
|
|
94
|
-
// 按自然语言提示或 JSON Schema 从页面抽取数据。
|
|
95
|
-
// Pi 工具循环在模型选择 `browser_extract` 时调用,至少需要 prompt 或 schema 之一。
|
|
96
|
-
// schema 必须转成 Browser Run 期望的 `json_schema` response_format,而空抽取要求必须在发起请求前失败。
|
|
97
|
-
async execute(
|
|
98
|
-
_toolCallId,
|
|
99
|
-
{ prompt, schema, ...input },
|
|
100
|
-
signal,
|
|
101
|
-
) {
|
|
102
|
-
signal?.throwIfAborted();
|
|
103
|
-
if (!prompt && schema === undefined) {
|
|
104
|
-
throw new Error("Provide either 'prompt' or 'schema'");
|
|
105
|
-
}
|
|
106
|
-
return result(await browserExtract(browser, {
|
|
107
|
-
...page(input),
|
|
108
|
-
prompt,
|
|
109
|
-
response_format: schema === undefined
|
|
110
|
-
? undefined
|
|
111
|
-
: { type: "json_schema", schema },
|
|
112
|
-
}));
|
|
113
|
-
},
|
|
114
|
-
};
|
|
115
|
-
const links: AgentTool<typeof browserPageParameters> = {
|
|
116
|
-
name: "browser_links",
|
|
117
|
-
label: "List web links",
|
|
118
|
-
description:
|
|
119
|
-
"Return every link found on a web page (including ones not visible). Useful for discovering pages to follow.",
|
|
120
|
-
parameters: browserPageParameters,
|
|
121
|
-
// 列出 URL 或 HTML 页面中的链接。
|
|
122
|
-
// Pi 工具循环在模型选择 `browser_links` 时调用,调用前允许 Turn 取消。
|
|
123
|
-
// 保留 Agents SDK 的原始结果形状,让统一 result 边界负责文本投影而不在此处二次整形。
|
|
124
|
-
async execute(_toolCallId, input, signal) {
|
|
125
|
-
signal?.throwIfAborted();
|
|
126
|
-
return result(await browserLinks(browser, page(input)));
|
|
127
|
-
},
|
|
128
|
-
};
|
|
129
|
-
const scrape: AgentTool<typeof browserScrapeParameters> = {
|
|
130
|
-
name: "browser_scrape",
|
|
131
|
-
label: "Scrape web elements",
|
|
132
|
-
description:
|
|
133
|
-
"Scrape specific elements from a web page by CSS selector. Returns the matched elements' text, HTML, and attributes.",
|
|
134
|
-
parameters: browserScrapeParameters,
|
|
135
|
-
// 按 CSS 选择器抓取 URL 或 HTML 页面的指定元素。
|
|
136
|
-
// Pi 工具循环在模型选择 `browser_scrape` 时调用,并把简化的字符串列表传入。
|
|
137
|
-
// Browser Run 需要 `{ selector }` 对象列表,因此这个适配只在边界做一次形状转换。
|
|
138
|
-
async execute(
|
|
139
|
-
_toolCallId,
|
|
140
|
-
{ selectors, ...input },
|
|
141
|
-
signal,
|
|
142
|
-
) {
|
|
143
|
-
signal?.throwIfAborted();
|
|
144
|
-
return result(await browserScrape(browser, {
|
|
145
|
-
...page(input),
|
|
146
|
-
elements: selectors.map((selector) => ({ selector })),
|
|
147
|
-
}));
|
|
148
|
-
},
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
return [markdown, extract, links, scrape].map((tool) => ({
|
|
152
|
-
owner: "core:browser",
|
|
153
|
-
authorized: true,
|
|
154
|
-
requiredExecutionLevel: "low",
|
|
155
|
-
tool,
|
|
156
|
-
}));
|
|
47
|
+
return Object.entries(createQuickActionTools({ browser })).map(
|
|
48
|
+
([name, tool]) => ({
|
|
49
|
+
owner: "core:browser",
|
|
50
|
+
authorized: true,
|
|
51
|
+
requiredExecutionLevel: "low",
|
|
52
|
+
tool: aiToolToPi(name, tool, {
|
|
53
|
+
label: BROWSER_TOOL_LABELS[name] ?? name,
|
|
54
|
+
}),
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
157
57
|
}
|
|
158
58
|
|
|
159
59
|
// #endregion
|