@springbrand/agent-runtime 0.1.3-alpha.1 → 0.1.3-alpha.10

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.
Files changed (76) hide show
  1. package/package.json +12 -3
  2. package/src/adapter/cloudflare/index.ts +56 -0
  3. package/src/adapter/cloudflare/resources/runtime-resources.ts +89 -0
  4. package/src/adapter/cloudflare/sandbox/adapter.ts +1513 -0
  5. package/src/adapter/cloudflare/sandbox/id.ts +23 -0
  6. package/src/adapter/cloudflare/sandbox/policy.ts +15 -0
  7. package/src/adapter/cloudflare/subagent/definition.ts +574 -0
  8. package/src/adapter/cloudflare/subagent/runner.ts +175 -0
  9. package/src/adapter/cloudflare/subagent/tools.ts +254 -0
  10. package/src/adapter/cloudflare/universal-agent/hooks.ts +35 -0
  11. package/src/adapter/cloudflare/universal-agent/preparation.ts +277 -0
  12. package/src/adapter/cloudflare/universal-agent/tools.ts +80 -0
  13. package/src/adapter/cloudflare/workspace/git-fs.ts +178 -0
  14. package/src/adapter/cloudflare/workspace/publisher.ts +31 -0
  15. package/src/adapter/cloudflare/workspace/scoped-workspace.ts +376 -0
  16. package/src/adapter/cloudflare/workspace/version-control.ts +374 -0
  17. package/src/agent-tool-runtime.ts +152 -0
  18. package/src/db/agent-tool.repo.ts +27 -0
  19. package/src/db/index.ts +33 -0
  20. package/src/db/interaction.repo.ts +185 -0
  21. package/src/db/schema.ts +25 -1
  22. package/src/db/submission.repo.ts +63 -1
  23. package/src/index.ts +57 -21
  24. package/src/kernel/approval-lifecycle.ts +41 -6
  25. package/src/kernel/bindings.ts +73 -9
  26. package/src/kernel/interaction-lifecycle.ts +395 -0
  27. package/src/kernel/public-contracts.ts +2 -0
  28. package/src/kernel/recoverable-chat-agent.ts +104 -6
  29. package/src/kernel/runtime-assembly-view.ts +37 -0
  30. package/src/kernel/runtime-assembly.ts +41 -0
  31. package/src/kernel/runtime-config.ts +4 -0
  32. package/src/kernel/runtime-load.ts +191 -0
  33. package/src/kernel/state.ts +12 -1
  34. package/src/kernel/submission-lifecycle.ts +33 -2
  35. package/src/layers/orchestration/temporary-agent/core.ts +12 -1
  36. package/src/layers/orchestration/temporary-agent/runner.ts +1 -2
  37. package/src/lib/mcp.ts +7 -3
  38. package/src/lib/prompt.ts +1 -1
  39. package/src/lib/telemetry-dev.ts +7 -4
  40. package/src/pi/assembly/context.ts +3 -3
  41. package/src/pi/assembly/extensions.ts +11 -22
  42. package/src/pi/assembly/snapshot.ts +6 -3
  43. package/src/pi/message/contract.ts +7 -0
  44. package/src/pi/message/conversion.ts +9 -1
  45. package/src/pi/runtime-adapter/assembly.ts +26 -31
  46. package/src/pi/runtime-adapter/execution.ts +198 -15
  47. package/src/pi/runtime-adapter/index.ts +24 -8
  48. package/src/pi/runtime-adapter/models.ts +382 -35
  49. package/src/pi/runtime-adapter/recovery.ts +188 -1
  50. package/src/pi/runtime-adapter/transcript.ts +61 -3
  51. package/src/pi/tool/ai-adapter.ts +58 -1
  52. package/src/pi/tool/base.ts +190 -12
  53. package/src/pi/tool/compiler.ts +34 -1
  54. package/src/pi/tool/core-host.ts +19 -24
  55. package/src/pi/tool/core.ts +30 -120
  56. package/src/pi/tool/gateway.ts +54 -0
  57. package/src/pi/tool/index.ts +2 -0
  58. package/src/pi/tool/mcp.ts +96 -68
  59. package/src/pi/tool/schedule.ts +41 -19
  60. package/src/pi/tool/skill.ts +126 -420
  61. package/src/pi/tool/subagent.ts +14 -2
  62. package/src/pi/tool/web-fetch.ts +281 -0
  63. package/src/pi/tool/web-search/api.ts +34 -18
  64. package/src/pi/tool/web-search/web-search.ts +0 -1
  65. package/src/pi/tool/workspace-revision.ts +64 -0
  66. package/src/pi/tool/workspace-sandbox.ts +105 -263
  67. package/src/pi/turn/index.ts +20 -0
  68. package/src/pi/turn/interaction.ts +181 -0
  69. package/src/pi/turn/tool-recovery.ts +244 -1
  70. package/src/runtime-agent-context.ts +112 -0
  71. package/src/runtime-agent.ts +568 -321
  72. package/src/{plugins.ts → runtime-assembler.ts} +372 -398
  73. package/src/runtime-definition.ts +175 -0
  74. package/src/runtime.ts +835 -204
  75. package/src/tool-registry.ts +143 -0
  76. package/src/workspace-versioning.ts +46 -0
@@ -0,0 +1,374 @@
1
+ import * as git from "isomorphic-git";
2
+ import type { WorkspacePort } from "../../../kernel/bindings";
3
+ import {
4
+ WorkspaceRevisionNotFoundError,
5
+ type WorkspaceRevision,
6
+ type WorkspaceRevisionManifest,
7
+ type WorkspaceVersionPort,
8
+ } from "../../../workspace-versioning";
9
+ import { createWorkspaceGitFs } from "./git-fs";
10
+
11
+ const PAGE_SIZE = 256;
12
+ const MAX_LIST_LIMIT = 100;
13
+ const REVISION_PATTERN = /^[0-9a-f]{40}$/u;
14
+ const AUTHOR = Object.freeze({
15
+ name: "Workspace",
16
+ email: "workspace@springbrand.local",
17
+ });
18
+
19
+ type VersionControlOptions = {
20
+ workspace: WorkspacePort;
21
+ dir: string;
22
+ gitdir: string;
23
+ };
24
+
25
+ type TreeFile = {
26
+ path: string;
27
+ oid: string;
28
+ bytes: Uint8Array;
29
+ };
30
+
31
+ function coordinate(value: string): string {
32
+ if (!value.startsWith("/") || value.includes("\\")) {
33
+ throw new Error("Workspace revision coordinates must be absolute POSIX paths");
34
+ }
35
+ const parts = value.split("/").filter(Boolean);
36
+ if (parts.some((part) => part === "." || part === "..")) {
37
+ throw new Error("Workspace revision coordinates cannot traverse");
38
+ }
39
+ return `/${parts.join("/")}`;
40
+ }
41
+
42
+ function relativePath(value: string): string {
43
+ if (!value || value.startsWith("/") || value.includes("\\")) {
44
+ throw new WorkspaceRevisionNotFoundError();
45
+ }
46
+ const parts = value.split("/");
47
+ if (parts.some((part) => !part || part === "." || part === ".." || part === ".git")) {
48
+ throw new WorkspaceRevisionNotFoundError();
49
+ }
50
+ return parts.join("/");
51
+ }
52
+
53
+ function pathIn(root: string, relative: string): string {
54
+ return `${root}/${relative}`;
55
+ }
56
+
57
+ function isMissing(error: unknown) {
58
+ return error instanceof Error && (
59
+ "code" in error && String((error as { code: unknown }).code).includes("NotFound") ||
60
+ error.message.includes("Could not find") ||
61
+ error.message.includes("ENOENT")
62
+ );
63
+ }
64
+
65
+ /** A linear Git history for one Host-selected Workspace subtree. */
66
+ export class WorkspaceVersionControl implements WorkspaceVersionPort {
67
+ private readonly workspace: WorkspacePort;
68
+ private readonly dir: string;
69
+ private readonly gitdir: string;
70
+ private readonly fs: git.FsClient;
71
+
72
+ constructor(options: VersionControlOptions) {
73
+ this.workspace = options.workspace;
74
+ this.dir = coordinate(options.dir);
75
+ this.gitdir = coordinate(options.gitdir);
76
+ if (
77
+ this.dir === this.gitdir ||
78
+ this.dir.startsWith(`${this.gitdir}/`) ||
79
+ this.gitdir.startsWith(`${this.dir}/`)
80
+ ) throw new Error("Workspace revision gitdir must be separate from its worktree");
81
+ this.fs = createWorkspaceGitFs(this.workspace);
82
+ }
83
+
84
+ async current(): Promise<WorkspaceRevision | null> {
85
+ await this.ensure();
86
+ try {
87
+ const entries = await git.log({
88
+ fs: this.fs,
89
+ dir: this.dir,
90
+ gitdir: this.gitdir,
91
+ depth: 1,
92
+ });
93
+ return entries[0] ? this.revision(entries[0]) : null;
94
+ } catch (error) {
95
+ if (isMissing(error)) return null;
96
+ throw error;
97
+ }
98
+ }
99
+
100
+ async checkpoint(input: { reason: string }) {
101
+ await this.ensure();
102
+ const reason = input.reason.trim();
103
+ if (!reason) throw new Error("Workspace revision reason is required");
104
+ const currentFiles = await this.worktreeFiles();
105
+ const before = await this.current();
106
+ if (currentFiles.size === 0 && !before) {
107
+ return { revision: null, treeHash: null, changed: false };
108
+ }
109
+ if (
110
+ before &&
111
+ sameFiles(currentFiles, (await this.readRevision(before.revision)).files)
112
+ ) {
113
+ return {
114
+ revision: before.revision,
115
+ treeHash: before.treeHash,
116
+ changed: false,
117
+ };
118
+ }
119
+
120
+ await this.stageWorktree(currentFiles);
121
+ const revision = await git.commit({
122
+ fs: this.fs,
123
+ dir: this.dir,
124
+ gitdir: this.gitdir,
125
+ message: reason,
126
+ author: AUTHOR,
127
+ committer: AUTHOR,
128
+ });
129
+ const treeHash = (await git.readTree({
130
+ fs: this.fs,
131
+ dir: this.dir,
132
+ gitdir: this.gitdir,
133
+ oid: revision,
134
+ })).oid;
135
+ return { revision, treeHash, changed: true };
136
+ }
137
+
138
+ async list(input?: { limit?: number }) {
139
+ await this.ensure();
140
+ const limit = Math.min(Math.max(input?.limit ?? 20, 1), MAX_LIST_LIMIT);
141
+ try {
142
+ return Promise.all((await git.log({
143
+ fs: this.fs,
144
+ dir: this.dir,
145
+ gitdir: this.gitdir,
146
+ depth: limit,
147
+ })).map((entry) => this.revision(entry)));
148
+ } catch (error) {
149
+ if (isMissing(error)) return [];
150
+ throw error;
151
+ }
152
+ }
153
+
154
+ async manifest(revision: string): Promise<WorkspaceRevisionManifest> {
155
+ const { treeHash, files } = await this.readRevision(revision);
156
+ return {
157
+ revision,
158
+ treeHash,
159
+ files: files.map(({ path, bytes }) => ({ path, size: bytes.byteLength })),
160
+ };
161
+ }
162
+
163
+ async readFile(revision: string, path: string): Promise<Uint8Array> {
164
+ const normalized = relativePath(path);
165
+ const { files } = await this.readRevision(revision);
166
+ const file = files.find((candidate) => candidate.path === normalized);
167
+ if (!file) throw new WorkspaceRevisionNotFoundError();
168
+ return file.bytes.slice();
169
+ }
170
+
171
+ async restore(input: { revision: string; reason: string }) {
172
+ const target = await this.readRevision(input.revision);
173
+ const preserved = await this.checkpoint({ reason: "Before restore" });
174
+ if (preserved.treeHash === target.treeHash) {
175
+ return {
176
+ revision: preserved.revision!,
177
+ treeHash: preserved.treeHash!,
178
+ };
179
+ }
180
+ const before = preserved.revision
181
+ ? await this.readRevision(preserved.revision)
182
+ : { treeHash: "", files: [] };
183
+ try {
184
+ await this.replaceWorktree(target.files);
185
+ const restored = await this.checkpoint({ reason: input.reason });
186
+ return { revision: restored.revision!, treeHash: restored.treeHash! };
187
+ } catch (error) {
188
+ try {
189
+ await this.replaceWorktree(before.files);
190
+ await this.stageWorktree();
191
+ } catch (compensationError) {
192
+ throw new AggregateError(
193
+ [error, compensationError],
194
+ "workspace_revision_restore_compensation_failed",
195
+ );
196
+ }
197
+ throw error;
198
+ }
199
+ }
200
+
201
+ private async ensure() {
202
+ await this.workspace.mkdir(this.dir, { recursive: true });
203
+ await this.workspace.mkdir(this.gitdir, { recursive: true });
204
+ await git.init({
205
+ fs: this.fs,
206
+ dir: this.dir,
207
+ gitdir: this.gitdir,
208
+ defaultBranch: "main",
209
+ });
210
+ }
211
+
212
+ private async revision(entry: Awaited<ReturnType<typeof git.log>>[number]) {
213
+ const treeHash = (await git.readTree({
214
+ fs: this.fs,
215
+ dir: this.dir,
216
+ gitdir: this.gitdir,
217
+ oid: entry.oid,
218
+ })).oid;
219
+ return {
220
+ revision: entry.oid,
221
+ treeHash,
222
+ reason: entry.commit.message.trim(),
223
+ createdAt: entry.commit.author.timestamp * 1_000,
224
+ } satisfies WorkspaceRevision;
225
+ }
226
+
227
+ private async readRevision(revision: string): Promise<{
228
+ treeHash: string;
229
+ files: TreeFile[];
230
+ }> {
231
+ await this.ensure();
232
+ if (!REVISION_PATTERN.test(revision)) throw new WorkspaceRevisionNotFoundError();
233
+ try {
234
+ await git.readCommit({
235
+ fs: this.fs,
236
+ dir: this.dir,
237
+ gitdir: this.gitdir,
238
+ oid: revision,
239
+ });
240
+ const root = await git.readTree({
241
+ fs: this.fs,
242
+ dir: this.dir,
243
+ gitdir: this.gitdir,
244
+ oid: revision,
245
+ });
246
+ const files: TreeFile[] = [];
247
+ await this.readTreeFiles(root.oid, "", files);
248
+ files.sort((left, right) => left.path.localeCompare(right.path));
249
+ return { treeHash: root.oid, files };
250
+ } catch (error) {
251
+ if (error instanceof WorkspaceRevisionNotFoundError || isMissing(error)) {
252
+ throw new WorkspaceRevisionNotFoundError();
253
+ }
254
+ throw error;
255
+ }
256
+ }
257
+
258
+ private async readTreeFiles(oid: string, prefix: string, files: TreeFile[]) {
259
+ const { tree } = await git.readTree({
260
+ fs: this.fs,
261
+ dir: this.dir,
262
+ gitdir: this.gitdir,
263
+ oid,
264
+ });
265
+ for (const entry of tree) {
266
+ const path = prefix ? `${prefix}/${entry.path}` : entry.path;
267
+ relativePath(path);
268
+ if (entry.type === "tree") {
269
+ await this.readTreeFiles(entry.oid, path, files);
270
+ } else if (entry.type === "blob" && entry.mode !== "120000") {
271
+ files.push({
272
+ path,
273
+ oid: entry.oid,
274
+ bytes: (await git.readBlob({
275
+ fs: this.fs,
276
+ dir: this.dir,
277
+ gitdir: this.gitdir,
278
+ oid: entry.oid,
279
+ })).blob,
280
+ });
281
+ } else {
282
+ throw new WorkspaceRevisionNotFoundError();
283
+ }
284
+ }
285
+ }
286
+
287
+ private async worktreeFiles() {
288
+ const files = new Map<string, Uint8Array>();
289
+ const pending = [this.dir];
290
+ while (pending.length > 0) {
291
+ const directory = pending.pop()!;
292
+ for (const entry of await this.readDir(directory)) {
293
+ const path = entry.path.slice(this.dir.length + 1);
294
+ relativePath(path);
295
+ if (entry.type === "directory") pending.push(entry.path);
296
+ else if (entry.type === "file") {
297
+ const bytes = await this.workspace.readFileBytes(entry.path);
298
+ if (!bytes) throw new Error("Workspace file disappeared during checkpoint");
299
+ files.set(path, bytes);
300
+ } else {
301
+ throw new Error("Workspace revisions do not support symbolic links");
302
+ }
303
+ }
304
+ }
305
+ return files;
306
+ }
307
+
308
+ private async readDir(path: string) {
309
+ const entries = [];
310
+ let offset = 0;
311
+ while (true) {
312
+ const page = await this.workspace.readDir(path, {
313
+ limit: PAGE_SIZE,
314
+ offset,
315
+ });
316
+ if (page.length === 0) return entries;
317
+ entries.push(...page);
318
+ offset += page.length;
319
+ }
320
+ }
321
+
322
+ private async replaceWorktree(files: readonly TreeFile[]) {
323
+ const target = new Set(files.map(({ path }) => path));
324
+ const current = await this.worktreeFiles();
325
+ for (const path of current.keys()) {
326
+ if (!target.has(path)) {
327
+ await this.workspace.rm(pathIn(this.dir, path), { force: true });
328
+ }
329
+ }
330
+ for (const file of files) {
331
+ await this.workspace.writeFileBytes(pathIn(this.dir, file.path), file.bytes);
332
+ }
333
+ }
334
+
335
+ private async stageWorktree(files?: ReadonlyMap<string, Uint8Array>) {
336
+ files ??= await this.worktreeFiles();
337
+ const tracked = new Set(await git.listFiles({
338
+ fs: this.fs,
339
+ dir: this.dir,
340
+ gitdir: this.gitdir,
341
+ }));
342
+ for (const path of files.keys()) {
343
+ await git.add({
344
+ fs: this.fs,
345
+ dir: this.dir,
346
+ gitdir: this.gitdir,
347
+ filepath: path,
348
+ force: true,
349
+ });
350
+ tracked.delete(path);
351
+ }
352
+ for (const path of tracked) {
353
+ await git.remove({
354
+ fs: this.fs,
355
+ dir: this.dir,
356
+ gitdir: this.gitdir,
357
+ filepath: path,
358
+ });
359
+ }
360
+ }
361
+
362
+ }
363
+
364
+ function sameFiles(
365
+ current: ReadonlyMap<string, Uint8Array>,
366
+ revision: readonly TreeFile[],
367
+ ) {
368
+ if (current.size !== revision.length) return false;
369
+ return revision.every(({ path, bytes }) => {
370
+ const candidate = current.get(path);
371
+ return candidate?.byteLength === bytes.byteLength &&
372
+ candidate.every((byte, index) => byte === bytes[index]);
373
+ });
374
+ }
@@ -0,0 +1,152 @@
1
+ import type {
2
+ AgentToolChildAdapter,
3
+ AgentToolRunInspection,
4
+ AgentToolStoredChunk,
5
+ } from "agents";
6
+ import { AgentRuntimeKernel } from "./runtime";
7
+
8
+ const AGENT_TOOL_RUN_PREFIX = "universal-agent:temporary-agent-run:";
9
+
10
+ interface AgentToolRunRecord {
11
+ runId: string;
12
+ submissionId: string;
13
+ startedAt: number;
14
+ completedAt?: number;
15
+ summary?: string;
16
+ }
17
+
18
+ function agentToolRunKey(runId: string): string {
19
+ return `${AGENT_TOOL_RUN_PREFIX}${runId}`;
20
+ }
21
+
22
+ /**
23
+ * 把 Cloudflare Agent Tool 子运行协议适配到 Runtime Submission 生命周期。
24
+ *
25
+ * 具体 Runtime 只负责准备输入与清理资源;启动、查询、取消和结果读取统一留在这里。
26
+ */
27
+ export abstract class AgentToolRuntimeKernel<
28
+ Env extends Cloudflare.Env = Cloudflare.Env,
29
+ > extends AgentRuntimeKernel<Env>
30
+ implements AgentToolChildAdapter<unknown, string> {
31
+ protected abstract prepareAgentToolRun(input: unknown): Promise<string>;
32
+ protected abstract cleanupAgentToolRun(): Promise<void>;
33
+
34
+ async startAgentToolRun(
35
+ input: unknown,
36
+ options: { runId: string; signal?: AbortSignal },
37
+ ): Promise<AgentToolRunInspection<string>> {
38
+ options.signal?.throwIfAborted();
39
+ try {
40
+ const prompt = await this.prepareAgentToolRun(input);
41
+ const receipt = await this.submitPrompt(prompt, {
42
+ idempotencyKey: options.runId,
43
+ });
44
+ const record: AgentToolRunRecord = {
45
+ runId: options.runId,
46
+ submissionId: receipt.submissionId,
47
+ startedAt: receipt.createdAt,
48
+ };
49
+ await this.ctx.storage.put(agentToolRunKey(options.runId), record);
50
+ const cancel = () => {
51
+ void this.cancelAgentToolRun(options.runId, options.signal?.reason);
52
+ };
53
+ if (options.signal?.aborted) cancel();
54
+ else options.signal?.addEventListener("abort", cancel, { once: true });
55
+ return {
56
+ runId: options.runId,
57
+ status: "running",
58
+ startedAt: record.startedAt,
59
+ };
60
+ } catch (error) {
61
+ await this.cleanupAgentToolRun();
62
+ throw error;
63
+ }
64
+ }
65
+
66
+ async inspectAgentToolRun(
67
+ runId: string,
68
+ ): Promise<AgentToolRunInspection<string> | null> {
69
+ const key = agentToolRunKey(runId);
70
+ const record = await this.ctx.storage.get<AgentToolRunRecord>(key);
71
+ if (!record) return null;
72
+ const receipt = await this.getSubmission(record.submissionId);
73
+ if (!receipt) return null;
74
+ if (receipt.status === "pending" || receipt.status === "running") {
75
+ return { runId, status: "running", startedAt: record.startedAt };
76
+ }
77
+
78
+ const completedAt = receipt.completedAt ?? Date.now();
79
+ if (receipt.status === "completed") {
80
+ const summary = record.summary ?? await this.latestAssistantText();
81
+ if (!summary) {
82
+ await this.cleanupAgentToolRun();
83
+ return {
84
+ runId,
85
+ status: "error",
86
+ error: "Temporary Agent completed without final text",
87
+ startedAt: record.startedAt,
88
+ completedAt,
89
+ };
90
+ }
91
+ await this.ctx.storage.put(key, {
92
+ ...record,
93
+ summary,
94
+ completedAt,
95
+ } satisfies AgentToolRunRecord);
96
+ await this.cleanupAgentToolRun();
97
+ return {
98
+ runId,
99
+ status: "completed",
100
+ output: summary,
101
+ summary,
102
+ startedAt: record.startedAt,
103
+ completedAt,
104
+ };
105
+ }
106
+
107
+ await this.cleanupAgentToolRun();
108
+ return receipt.status === "aborted"
109
+ ? {
110
+ runId,
111
+ status: "aborted",
112
+ error: receipt.error ?? "Temporary Agent was cancelled",
113
+ startedAt: record.startedAt,
114
+ completedAt,
115
+ }
116
+ : {
117
+ runId,
118
+ status: "error",
119
+ error: receipt.error ??
120
+ (receipt.status === "skipped"
121
+ ? "Temporary Agent submission was skipped"
122
+ : "Temporary Agent failed"),
123
+ startedAt: record.startedAt,
124
+ completedAt,
125
+ };
126
+ }
127
+
128
+ async getAgentToolChunks(
129
+ _runId: string,
130
+ _options?: { afterSequence?: number },
131
+ ): Promise<AgentToolStoredChunk[]> {
132
+ return [];
133
+ }
134
+
135
+ async cancelAgentToolRun(runId: string, reason?: unknown): Promise<void> {
136
+ const record = await this.ctx.storage.get<AgentToolRunRecord>(
137
+ agentToolRunKey(runId),
138
+ );
139
+ try {
140
+ if (record) {
141
+ await this.cancelSubmissionById(
142
+ record.submissionId,
143
+ reason instanceof Error
144
+ ? reason.message
145
+ : String(reason ?? "Temporary Agent cancelled"),
146
+ );
147
+ }
148
+ } finally {
149
+ await this.cleanupAgentToolRun();
150
+ }
151
+ }
152
+ }
@@ -0,0 +1,27 @@
1
+ import type { SqlTaggedTemplate } from "agents/chat";
2
+
3
+ // Agent Tool Run 的统一定义见 ./index.ts。
4
+ export class AgentToolRepository {
5
+ // 保存当前 Agent SQLite 的查询入口。
6
+ // RuntimeDatabase 构造时调用,活动投影随后通过这个 Repository 读取子运行。
7
+ // 把存储绑定留在构造阶段,避免每次读取都传入可能属于其他 Agent 的 sql。
8
+ constructor(private readonly sql: SqlTaggedTemplate) {}
9
+
10
+ // 是否仍有 Agent Tool 子运行在执行。
11
+ // 活动投影在重算 activity 时调用,判定只在这里做一次。
12
+ // `interrupted` 单独判 `child_still_running`:父级不再等待并不代表子级已经停下。
13
+ hasRunning(): boolean {
14
+ try {
15
+ return (
16
+ this.sql<{ count: number }>`
17
+ SELECT COUNT(*) AS count FROM cf_agent_tool_runs
18
+ WHERE status IN ('starting', 'running')
19
+ OR (status = 'interrupted' AND child_still_running = 1)
20
+ `[0]?.count ?? 0
21
+ ) > 0;
22
+ } catch {
23
+ // 一个子运行都没派发过时,Agents SDK 还没有把该表建出来。
24
+ return false;
25
+ }
26
+ }
27
+ }
package/src/db/index.ts CHANGED
@@ -3,41 +3,52 @@ import type { SqlTaggedTemplate } from "agents/chat";
3
3
  import { initializeSchema } from "./schema";
4
4
  import { SubmissionRepository } from "./submission.repo";
5
5
  import { ApprovalRepository } from "./approval.repo";
6
+ import { ToolInteractionRepository } from "./interaction.repo";
6
7
  import { ToolSettlementRepository } from "./settlement.repo";
7
8
  import { RecoveryMilestoneRepository } from "./milestone.repo";
8
9
  import { ExtContextRepository } from "./ext-context.repo";
9
10
  import { MessageUiRepository } from "./message-ui.repo";
10
11
  import { SteerRepository } from "./steer.repo";
11
12
  import { RuntimeEventOutboxRepository } from "./runtime-event-outbox.repo";
13
+ import { AgentToolRepository } from "./agent-tool.repo";
12
14
 
13
15
  // 数据库术语以这里为准。
14
16
  // Submission 是一次可持久化的用户提交,从排队到终态都用同一个 submissionId 跟踪。
15
17
  // Approval 是某次工具调用的持久化审批记录,executionId 是对外决策时的主键。
18
+ // Tool Interaction 是「结果由客户端提供」的工具调用的 park 记录,interactionId 是主键;
19
+ // 它只回答还等不等得到响应,答案的权威副本仍是 Settlement。
16
20
  // Settlement 是工具调用的最终结果,同一 Submission 内用 toolCallId 防止重复落盘。
17
21
  // Recovery Milestone 是按 seq 重放的恢复事实,milestoneKey 用来去重。
18
22
  // Extension Context 是扩展按 label 保存的文本,不随清空聊天记录删除。
19
23
  // Message UI 是会话消息的用户界面补充数据,它由 Transcript 而不是 Turn 生命周期管理。
24
+ // Agent Tool Run 是 Agents SDK 派发的子运行,唯一一张不归 Runtime 所有的表:SDK 建它、写它、清它,
25
+ // 这里只读。它没有 `pi_` 前缀,也不进 initializeSchema 和 clearAll。之所以直读 SDK 的表而不另建账本,
26
+ // 是因为 SDK 没有公开父级侧的「还有没有子运行在跑」读法,而复制一份必然产生第二个真相源。
20
27
  // Repository 只把 Runtime 语义翻译成参数化 SQL,所有表都位于当前 Agent 实例自己的 Durable Object SQLite 存储中。
21
28
  // Transaction 指 Cloudflare transactionSync 包住的同步操作,回调抛错时整体回滚。
22
29
  export * from "./schema";
23
30
  export * from "./submission.repo";
24
31
  export * from "./approval.repo";
32
+ export * from "./interaction.repo";
25
33
  export * from "./settlement.repo";
26
34
  export * from "./milestone.repo";
27
35
  export * from "./ext-context.repo";
28
36
  export * from "./message-ui.repo";
29
37
  export * from "./steer.repo";
30
38
  export * from "./runtime-event-outbox.repo";
39
+ export * from "./agent-tool.repo";
31
40
 
32
41
  export class RuntimeDatabase {
33
42
  readonly submissions: SubmissionRepository;
34
43
  readonly approvals: ApprovalRepository;
44
+ readonly interactions: ToolInteractionRepository;
35
45
  readonly settlements: ToolSettlementRepository;
36
46
  readonly milestones: RecoveryMilestoneRepository;
37
47
  readonly extContext: ExtContextRepository;
38
48
  readonly messageUi: MessageUiRepository;
39
49
  readonly steers: SteerRepository;
40
50
  readonly runtimeEvents: RuntimeEventOutboxRepository;
51
+ readonly agentTools: AgentToolRepository;
41
52
 
42
53
  // 给各个 Repository 分配同一个 Agent SQLite 入口和事务入口。
43
54
  // AgentRuntimeKernel 构造时只创建一次,业务代码随后通过对应属性访问仓储。
@@ -48,12 +59,33 @@ export class RuntimeDatabase {
48
59
  ) {
49
60
  this.submissions = new SubmissionRepository(sql);
50
61
  this.approvals = new ApprovalRepository(sql);
62
+ this.interactions = new ToolInteractionRepository(sql);
51
63
  this.settlements = new ToolSettlementRepository(sql);
52
64
  this.milestones = new RecoveryMilestoneRepository(sql);
53
65
  this.extContext = new ExtContextRepository(sql);
54
66
  this.messageUi = new MessageUiRepository(sql);
55
67
  this.steers = new SteerRepository(sql);
56
68
  this.runtimeEvents = new RuntimeEventOutboxRepository(sql);
69
+ this.agentTools = new AgentToolRepository(sql);
70
+ }
71
+
72
+ /**
73
+ * 有未完成 Submission,且每一条都停在人机等待上。
74
+ *
75
+ * 换装配的忙碌闸门调用它。「停在等人」有两种形态:待决审批,和等客户端
76
+ * 投递结果的 Tool Interaction —— 两者都意味着没有模型请求在飞、没有工具
77
+ * 在执行,因此换掉能力语义是安全的。
78
+ *
79
+ * 注意它不是「存在待处理的人机等待」:只要有一条 Submission 正在真的执行,
80
+ * 整个判断就必须为假,否则会把执行中的 Turn 切成两套能力语义。
81
+ */
82
+ everyUnfinishedSubmissionParked(): boolean {
83
+ const unfinished = this.submissions.listUnfinishedIds();
84
+ if (unfinished.length === 0) return false;
85
+ return unfinished.every((submissionId) =>
86
+ this.approvals.listPendingForSubmission(submissionId).length > 0 ||
87
+ this.interactions.listPendingForSubmission(submissionId).length > 0
88
+ );
57
89
  }
58
90
 
59
91
  // 在一个同步 SQLite 事务里运行一组读写。
@@ -77,6 +109,7 @@ export class RuntimeDatabase {
77
109
  this.sql`DELETE FROM pi_pending_steers`;
78
110
  this.sql`DELETE FROM pi_submissions`;
79
111
  this.sql`DELETE FROM pi_approvals`;
112
+ this.sql`DELETE FROM pi_tool_interactions`;
80
113
  this.sql`DELETE FROM pi_tool_settlements`;
81
114
  this.sql`DELETE FROM pi_recovery_milestones`;
82
115
  }