@springbrand/agent-runtime 0.1.0
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 +28 -0
- package/src/db/approval.repo.ts +291 -0
- package/src/db/ext-context.repo.ts +34 -0
- package/src/db/index.ts +83 -0
- package/src/db/message-ui.repo.ts +39 -0
- package/src/db/milestone.repo.ts +96 -0
- package/src/db/runtime-event-outbox.repo.ts +89 -0
- package/src/db/schema.ts +164 -0
- package/src/db/settlement.repo.ts +104 -0
- package/src/db/steer.repo.ts +73 -0
- package/src/db/submission.repo.ts +323 -0
- package/src/index.ts +133 -0
- package/src/kernel/approval-lifecycle.ts +552 -0
- package/src/kernel/bindings.ts +898 -0
- package/src/kernel/degradation.ts +15 -0
- package/src/kernel/extensions.ts +108 -0
- package/src/kernel/profile.ts +116 -0
- package/src/kernel/public-contracts.ts +17 -0
- package/src/kernel/receipts.ts +124 -0
- package/src/kernel/recoverable-chat-agent.ts +899 -0
- package/src/kernel/state.ts +76 -0
- package/src/kernel/submission-lifecycle.ts +600 -0
- package/src/layers/context/budget/gate.ts +88 -0
- package/src/layers/orchestration/subagents/agent-types/contract.ts +78 -0
- package/src/layers/orchestration/subagents/agent-types/extract/index.ts +47 -0
- package/src/layers/orchestration/subagents/agent-types/fanout/index.ts +53 -0
- package/src/layers/orchestration/subagents/agent-types/registry.ts +16 -0
- package/src/layers/orchestration/temporary-agent/core.ts +152 -0
- package/src/layers/orchestration/temporary-agent/runner.ts +133 -0
- package/src/layers/orchestration/temporary-agent/workspace.ts +154 -0
- package/src/lib/artifacts.ts +54 -0
- package/src/lib/egress.ts +44 -0
- package/src/lib/execution-level.ts +27 -0
- package/src/lib/extension-name.ts +18 -0
- package/src/lib/host-actions.ts +57 -0
- package/src/lib/mcp.ts +86 -0
- package/src/lib/model-catalog.ts +7 -0
- package/src/lib/prompt.ts +139 -0
- package/src/lib/telemetry-dev.ts +44 -0
- package/src/pi/assembly/context.ts +510 -0
- package/src/pi/assembly/extensions.ts +661 -0
- package/src/pi/assembly/index.ts +19 -0
- package/src/pi/assembly/snapshot.ts +200 -0
- package/src/pi/message/contract.ts +8 -0
- package/src/pi/message/conversion.ts +73 -0
- package/src/pi/message/index.ts +3 -0
- package/src/pi/message/projection.ts +604 -0
- package/src/pi/runtime-adapter/assembly.ts +552 -0
- package/src/pi/runtime-adapter/execution.ts +683 -0
- package/src/pi/runtime-adapter/index.ts +232 -0
- package/src/pi/runtime-adapter/models.ts +243 -0
- package/src/pi/runtime-adapter/recovery.ts +805 -0
- package/src/pi/runtime-adapter/transcript.ts +825 -0
- package/src/pi/session/index.ts +24 -0
- package/src/pi/session/storage.ts +353 -0
- package/src/pi/tool/ai-adapter.ts +100 -0
- package/src/pi/tool/base.ts +110 -0
- package/src/pi/tool/compiler.ts +444 -0
- package/src/pi/tool/core-host.ts +48 -0
- package/src/pi/tool/core.ts +251 -0
- package/src/pi/tool/index.ts +32 -0
- package/src/pi/tool/mcp.ts +319 -0
- package/src/pi/tool/schedule.ts +198 -0
- package/src/pi/tool/skill.ts +455 -0
- package/src/pi/tool/subagent.ts +148 -0
- package/src/pi/tool/web-search/api.ts +1292 -0
- package/src/pi/tool/web-search/index.ts +2 -0
- package/src/pi/tool/web-search/web-search.ts +127 -0
- package/src/pi/tool/workspace-sandbox.ts +664 -0
- package/src/pi/turn/approval.ts +181 -0
- package/src/pi/turn/index.ts +62 -0
- package/src/pi/turn/tool-recovery.ts +792 -0
- package/src/plugins.ts +1024 -0
- package/src/runtime-agent.ts +654 -0
- package/src/runtime.ts +2880 -0
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@springbrand/agent-runtime",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": [
|
|
6
|
+
"src",
|
|
7
|
+
"!src/**/*.test.ts"
|
|
8
|
+
],
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./src/index.ts",
|
|
14
|
+
"./contracts": "./src/kernel/public-contracts.ts",
|
|
15
|
+
"./models": "./src/lib/model-catalog.ts"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@cloudflare/codemode": "0.5.0",
|
|
19
|
+
"@cloudflare/shell": "0.4.3",
|
|
20
|
+
"@cloudflare/think": "0.15.0",
|
|
21
|
+
"@earendil-works/pi-agent-core": "0.83.0",
|
|
22
|
+
"@earendil-works/pi-ai": "0.83.0",
|
|
23
|
+
"agents": "^0.19.0",
|
|
24
|
+
"ai": "^7.0.0",
|
|
25
|
+
"lodash-es": "^4.18.1",
|
|
26
|
+
"zod": "^4.4.3"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import type { SqlTaggedTemplate } from "agents/chat";
|
|
2
|
+
import type { ExecutionLevel } from "../lib/execution-level";
|
|
3
|
+
|
|
4
|
+
// Approval 的统一定义见 ./index.ts。
|
|
5
|
+
// #region 状态与行类型
|
|
6
|
+
export type ApprovalStatus = "pending" | "approved" | "rejected";
|
|
7
|
+
|
|
8
|
+
export interface StoredApproval {
|
|
9
|
+
executionId: string;
|
|
10
|
+
submissionId: string;
|
|
11
|
+
requestId: string;
|
|
12
|
+
source: string;
|
|
13
|
+
toolCallId: string;
|
|
14
|
+
toolName: string;
|
|
15
|
+
summary: string;
|
|
16
|
+
executionLevel: ExecutionLevel;
|
|
17
|
+
requiredExecutionLevel: ExecutionLevel;
|
|
18
|
+
inputJson: string;
|
|
19
|
+
status: ApprovalStatus;
|
|
20
|
+
createdAt: number;
|
|
21
|
+
decidedAt: number | null;
|
|
22
|
+
reason: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface NewApproval {
|
|
26
|
+
executionId: string;
|
|
27
|
+
submissionId: string;
|
|
28
|
+
requestId: string;
|
|
29
|
+
source: string;
|
|
30
|
+
toolCallId: string;
|
|
31
|
+
toolName: string;
|
|
32
|
+
summary: string;
|
|
33
|
+
executionLevel: ExecutionLevel;
|
|
34
|
+
requiredExecutionLevel: ExecutionLevel;
|
|
35
|
+
inputJson: string;
|
|
36
|
+
status: ApprovalStatus;
|
|
37
|
+
createdAt: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface PendingApprovalSummary {
|
|
41
|
+
executionId: string;
|
|
42
|
+
source: string;
|
|
43
|
+
toolName: string;
|
|
44
|
+
summary: string;
|
|
45
|
+
executionLevel: ExecutionLevel;
|
|
46
|
+
requiredExecutionLevel: ExecutionLevel;
|
|
47
|
+
inputJson: string;
|
|
48
|
+
requestId: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ApprovalView {
|
|
52
|
+
executionId: string;
|
|
53
|
+
toolCallId: string;
|
|
54
|
+
status: ApprovalStatus;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface DecidedApprovalRef {
|
|
58
|
+
executionId: string;
|
|
59
|
+
submissionId: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type ApprovalRow = {
|
|
63
|
+
execution_id: string;
|
|
64
|
+
submission_id: string;
|
|
65
|
+
request_id: string;
|
|
66
|
+
source: string;
|
|
67
|
+
tool_call_id: string;
|
|
68
|
+
tool_name: string;
|
|
69
|
+
summary: string;
|
|
70
|
+
execution_level: string;
|
|
71
|
+
required_level: string;
|
|
72
|
+
input_json: string;
|
|
73
|
+
status: string;
|
|
74
|
+
created_at: number;
|
|
75
|
+
decided_at: number | null;
|
|
76
|
+
reason: string | null;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// #endregion
|
|
80
|
+
// #region 行映射
|
|
81
|
+
// 把 SQLite 的 Approval 行转成 Runtime 对象。
|
|
82
|
+
// find 查到记录后调用,上层因而不需要理解 snake_case 列名。
|
|
83
|
+
// 状态由表内字符串转成联合类型;新增列或状态时必须同步核对类型、SELECT 和映射。
|
|
84
|
+
function mapRow(row: ApprovalRow): StoredApproval {
|
|
85
|
+
return {
|
|
86
|
+
executionId: row.execution_id,
|
|
87
|
+
submissionId: row.submission_id,
|
|
88
|
+
requestId: row.request_id,
|
|
89
|
+
source: row.source,
|
|
90
|
+
toolCallId: row.tool_call_id,
|
|
91
|
+
toolName: row.tool_name,
|
|
92
|
+
summary: row.summary,
|
|
93
|
+
executionLevel: row.execution_level as ExecutionLevel,
|
|
94
|
+
requiredExecutionLevel: row.required_level as ExecutionLevel,
|
|
95
|
+
inputJson: row.input_json,
|
|
96
|
+
status: row.status as ApprovalStatus,
|
|
97
|
+
createdAt: row.created_at,
|
|
98
|
+
decidedAt: row.decided_at,
|
|
99
|
+
reason: row.reason,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// #endregion
|
|
104
|
+
// #region Repository
|
|
105
|
+
export class ApprovalRepository {
|
|
106
|
+
// 保存当前 Agent SQLite 的查询入口。
|
|
107
|
+
// RuntimeDatabase 构造时调用,ApprovalLifecycle 和 Transcript 随后共享这个 Repository。
|
|
108
|
+
// Approval 必须与所属 Submission 在同一 Agent 存储中查询,不能跨 Durable Object 拼接状态。
|
|
109
|
+
constructor(private readonly sql: SqlTaggedTemplate) {}
|
|
110
|
+
|
|
111
|
+
// 按 executionId 读取一条完整 Approval,不存在时返回 null。
|
|
112
|
+
// ApprovalLifecycle 在请求去重、决策、继续和拒绝时调用,调用方必须处理 null。
|
|
113
|
+
// execution_id 是主键,LIMIT 1 明确只消费一行;不能改成按非唯一字段随意取首行。
|
|
114
|
+
find(executionId: string): StoredApproval | null {
|
|
115
|
+
const row = this.sql<ApprovalRow>`
|
|
116
|
+
SELECT execution_id, submission_id, request_id, source,
|
|
117
|
+
tool_call_id, tool_name, summary,
|
|
118
|
+
execution_level, required_level, input_json,
|
|
119
|
+
status, created_at, decided_at, reason
|
|
120
|
+
FROM pi_approvals
|
|
121
|
+
WHERE execution_id = ${executionId}
|
|
122
|
+
LIMIT 1
|
|
123
|
+
`[0];
|
|
124
|
+
return row ? mapRow(row) : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 持久化一条新的 Approval。
|
|
128
|
+
// ApprovalLifecycle.ensurePending 在准入事务内确认没有同一记录后调用。
|
|
129
|
+
// 主键和 submission_id/tool_call_id 唯一约束是持久化去重防线,不能用覆盖式 INSERT 掩盖冲突审批。
|
|
130
|
+
insert(approval: NewApproval): void {
|
|
131
|
+
this.sql`
|
|
132
|
+
INSERT INTO pi_approvals (
|
|
133
|
+
execution_id, submission_id, request_id, source,
|
|
134
|
+
tool_call_id, tool_name, summary,
|
|
135
|
+
execution_level, required_level, input_json,
|
|
136
|
+
status, created_at
|
|
137
|
+
) VALUES (
|
|
138
|
+
${approval.executionId},
|
|
139
|
+
${approval.submissionId},
|
|
140
|
+
${approval.requestId},
|
|
141
|
+
${approval.source},
|
|
142
|
+
${approval.toolCallId},
|
|
143
|
+
${approval.toolName},
|
|
144
|
+
${approval.summary},
|
|
145
|
+
${approval.executionLevel},
|
|
146
|
+
${approval.requiredExecutionLevel},
|
|
147
|
+
${approval.inputJson},
|
|
148
|
+
${approval.status},
|
|
149
|
+
${approval.createdAt}
|
|
150
|
+
)
|
|
151
|
+
`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 只把 pending Approval 写成指定决策,并记录时间和原因。
|
|
155
|
+
// applyPiRecoveryMutations 在处理用户决策或恢复命令时调用,迟到的重复决策不应覆盖首次结果。
|
|
156
|
+
// WHERE status='pending' 实现首次决策胜出,不能去掉这个条件;调用方在事务后重读来确认是否真的生效。
|
|
157
|
+
decide(
|
|
158
|
+
executionId: string,
|
|
159
|
+
status: ApprovalStatus,
|
|
160
|
+
decidedAt: number,
|
|
161
|
+
reason?: string,
|
|
162
|
+
): void {
|
|
163
|
+
this.sql`
|
|
164
|
+
UPDATE pi_approvals
|
|
165
|
+
SET status = ${status},
|
|
166
|
+
decided_at = ${decidedAt},
|
|
167
|
+
reason = ${reason ?? null}
|
|
168
|
+
WHERE execution_id = ${executionId}
|
|
169
|
+
AND status = 'pending'
|
|
170
|
+
`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 按创建顺序列出某次 Submission 仍待决策的 executionId。
|
|
174
|
+
// ApprovalLifecycle.rejectPending 在终止 Turn 时调用,随后为每条待决策记录追加拒绝恢复事实。
|
|
175
|
+
// ORDER BY created_at 保留决策的原始顺序,不能依赖 SQLite 未声明的默认行顺序。
|
|
176
|
+
listPendingForSubmission(submissionId: string): string[] {
|
|
177
|
+
return this.sql<{ execution_id: string }>`
|
|
178
|
+
SELECT execution_id
|
|
179
|
+
FROM pi_approvals
|
|
180
|
+
WHERE submission_id = ${submissionId}
|
|
181
|
+
AND status = 'pending'
|
|
182
|
+
ORDER BY created_at ASC
|
|
183
|
+
`.map((row) => row.execution_id);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 列出所属 Submission 仍活跃的全部待决策 Approval 摘要。
|
|
187
|
+
// ApprovalLifecycle.list 和广播流程在生成用户可见审批列表时调用。
|
|
188
|
+
// JOIN Submission 是为了隐藏已终止 Turn 的孤立 pending 行,不能只按 Approval 自身状态暴露给用户。
|
|
189
|
+
listPendingActive(): PendingApprovalSummary[] {
|
|
190
|
+
return this.sql<{
|
|
191
|
+
execution_id: string;
|
|
192
|
+
source: string;
|
|
193
|
+
tool_name: string;
|
|
194
|
+
summary: string;
|
|
195
|
+
execution_level: string;
|
|
196
|
+
required_level: string;
|
|
197
|
+
input_json: string;
|
|
198
|
+
request_id: string;
|
|
199
|
+
}>`
|
|
200
|
+
SELECT approval.execution_id, approval.source,
|
|
201
|
+
approval.tool_name, approval.summary,
|
|
202
|
+
approval.execution_level, approval.required_level,
|
|
203
|
+
approval.input_json, approval.request_id
|
|
204
|
+
FROM pi_approvals approval
|
|
205
|
+
JOIN pi_submissions submission
|
|
206
|
+
ON submission.submission_id = approval.submission_id
|
|
207
|
+
WHERE approval.status = 'pending'
|
|
208
|
+
AND submission.status IN ('pending', 'running')
|
|
209
|
+
ORDER BY approval.created_at ASC
|
|
210
|
+
`.map((row) => ({
|
|
211
|
+
executionId: row.execution_id,
|
|
212
|
+
source: row.source,
|
|
213
|
+
toolName: row.tool_name,
|
|
214
|
+
summary: row.summary,
|
|
215
|
+
executionLevel: row.execution_level as ExecutionLevel,
|
|
216
|
+
requiredExecutionLevel: row.required_level as ExecutionLevel,
|
|
217
|
+
inputJson: row.input_json,
|
|
218
|
+
requestId: row.request_id,
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 按决策时间列出所有已批准或已拒绝的 Approval 引用。
|
|
223
|
+
// ApprovalLifecycle.dispatchPendingContinuations 在 Runtime 启动时调用,用恢复决策判断哪些记录仍需要继续。
|
|
224
|
+
// 这里故意不预先猜测“已分派”,幂等性由 Recovery Milestone 决定;不能用内存队列替代跨重启扫描。
|
|
225
|
+
listDecided(): DecidedApprovalRef[] {
|
|
226
|
+
return this.sql<{ execution_id: string; submission_id: string }>`
|
|
227
|
+
SELECT execution_id, submission_id
|
|
228
|
+
FROM pi_approvals
|
|
229
|
+
WHERE status IN ('approved', 'rejected')
|
|
230
|
+
ORDER BY decided_at ASC
|
|
231
|
+
`.map((row) => ({
|
|
232
|
+
executionId: row.execution_id,
|
|
233
|
+
submissionId: row.submission_id,
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 计算所属 Submission 仍活跃的 pending Approval 数量。
|
|
238
|
+
// ApprovalLifecycle.countPending 在报告交互阻塞和 Runtime 稳定状态时调用。
|
|
239
|
+
// 与 listPendingActive 使用同样的 JOIN 和状态条件才能保证计数和用户可见列表一致,修改其一时必须核对另一个。
|
|
240
|
+
countPendingActive(): number {
|
|
241
|
+
return (
|
|
242
|
+
this.sql<{ count: number }>`
|
|
243
|
+
SELECT COUNT(*) AS count
|
|
244
|
+
FROM pi_approvals approval
|
|
245
|
+
JOIN pi_submissions submission
|
|
246
|
+
ON submission.submission_id = approval.submission_id
|
|
247
|
+
WHERE approval.status = 'pending'
|
|
248
|
+
AND submission.status IN ('pending', 'running')
|
|
249
|
+
`[0]?.count ?? 0
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 只在某个 toolCallId 唯一匹配一条活跃 pending Approval 时返回 executionId。
|
|
254
|
+
// Runtime 处理工具审批决策时通过 ApprovalLifecycle 调用,返回 null 表示没有或存在歧义。
|
|
255
|
+
// toolCallId 只在单个 Submission 内唯一,LIMIT 2 用最小查询识别跨 Submission 重名;不能用 LIMIT 1 静默选中任意 Approval。
|
|
256
|
+
findPendingByToolCallId(toolCallId: string): string | null {
|
|
257
|
+
const matches = this.sql<{ execution_id: string }>`
|
|
258
|
+
SELECT approval.execution_id
|
|
259
|
+
FROM pi_approvals approval
|
|
260
|
+
JOIN pi_submissions submission
|
|
261
|
+
ON submission.submission_id = approval.submission_id
|
|
262
|
+
WHERE approval.tool_call_id = ${toolCallId}
|
|
263
|
+
AND approval.status = 'pending'
|
|
264
|
+
AND submission.status NOT IN (
|
|
265
|
+
'completed', 'aborted', 'skipped', 'error'
|
|
266
|
+
)
|
|
267
|
+
LIMIT 2
|
|
268
|
+
`;
|
|
269
|
+
return matches.length === 1 ? (matches[0]?.execution_id ?? null) : null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 列出某次 Submission 的审批 ID、工具调用 ID 和状态。
|
|
273
|
+
// Transcript 投影浏览器消息时调用,用这些精简字段补齐工具调用视图。
|
|
274
|
+
// 这里不返回风险、输入等内容,避免 Transcript 和 ApprovalLifecycle 共享过宽的数据契约。
|
|
275
|
+
listApprovalViews(submissionId: string): ApprovalView[] {
|
|
276
|
+
return this.sql<{
|
|
277
|
+
execution_id: string;
|
|
278
|
+
tool_call_id: string;
|
|
279
|
+
status: string;
|
|
280
|
+
}>`
|
|
281
|
+
SELECT execution_id, tool_call_id, status
|
|
282
|
+
FROM pi_approvals
|
|
283
|
+
WHERE submission_id = ${submissionId}
|
|
284
|
+
`.map((row) => ({
|
|
285
|
+
executionId: row.execution_id,
|
|
286
|
+
toolCallId: row.tool_call_id,
|
|
287
|
+
status: row.status as ApprovalStatus,
|
|
288
|
+
}));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// #endregion
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SqlTaggedTemplate } from "agents/chat";
|
|
2
|
+
|
|
3
|
+
// Extension Context 的统一定义见 ./index.ts。
|
|
4
|
+
export class ExtContextRepository {
|
|
5
|
+
// 保存当前 Agent SQLite 的查询入口。
|
|
6
|
+
// RuntimeDatabase 构造时调用,宿主扩展只通过已创建的 Repository 读写。
|
|
7
|
+
// Repository 不自行找 Durable Object,避免把不同 Agent 的上下文混在一起。
|
|
8
|
+
constructor(private readonly sql: SqlTaggedTemplate) {}
|
|
9
|
+
|
|
10
|
+
// 按 label 读取扩展上下文,没有记录时返回 null。
|
|
11
|
+
// _hostGetContext 在扩展请求已保存的文本时调用,调用方应处理 null。
|
|
12
|
+
// 只选 content 保持返回值稳定;如果要暴露 updatedAt,必须同步扩展宿主契约。
|
|
13
|
+
get(label: string): string | null {
|
|
14
|
+
return (
|
|
15
|
+
this.sql<{ content: string }>`
|
|
16
|
+
SELECT content FROM pi_extension_context
|
|
17
|
+
WHERE label = ${label}
|
|
18
|
+
`[0]?.content ?? null
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 按 label 新增或替换扩展上下文。
|
|
23
|
+
// _hostSetContext 在扩展写入文本时调用,同一 label 的后一次写入覆盖前值。
|
|
24
|
+
// SQLite ON CONFLICT 利用 label 主键一条语句完成替换并同步时间,不能拆成会丢失 updated_at 的两条路径。
|
|
25
|
+
set(label: string, content: string): void {
|
|
26
|
+
this.sql`
|
|
27
|
+
INSERT INTO pi_extension_context (label, content, updated_at)
|
|
28
|
+
VALUES (${label}, ${content}, ${Date.now()})
|
|
29
|
+
ON CONFLICT(label) DO UPDATE SET
|
|
30
|
+
content = excluded.content,
|
|
31
|
+
updated_at = excluded.updated_at
|
|
32
|
+
`;
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/db/index.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { SqlTaggedTemplate } from "agents/chat";
|
|
2
|
+
|
|
3
|
+
import { initializeSchema } from "./schema";
|
|
4
|
+
import { SubmissionRepository } from "./submission.repo";
|
|
5
|
+
import { ApprovalRepository } from "./approval.repo";
|
|
6
|
+
import { ToolSettlementRepository } from "./settlement.repo";
|
|
7
|
+
import { RecoveryMilestoneRepository } from "./milestone.repo";
|
|
8
|
+
import { ExtContextRepository } from "./ext-context.repo";
|
|
9
|
+
import { MessageUiRepository } from "./message-ui.repo";
|
|
10
|
+
import { SteerRepository } from "./steer.repo";
|
|
11
|
+
import { RuntimeEventOutboxRepository } from "./runtime-event-outbox.repo";
|
|
12
|
+
|
|
13
|
+
// 数据库术语以这里为准。
|
|
14
|
+
// Submission 是一次可持久化的用户提交,从排队到终态都用同一个 submissionId 跟踪。
|
|
15
|
+
// Approval 是某次工具调用的持久化审批记录,executionId 是对外决策时的主键。
|
|
16
|
+
// Settlement 是工具调用的最终结果,同一 Submission 内用 toolCallId 防止重复落盘。
|
|
17
|
+
// Recovery Milestone 是按 seq 重放的恢复事实,milestoneKey 用来去重。
|
|
18
|
+
// Extension Context 是扩展按 label 保存的文本,不随清空聊天记录删除。
|
|
19
|
+
// Message UI 是会话消息的用户界面补充数据,它由 Transcript 而不是 Turn 生命周期管理。
|
|
20
|
+
// Repository 只把 Runtime 语义翻译成参数化 SQL,所有表都位于当前 Agent 实例自己的 Durable Object SQLite 存储中。
|
|
21
|
+
// Transaction 指 Cloudflare transactionSync 包住的同步操作,回调抛错时整体回滚。
|
|
22
|
+
export * from "./schema";
|
|
23
|
+
export * from "./submission.repo";
|
|
24
|
+
export * from "./approval.repo";
|
|
25
|
+
export * from "./settlement.repo";
|
|
26
|
+
export * from "./milestone.repo";
|
|
27
|
+
export * from "./ext-context.repo";
|
|
28
|
+
export * from "./message-ui.repo";
|
|
29
|
+
export * from "./steer.repo";
|
|
30
|
+
export * from "./runtime-event-outbox.repo";
|
|
31
|
+
|
|
32
|
+
export class RuntimeDatabase {
|
|
33
|
+
readonly submissions: SubmissionRepository;
|
|
34
|
+
readonly approvals: ApprovalRepository;
|
|
35
|
+
readonly settlements: ToolSettlementRepository;
|
|
36
|
+
readonly milestones: RecoveryMilestoneRepository;
|
|
37
|
+
readonly extContext: ExtContextRepository;
|
|
38
|
+
readonly messageUi: MessageUiRepository;
|
|
39
|
+
readonly steers: SteerRepository;
|
|
40
|
+
readonly runtimeEvents: RuntimeEventOutboxRepository;
|
|
41
|
+
|
|
42
|
+
// 给各个 Repository 分配同一个 Agent SQLite 入口和事务入口。
|
|
43
|
+
// AgentRuntimeKernel 构造时只创建一次,业务代码随后通过对应属性访问仓储。
|
|
44
|
+
// 共享入口保证事务内的读写落在同一对象;不能用另一个 Agent 的 sql 或 transact 混用。
|
|
45
|
+
constructor(
|
|
46
|
+
private readonly sql: SqlTaggedTemplate,
|
|
47
|
+
private readonly transact: <T>(fn: () => T) => T,
|
|
48
|
+
) {
|
|
49
|
+
this.submissions = new SubmissionRepository(sql);
|
|
50
|
+
this.approvals = new ApprovalRepository(sql);
|
|
51
|
+
this.settlements = new ToolSettlementRepository(sql);
|
|
52
|
+
this.milestones = new RecoveryMilestoneRepository(sql);
|
|
53
|
+
this.extContext = new ExtContextRepository(sql);
|
|
54
|
+
this.messageUi = new MessageUiRepository(sql);
|
|
55
|
+
this.steers = new SteerRepository(sql);
|
|
56
|
+
this.runtimeEvents = new RuntimeEventOutboxRepository(sql);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 在一个同步 SQLite 事务里运行一组读写。
|
|
60
|
+
// Submission、Approval、Transcript 等需要一起成功或一起回滚时调用,回调不得返回 Promise。
|
|
61
|
+
// 这里直接保留 transactionSync 的抛错回滚语义;待确认:当前泛型签名无法在类型层阻止 async 回调。
|
|
62
|
+
transaction<T>(fn: () => T): T {
|
|
63
|
+
return this.transact(fn);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 确保当前 Runtime 需要的表已经存在。
|
|
67
|
+
// AgentRuntimeKernel 每次构造时调用,调用方不需要先检查表。
|
|
68
|
+
// 这里保留单一 schema 入口;具体的旧库升级边界见 initializeSchema 的待确认说明。
|
|
69
|
+
initializeSchema(): void {
|
|
70
|
+
initializeSchema(this.sql);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 删除所有持久化 Turn 行,但不删除 Extension Context 或 Message UI。
|
|
74
|
+
// 客户端 clear 事件会在同一 transactionSync 内先让 Transcript 清除会话和 Message UI,再调用本方法。
|
|
75
|
+
// Message UI 曾在这里删除,后来归还 Transcript 管理;不能把本方法当成整个 Agent 数据库的通用清空。
|
|
76
|
+
clearAll(): void {
|
|
77
|
+
this.sql`DELETE FROM pi_pending_steers`;
|
|
78
|
+
this.sql`DELETE FROM pi_submissions`;
|
|
79
|
+
this.sql`DELETE FROM pi_approvals`;
|
|
80
|
+
this.sql`DELETE FROM pi_tool_settlements`;
|
|
81
|
+
this.sql`DELETE FROM pi_recovery_milestones`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { SqlTaggedTemplate } from "agents/chat";
|
|
2
|
+
|
|
3
|
+
// Message UI 的统一定义见 ./index.ts。
|
|
4
|
+
export class MessageUiRepository {
|
|
5
|
+
// 保存当前 Agent SQLite 的查询入口。
|
|
6
|
+
// RuntimeDatabase 构造时调用,Transcript 随后通过这个 Repository 管理用户侧补充数据。
|
|
7
|
+
// 把存储绑定留在构造阶段,避免每次读写都传入可能属于其他 Agent 的 sql。
|
|
8
|
+
constructor(private readonly sql: SqlTaggedTemplate) {}
|
|
9
|
+
|
|
10
|
+
// 按消息 ID 读取用户界面补充数据,没有记录时返回 null。
|
|
11
|
+
// Transcript 组装浏览器消息时调用,调用方负责解析 body。
|
|
12
|
+
// Repository 故意不理解 JSON 格式,否则会与 Transcript 的 sidecar 契约形成两个真相源。
|
|
13
|
+
get(messageId: string): string | null {
|
|
14
|
+
return (
|
|
15
|
+
this.sql<{ body: string }>`
|
|
16
|
+
SELECT body FROM pi_message_ui
|
|
17
|
+
WHERE message_id = ${messageId}
|
|
18
|
+
`[0]?.body ?? null
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 按消息 ID 新增或替换用户界面补充数据。
|
|
23
|
+
// Transcript 写入用户消息 sidecar 时调用,重新投影同一消息可以安全覆盖。
|
|
24
|
+
// SQLite ON CONFLICT 利用 message_id 主键收口成一条路径,不能改成会因重复主键失败的纯 INSERT。
|
|
25
|
+
upsert(messageId: string, body: string): void {
|
|
26
|
+
this.sql`
|
|
27
|
+
INSERT INTO pi_message_ui (message_id, body)
|
|
28
|
+
VALUES (${messageId}, ${body})
|
|
29
|
+
ON CONFLICT(message_id) DO UPDATE SET body = excluded.body
|
|
30
|
+
`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 删除全部 Message UI 补充数据。
|
|
34
|
+
// Transcript.clear 在清空标准会话时调用,并由上层事务把两类数据一起提交。
|
|
35
|
+
// 该责任曾属于 RuntimeDatabase.clearAll,现在归 Transcript 所有;不能两处分别维护清理规则。
|
|
36
|
+
clear(): void {
|
|
37
|
+
this.sql`DELETE FROM pi_message_ui`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { SqlTaggedTemplate } from "agents/chat";
|
|
2
|
+
|
|
3
|
+
// Recovery Milestone 的统一定义见 ./index.ts。
|
|
4
|
+
export interface StoredMilestone {
|
|
5
|
+
submissionId: string;
|
|
6
|
+
seq: number;
|
|
7
|
+
milestoneKey: string;
|
|
8
|
+
body: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type MilestoneRow = {
|
|
12
|
+
submission_id: string;
|
|
13
|
+
seq: number;
|
|
14
|
+
milestone_key: string;
|
|
15
|
+
body: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// 把 SQLite 的 snake_case 行转成 Runtime 使用的 Milestone 对象。
|
|
19
|
+
// 待确认:当前 Repository 只返回 body,没有任何调用方使用这个映射函数。
|
|
20
|
+
// 这个边界可为完整 Milestone 查询隔离表列名,但在确认未来用途前不应继续围绕它增加代码。
|
|
21
|
+
function mapRow(row: MilestoneRow): StoredMilestone {
|
|
22
|
+
return {
|
|
23
|
+
submissionId: row.submission_id,
|
|
24
|
+
seq: row.seq,
|
|
25
|
+
milestoneKey: row.milestone_key,
|
|
26
|
+
body: row.body,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class RecoveryMilestoneRepository {
|
|
31
|
+
// 保存当前 Agent SQLite 的查询入口。
|
|
32
|
+
// RuntimeDatabase 构造时调用,Runtime 和 ApprovalLifecycle 随后共享这个 Repository。
|
|
33
|
+
// 恢复事实必须与 Submission 位于同一 Agent 存储中,不能用另一个 sql 入口拼接。
|
|
34
|
+
constructor(private readonly sql: SqlTaggedTemplate) {}
|
|
35
|
+
|
|
36
|
+
// 按写入顺序返回某次 Submission 的全部恢复内容。
|
|
37
|
+
// Runtime 和 ApprovalLifecycle 在计算下一个恢复决策前调用,调用方直接把结果交给 Pi 恢复器。
|
|
38
|
+
// ORDER BY seq 是重放契约,不能改成 milestoneKey 或无排序查询,否则决策顺序会变。
|
|
39
|
+
listBodies(submissionId: string): string[] {
|
|
40
|
+
return this.sql<{ body: string }>`
|
|
41
|
+
SELECT body
|
|
42
|
+
FROM pi_recovery_milestones
|
|
43
|
+
WHERE submission_id = ${submissionId}
|
|
44
|
+
ORDER BY seq ASC
|
|
45
|
+
`.map((row) => row.body);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 按 milestoneKey 只插入一次恢复事实,并告诉调用方是否真的新增。
|
|
49
|
+
// applyPiRecoveryMutations 在事务中调用,false 会让它对 terminal:intent 走定向更新。
|
|
50
|
+
// seq 用 MAX + 1 保留插入顺序,UNIQUE 约束是最后的去重防线;不能改成会覆盖任意旧事实的普通 UPSERT。
|
|
51
|
+
upsert(
|
|
52
|
+
submissionId: string,
|
|
53
|
+
milestoneKey: string,
|
|
54
|
+
body: string,
|
|
55
|
+
): boolean {
|
|
56
|
+
const existing =
|
|
57
|
+
this.sql<{ count: number }>`
|
|
58
|
+
SELECT COUNT(*) AS count
|
|
59
|
+
FROM pi_recovery_milestones
|
|
60
|
+
WHERE submission_id = ${submissionId}
|
|
61
|
+
AND milestone_key = ${milestoneKey}
|
|
62
|
+
`[0]?.count ?? 0;
|
|
63
|
+
if (existing > 0) return false;
|
|
64
|
+
|
|
65
|
+
const seq =
|
|
66
|
+
this.sql<{ next_seq: number }>`
|
|
67
|
+
SELECT COALESCE(MAX(seq), 0) + 1 AS next_seq
|
|
68
|
+
FROM pi_recovery_milestones
|
|
69
|
+
WHERE submission_id = ${submissionId}
|
|
70
|
+
`[0]?.next_seq ?? 1;
|
|
71
|
+
|
|
72
|
+
this.sql`
|
|
73
|
+
INSERT INTO pi_recovery_milestones (
|
|
74
|
+
submission_id, seq, milestone_key, body
|
|
75
|
+
) VALUES (
|
|
76
|
+
${submissionId},
|
|
77
|
+
${seq},
|
|
78
|
+
${milestoneKey},
|
|
79
|
+
${body}
|
|
80
|
+
)
|
|
81
|
+
`;
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 只替换已有 terminal:intent 的内容,不新建事实。
|
|
86
|
+
// applyPiRecoveryMutations 在 upsert 返回 false 且 key 是 terminal:intent 时调用。
|
|
87
|
+
// 该例外允许最终失败理由收敛,但不能放宽 WHERE 去改写其他已记录的恢复事实。
|
|
88
|
+
updateTerminalIntent(submissionId: string, body: string): void {
|
|
89
|
+
this.sql`
|
|
90
|
+
UPDATE pi_recovery_milestones
|
|
91
|
+
SET body = ${body}
|
|
92
|
+
WHERE submission_id = ${submissionId}
|
|
93
|
+
AND milestone_key = 'terminal:intent'
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { SqlTaggedTemplate } from "agents/chat";
|
|
2
|
+
|
|
3
|
+
export interface StoredRuntimeEvent {
|
|
4
|
+
eventId: string;
|
|
5
|
+
body: string;
|
|
6
|
+
createdAt: number;
|
|
7
|
+
deliveredAt: number | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface NewRuntimeEvent {
|
|
11
|
+
eventId: string;
|
|
12
|
+
body: string;
|
|
13
|
+
createdAt: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type RuntimeEventRow = {
|
|
17
|
+
event_id: string;
|
|
18
|
+
body: string;
|
|
19
|
+
created_at: number;
|
|
20
|
+
delivered_at: number | null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function mapRow(row: RuntimeEventRow): StoredRuntimeEvent {
|
|
24
|
+
return {
|
|
25
|
+
eventId: row.event_id,
|
|
26
|
+
body: row.body,
|
|
27
|
+
createdAt: row.created_at,
|
|
28
|
+
deliveredAt: row.delivered_at,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class RuntimeEventOutboxRepository {
|
|
33
|
+
constructor(private readonly sql: SqlTaggedTemplate) {}
|
|
34
|
+
|
|
35
|
+
insert(event: NewRuntimeEvent): boolean {
|
|
36
|
+
const existing = this.find(event.eventId);
|
|
37
|
+
if (existing) {
|
|
38
|
+
if (existing.body !== event.body) {
|
|
39
|
+
throw new Error(`Conflicting runtime event: ${event.eventId}`);
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
this.sql`
|
|
44
|
+
INSERT INTO pi_turn_event_outbox (
|
|
45
|
+
event_id, body, created_at, delivered_at
|
|
46
|
+
) VALUES (
|
|
47
|
+
${event.eventId}, ${event.body}, ${event.createdAt}, NULL
|
|
48
|
+
)
|
|
49
|
+
`;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
listPending(): StoredRuntimeEvent[] {
|
|
54
|
+
return this.sql<RuntimeEventRow>`
|
|
55
|
+
SELECT event_id, body, created_at, delivered_at
|
|
56
|
+
FROM pi_turn_event_outbox
|
|
57
|
+
WHERE delivered_at IS NULL
|
|
58
|
+
ORDER BY created_at, event_id
|
|
59
|
+
`.map(mapRow);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
hasPending(): boolean {
|
|
63
|
+
return Boolean(
|
|
64
|
+
this.sql<{ pending: number }>`
|
|
65
|
+
SELECT 1 AS pending
|
|
66
|
+
FROM pi_turn_event_outbox
|
|
67
|
+
WHERE delivered_at IS NULL
|
|
68
|
+
LIMIT 1
|
|
69
|
+
`[0],
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
markDelivered(eventId: string, deliveredAt: number): void {
|
|
74
|
+
this.sql`
|
|
75
|
+
UPDATE pi_turn_event_outbox
|
|
76
|
+
SET delivered_at = ${deliveredAt}
|
|
77
|
+
WHERE event_id = ${eventId} AND delivered_at IS NULL
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private find(eventId: string): StoredRuntimeEvent | null {
|
|
82
|
+
const row = this.sql<RuntimeEventRow>`
|
|
83
|
+
SELECT event_id, body, created_at, delivered_at
|
|
84
|
+
FROM pi_turn_event_outbox
|
|
85
|
+
WHERE event_id = ${eventId}
|
|
86
|
+
`[0];
|
|
87
|
+
return row ? mapRow(row) : null;
|
|
88
|
+
}
|
|
89
|
+
}
|