@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/src/db/schema.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { SqlTaggedTemplate } from "agents/chat";
|
|
2
|
+
|
|
3
|
+
// Repository 和各持久化记录的统一定义见 ./index.ts。
|
|
4
|
+
// 创建当前 Runtime 需要但尚不存在的 SQLite 表。
|
|
5
|
+
// AgentRuntimeKernel 构造时通过 RuntimeDatabase.initializeSchema 调用,重复启动依赖 IF NOT EXISTS 保留现有表。
|
|
6
|
+
// pi_approvals 有一次显式的 risk → 四档升级;步骤均可重入,中断后下次初始化会继续。
|
|
7
|
+
export function initializeSchema(sql: SqlTaggedTemplate): void {
|
|
8
|
+
sql`CREATE TABLE IF NOT EXISTS pi_message_ui (
|
|
9
|
+
message_id TEXT PRIMARY KEY,
|
|
10
|
+
body TEXT NOT NULL
|
|
11
|
+
)`;
|
|
12
|
+
sql`CREATE TABLE IF NOT EXISTS pi_submissions (
|
|
13
|
+
submission_id TEXT PRIMARY KEY,
|
|
14
|
+
request_id TEXT NOT NULL UNIQUE,
|
|
15
|
+
idempotency_key TEXT UNIQUE,
|
|
16
|
+
status TEXT NOT NULL,
|
|
17
|
+
accepted INTEGER NOT NULL,
|
|
18
|
+
error TEXT,
|
|
19
|
+
created_at INTEGER NOT NULL,
|
|
20
|
+
completed_at INTEGER,
|
|
21
|
+
assembly_revision TEXT NOT NULL,
|
|
22
|
+
assembly_descriptor TEXT NOT NULL,
|
|
23
|
+
assistant_message_id TEXT NOT NULL,
|
|
24
|
+
abort_reason TEXT
|
|
25
|
+
)`;
|
|
26
|
+
const submissionColumns = new Set(
|
|
27
|
+
sql<{ name: string }>`PRAGMA table_info(pi_submissions)`
|
|
28
|
+
.map((column) => column.name),
|
|
29
|
+
);
|
|
30
|
+
if (!submissionColumns.has("queued_input_json")) {
|
|
31
|
+
sql`ALTER TABLE pi_submissions ADD COLUMN queued_input_json TEXT`;
|
|
32
|
+
}
|
|
33
|
+
if (!submissionColumns.has("queued_ui_message_json")) {
|
|
34
|
+
sql`ALTER TABLE pi_submissions ADD COLUMN queued_ui_message_json TEXT`;
|
|
35
|
+
}
|
|
36
|
+
if (!submissionColumns.has("user_message_id")) {
|
|
37
|
+
sql`ALTER TABLE pi_submissions ADD COLUMN user_message_id TEXT`;
|
|
38
|
+
}
|
|
39
|
+
if (!submissionColumns.has("regenerate_message_id")) {
|
|
40
|
+
sql`ALTER TABLE pi_submissions ADD COLUMN regenerate_message_id TEXT`;
|
|
41
|
+
}
|
|
42
|
+
sql`CREATE UNIQUE INDEX IF NOT EXISTS pi_submissions_one_running
|
|
43
|
+
ON pi_submissions(status)
|
|
44
|
+
WHERE status = 'running'`;
|
|
45
|
+
sql`CREATE TABLE IF NOT EXISTS pi_pending_steers (
|
|
46
|
+
steer_id TEXT PRIMARY KEY,
|
|
47
|
+
submission_id TEXT NOT NULL,
|
|
48
|
+
message_id TEXT NOT NULL UNIQUE,
|
|
49
|
+
canonical_json TEXT NOT NULL,
|
|
50
|
+
ui_message_json TEXT,
|
|
51
|
+
created_at INTEGER NOT NULL
|
|
52
|
+
)`;
|
|
53
|
+
sql`CREATE TABLE IF NOT EXISTS pi_tool_settlements (
|
|
54
|
+
submission_id TEXT NOT NULL,
|
|
55
|
+
tool_call_id TEXT NOT NULL,
|
|
56
|
+
tool_name TEXT NOT NULL,
|
|
57
|
+
args TEXT NOT NULL,
|
|
58
|
+
result TEXT NOT NULL,
|
|
59
|
+
is_error INTEGER NOT NULL,
|
|
60
|
+
created_at INTEGER NOT NULL,
|
|
61
|
+
PRIMARY KEY (submission_id, tool_call_id)
|
|
62
|
+
)`;
|
|
63
|
+
sql`CREATE TABLE IF NOT EXISTS pi_turn_event_outbox (
|
|
64
|
+
event_id TEXT PRIMARY KEY,
|
|
65
|
+
body TEXT NOT NULL,
|
|
66
|
+
created_at INTEGER NOT NULL,
|
|
67
|
+
delivered_at INTEGER
|
|
68
|
+
)`;
|
|
69
|
+
sql`CREATE TABLE IF NOT EXISTS pi_approvals (
|
|
70
|
+
execution_id TEXT PRIMARY KEY,
|
|
71
|
+
submission_id TEXT NOT NULL,
|
|
72
|
+
request_id TEXT NOT NULL,
|
|
73
|
+
source TEXT NOT NULL,
|
|
74
|
+
tool_call_id TEXT NOT NULL,
|
|
75
|
+
tool_name TEXT NOT NULL,
|
|
76
|
+
summary TEXT NOT NULL,
|
|
77
|
+
execution_level TEXT NOT NULL,
|
|
78
|
+
required_level TEXT NOT NULL,
|
|
79
|
+
input_json TEXT NOT NULL,
|
|
80
|
+
status TEXT NOT NULL,
|
|
81
|
+
created_at INTEGER NOT NULL,
|
|
82
|
+
decided_at INTEGER,
|
|
83
|
+
reason TEXT,
|
|
84
|
+
UNIQUE (submission_id, tool_call_id)
|
|
85
|
+
)`;
|
|
86
|
+
const approvalColumns = () =>
|
|
87
|
+
new Set(
|
|
88
|
+
sql<{ name: string }>`PRAGMA table_info(pi_approvals)`
|
|
89
|
+
.map((column) => column.name),
|
|
90
|
+
);
|
|
91
|
+
let columns = approvalColumns();
|
|
92
|
+
if (!columns.has("execution_level")) {
|
|
93
|
+
// 旧 Session 没有持久化当时的授权档位,safe 失败关闭;只影响旧 pending 回执展示,不会静默放行。
|
|
94
|
+
sql`ALTER TABLE pi_approvals
|
|
95
|
+
ADD COLUMN execution_level TEXT NOT NULL DEFAULT 'safe'`;
|
|
96
|
+
columns = approvalColumns();
|
|
97
|
+
}
|
|
98
|
+
if (!columns.has("required_level")) {
|
|
99
|
+
sql`ALTER TABLE pi_approvals
|
|
100
|
+
ADD COLUMN required_level TEXT NOT NULL DEFAULT 'safe'`;
|
|
101
|
+
columns = approvalColumns();
|
|
102
|
+
}
|
|
103
|
+
if (columns.has("risk")) {
|
|
104
|
+
sql`UPDATE pi_approvals
|
|
105
|
+
SET required_level = CASE risk
|
|
106
|
+
WHEN 'low' THEN 'low'
|
|
107
|
+
WHEN 'medium' THEN 'medium'
|
|
108
|
+
WHEN 'high' THEN 'high'
|
|
109
|
+
ELSE 'safe'
|
|
110
|
+
END`;
|
|
111
|
+
sql`ALTER TABLE pi_approvals DROP COLUMN risk`;
|
|
112
|
+
}
|
|
113
|
+
sql`CREATE TABLE IF NOT EXISTS pi_recovery_milestones (
|
|
114
|
+
submission_id TEXT NOT NULL,
|
|
115
|
+
seq INTEGER NOT NULL,
|
|
116
|
+
milestone_key TEXT NOT NULL,
|
|
117
|
+
body TEXT NOT NULL,
|
|
118
|
+
PRIMARY KEY (submission_id, seq),
|
|
119
|
+
UNIQUE (submission_id, milestone_key)
|
|
120
|
+
)`;
|
|
121
|
+
sql`UPDATE pi_recovery_milestones
|
|
122
|
+
SET body = json_remove(
|
|
123
|
+
json_set(
|
|
124
|
+
body,
|
|
125
|
+
'$.approval.executionLevel', 'safe',
|
|
126
|
+
'$.approval.requiredExecutionLevel',
|
|
127
|
+
COALESCE(json_extract(body, '$.approval.risk'), 'safe')
|
|
128
|
+
),
|
|
129
|
+
'$.approval.risk'
|
|
130
|
+
)
|
|
131
|
+
WHERE json_valid(body)
|
|
132
|
+
AND json_extract(body, '$.version') = 1
|
|
133
|
+
AND json_extract(body, '$.type') = 'approval'
|
|
134
|
+
AND json_type(body, '$.turnId') = 'text'
|
|
135
|
+
AND json_type(body, '$.assemblyRevision') = 'text'
|
|
136
|
+
AND json_type(body, '$.approval') = 'object'
|
|
137
|
+
AND json_type(body, '$.approval.executionId') = 'text'
|
|
138
|
+
AND json_type(body, '$.approval.requestId') = 'text'
|
|
139
|
+
AND json_extract(body, '$.approval.source') IN (
|
|
140
|
+
'action', 'codemode', 'temporary-agent'
|
|
141
|
+
)
|
|
142
|
+
AND json_type(body, '$.approval.toolCallId') = 'text'
|
|
143
|
+
AND json_type(body, '$.approval.toolName') = 'text'
|
|
144
|
+
AND json_type(body, '$.approval.summary') = 'text'
|
|
145
|
+
AND json_type(body, '$.approval.inputJson') = 'text'
|
|
146
|
+
AND json_type(body, '$.approval.createdAt') IN ('integer', 'real')
|
|
147
|
+
AND json_extract(body, '$.approval.status') IN (
|
|
148
|
+
'pending', 'approved', 'rejected'
|
|
149
|
+
)
|
|
150
|
+
AND (
|
|
151
|
+
json_type(body, '$.approval.risk') IS NULL
|
|
152
|
+
OR json_type(body, '$.approval.risk') = 'null'
|
|
153
|
+
OR json_extract(body, '$.approval.risk') IN (
|
|
154
|
+
'low', 'medium', 'high'
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
AND json_type(body, '$.approval.executionLevel') IS NULL
|
|
158
|
+
AND json_type(body, '$.approval.requiredExecutionLevel') IS NULL`;
|
|
159
|
+
sql`CREATE TABLE IF NOT EXISTS pi_extension_context (
|
|
160
|
+
label TEXT PRIMARY KEY,
|
|
161
|
+
content TEXT NOT NULL,
|
|
162
|
+
updated_at INTEGER NOT NULL
|
|
163
|
+
)`;
|
|
164
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { SqlTaggedTemplate } from "agents/chat";
|
|
2
|
+
|
|
3
|
+
// Settlement 的统一定义见 ./index.ts。
|
|
4
|
+
export interface StoredToolSettlement {
|
|
5
|
+
toolCallId: string;
|
|
6
|
+
submissionId: string;
|
|
7
|
+
toolName: string;
|
|
8
|
+
args: string;
|
|
9
|
+
result: string;
|
|
10
|
+
isError: boolean;
|
|
11
|
+
createdAt: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface NewToolSettlement {
|
|
15
|
+
toolCallId: string;
|
|
16
|
+
submissionId: string;
|
|
17
|
+
toolName: string;
|
|
18
|
+
args: string;
|
|
19
|
+
result: string;
|
|
20
|
+
isError: boolean;
|
|
21
|
+
createdAt: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type SettlementRow = {
|
|
25
|
+
tool_call_id: string;
|
|
26
|
+
submission_id: string;
|
|
27
|
+
tool_name: string;
|
|
28
|
+
args: string;
|
|
29
|
+
result: string;
|
|
30
|
+
is_error: number;
|
|
31
|
+
created_at: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// 把 SQLite 行转成 Runtime 使用的工具结果对象。
|
|
35
|
+
// find 查到记录后调用,上层因而只看 camelCase 字段和 boolean。
|
|
36
|
+
// SQLite 用 0/1 存 is_error,转换必须留在此边界;增加列时要同步核对类型、SELECT 和映射。
|
|
37
|
+
function mapRow(row: SettlementRow): StoredToolSettlement {
|
|
38
|
+
return {
|
|
39
|
+
toolCallId: row.tool_call_id,
|
|
40
|
+
submissionId: row.submission_id,
|
|
41
|
+
toolName: row.tool_name,
|
|
42
|
+
args: row.args,
|
|
43
|
+
result: row.result,
|
|
44
|
+
isError: row.is_error === 1,
|
|
45
|
+
createdAt: row.created_at,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class ToolSettlementRepository {
|
|
50
|
+
// 保存当前 Agent SQLite 的查询入口。
|
|
51
|
+
// RuntimeDatabase 构造时调用,工具执行和恢复流程随后共享这个 Repository。
|
|
52
|
+
// Settlement 要与所属 Submission 同库持久化,否则重放时无法一致查重。
|
|
53
|
+
constructor(private readonly sql: SqlTaggedTemplate) {}
|
|
54
|
+
|
|
55
|
+
// 按 Submission 和工具调用 ID 读取已落盘的最终结果。
|
|
56
|
+
// settleTool 写入前和 readToolSettlement 恢复时调用,没有记录时返回 null。
|
|
57
|
+
// 两列共同定位表的复合主键,不能只按 toolCallId 查询而把其他 Submission 的结果当成当前结果。
|
|
58
|
+
find(
|
|
59
|
+
submissionId: string,
|
|
60
|
+
toolCallId: string,
|
|
61
|
+
): StoredToolSettlement | null {
|
|
62
|
+
const row = this.sql<SettlementRow>`
|
|
63
|
+
SELECT tool_call_id, submission_id, tool_name, args, result,
|
|
64
|
+
is_error, created_at
|
|
65
|
+
FROM pi_tool_settlements
|
|
66
|
+
WHERE submission_id = ${submissionId}
|
|
67
|
+
AND tool_call_id = ${toolCallId}
|
|
68
|
+
`[0];
|
|
69
|
+
return row ? mapRow(row) : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 持久化一条尚未存在的工具最终结果。
|
|
73
|
+
// settleTool 在事务中确认 find 返回 null 后调用,冲突的重复结果由上层拒绝。
|
|
74
|
+
// 复合主键会让未经查重的重复写入直接失败,不能用 INSERT OR REPLACE 掩盖不一致的工具结果。
|
|
75
|
+
insert(s: NewToolSettlement): void {
|
|
76
|
+
this.sql`
|
|
77
|
+
INSERT INTO pi_tool_settlements (
|
|
78
|
+
tool_call_id, submission_id, tool_name, args, result,
|
|
79
|
+
is_error, created_at
|
|
80
|
+
) VALUES (
|
|
81
|
+
${s.toolCallId},
|
|
82
|
+
${s.submissionId},
|
|
83
|
+
${s.toolName},
|
|
84
|
+
${s.args},
|
|
85
|
+
${s.result},
|
|
86
|
+
${s.isError ? 1 : 0},
|
|
87
|
+
${s.createdAt}
|
|
88
|
+
)
|
|
89
|
+
`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 统计某次 Submission 已落盘的工具结果数。
|
|
93
|
+
// Runtime 生成恢复快照时调用,没有记录时应得到 0。
|
|
94
|
+
// 这里以数据库为真相源,不能改成内存计数,否则 Durable Object 重启后会丢失进度。
|
|
95
|
+
countForSubmission(submissionId: string): number {
|
|
96
|
+
return (
|
|
97
|
+
this.sql<{ count: number }>`
|
|
98
|
+
SELECT COUNT(*) AS count
|
|
99
|
+
FROM pi_tool_settlements
|
|
100
|
+
WHERE submission_id = ${submissionId}
|
|
101
|
+
`[0]?.count ?? 0
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { SqlTaggedTemplate } from "agents/chat";
|
|
2
|
+
|
|
3
|
+
export interface PendingSteer {
|
|
4
|
+
steerId: string;
|
|
5
|
+
submissionId: string;
|
|
6
|
+
messageId: string;
|
|
7
|
+
canonicalJson: string;
|
|
8
|
+
uiMessageJson: string | null;
|
|
9
|
+
createdAt: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type PendingSteerRow = {
|
|
13
|
+
steer_id: string;
|
|
14
|
+
submission_id: string;
|
|
15
|
+
message_id: string;
|
|
16
|
+
canonical_json: string;
|
|
17
|
+
ui_message_json: string | null;
|
|
18
|
+
created_at: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function mapRow(row: PendingSteerRow): PendingSteer {
|
|
22
|
+
return {
|
|
23
|
+
steerId: row.steer_id,
|
|
24
|
+
submissionId: row.submission_id,
|
|
25
|
+
messageId: row.message_id,
|
|
26
|
+
canonicalJson: row.canonical_json,
|
|
27
|
+
uiMessageJson: row.ui_message_json,
|
|
28
|
+
createdAt: row.created_at,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class SteerRepository {
|
|
33
|
+
constructor(private readonly sql: SqlTaggedTemplate) {}
|
|
34
|
+
|
|
35
|
+
insert(steer: PendingSteer): void {
|
|
36
|
+
this.sql`
|
|
37
|
+
INSERT INTO pi_pending_steers (
|
|
38
|
+
steer_id, submission_id, message_id, canonical_json,
|
|
39
|
+
ui_message_json, created_at
|
|
40
|
+
) VALUES (
|
|
41
|
+
${steer.steerId}, ${steer.submissionId}, ${steer.messageId},
|
|
42
|
+
${steer.canonicalJson}, ${steer.uiMessageJson}, ${steer.createdAt}
|
|
43
|
+
)
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
listForSubmission(submissionId: string): PendingSteer[] {
|
|
48
|
+
return this.sql<PendingSteerRow>`
|
|
49
|
+
SELECT steer_id, submission_id, message_id, canonical_json,
|
|
50
|
+
ui_message_json, created_at
|
|
51
|
+
FROM pi_pending_steers
|
|
52
|
+
WHERE submission_id = ${submissionId}
|
|
53
|
+
ORDER BY created_at ASC, rowid ASC
|
|
54
|
+
`.map(mapRow);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
findByMessageId(messageId: string): PendingSteer | null {
|
|
58
|
+
const row = this.sql<PendingSteerRow>`
|
|
59
|
+
SELECT steer_id, submission_id, message_id, canonical_json,
|
|
60
|
+
ui_message_json, created_at
|
|
61
|
+
FROM pi_pending_steers
|
|
62
|
+
WHERE message_id = ${messageId}
|
|
63
|
+
`[0];
|
|
64
|
+
return row ? mapRow(row) : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
deleteByMessageId(messageId: string): void {
|
|
68
|
+
this.sql`
|
|
69
|
+
DELETE FROM pi_pending_steers
|
|
70
|
+
WHERE message_id = ${messageId}
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import type { SqlTaggedTemplate } from "agents/chat";
|
|
2
|
+
|
|
3
|
+
// Submission 的统一定义见 ./index.ts。
|
|
4
|
+
// #region 状态与行类型
|
|
5
|
+
export type SubmissionStatus =
|
|
6
|
+
| "pending"
|
|
7
|
+
| "running"
|
|
8
|
+
| "completed"
|
|
9
|
+
| "aborted"
|
|
10
|
+
| "skipped"
|
|
11
|
+
| "error";
|
|
12
|
+
|
|
13
|
+
const TERMINAL_SUBMISSION_STATUSES: ReadonlySet<SubmissionStatus> = new Set([
|
|
14
|
+
"completed",
|
|
15
|
+
"aborted",
|
|
16
|
+
"skipped",
|
|
17
|
+
"error",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
// 判断 Submission 是否已经结束。
|
|
21
|
+
// Runtime、SubmissionLifecycle 和 ApprovalLifecycle 在决定是否继续执行、等待或接受审批时调用。
|
|
22
|
+
// 终态集合收口在一处防止各生命周期的判断漂移;增加状态时必须先确认它是否真的不可再继续。
|
|
23
|
+
export function isTerminalSubmissionStatus(
|
|
24
|
+
status: SubmissionStatus,
|
|
25
|
+
): boolean {
|
|
26
|
+
return TERMINAL_SUBMISSION_STATUSES.has(status);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface StoredSubmission {
|
|
30
|
+
submissionId: string;
|
|
31
|
+
requestId: string;
|
|
32
|
+
idempotencyKey: string | null;
|
|
33
|
+
status: SubmissionStatus;
|
|
34
|
+
accepted: boolean;
|
|
35
|
+
error: string | null;
|
|
36
|
+
createdAt: number;
|
|
37
|
+
completedAt: number | null;
|
|
38
|
+
assemblyRevision: string;
|
|
39
|
+
assemblyDescriptor: string;
|
|
40
|
+
assistantMessageId: string;
|
|
41
|
+
abortReason: string | null;
|
|
42
|
+
queuedInputJson: string | null;
|
|
43
|
+
queuedUiMessageJson: string | null;
|
|
44
|
+
userMessageId: string | null;
|
|
45
|
+
regenerateMessageId: string | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface NewSubmission {
|
|
49
|
+
submissionId: string;
|
|
50
|
+
requestId: string;
|
|
51
|
+
idempotencyKey: string | null;
|
|
52
|
+
createdAt: number;
|
|
53
|
+
assemblyRevision: string;
|
|
54
|
+
assemblyDescriptor: string;
|
|
55
|
+
assistantMessageId: string;
|
|
56
|
+
queuedInputJson?: string | null;
|
|
57
|
+
queuedUiMessageJson?: string | null;
|
|
58
|
+
userMessageId?: string | null;
|
|
59
|
+
regenerateMessageId?: string | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type SubmissionRow = {
|
|
63
|
+
submission_id: string;
|
|
64
|
+
request_id: string;
|
|
65
|
+
idempotency_key: string | null;
|
|
66
|
+
status: string;
|
|
67
|
+
accepted: number;
|
|
68
|
+
error: string | null;
|
|
69
|
+
created_at: number;
|
|
70
|
+
completed_at: number | null;
|
|
71
|
+
assembly_revision: string;
|
|
72
|
+
assembly_descriptor: string;
|
|
73
|
+
assistant_message_id: string;
|
|
74
|
+
abort_reason: string | null;
|
|
75
|
+
queued_input_json: string | null;
|
|
76
|
+
queued_ui_message_json: string | null;
|
|
77
|
+
user_message_id: string | null;
|
|
78
|
+
regenerate_message_id: string | null;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// #endregion
|
|
82
|
+
// #region 行映射
|
|
83
|
+
// 把 SQLite 的 Submission 行转成 Runtime 对象。
|
|
84
|
+
// find 查到记录后调用,所有其他查询方法通过 find 共用这个映射。
|
|
85
|
+
// SQLite 用 0/1 存 accepted,状态则由表内字符串转成联合类型;新增列时必须同步核对类型、SELECT 和映射。
|
|
86
|
+
function mapRow(row: SubmissionRow): StoredSubmission {
|
|
87
|
+
return {
|
|
88
|
+
submissionId: row.submission_id,
|
|
89
|
+
requestId: row.request_id,
|
|
90
|
+
idempotencyKey: row.idempotency_key,
|
|
91
|
+
status: row.status as SubmissionStatus,
|
|
92
|
+
accepted: row.accepted === 1,
|
|
93
|
+
error: row.error,
|
|
94
|
+
createdAt: row.created_at,
|
|
95
|
+
completedAt: row.completed_at,
|
|
96
|
+
assemblyRevision: row.assembly_revision,
|
|
97
|
+
assemblyDescriptor: row.assembly_descriptor,
|
|
98
|
+
assistantMessageId: row.assistant_message_id,
|
|
99
|
+
abortReason: row.abort_reason,
|
|
100
|
+
queuedInputJson: row.queued_input_json,
|
|
101
|
+
queuedUiMessageJson: row.queued_ui_message_json,
|
|
102
|
+
userMessageId: row.user_message_id,
|
|
103
|
+
regenerateMessageId: row.regenerate_message_id,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// #endregion
|
|
108
|
+
// #region Repository
|
|
109
|
+
export class SubmissionRepository {
|
|
110
|
+
// 保存当前 Agent SQLite 的查询入口。
|
|
111
|
+
// RuntimeDatabase 构造时调用,SubmissionLifecycle 和 Runtime 随后共享这个 Repository。
|
|
112
|
+
// Submission 的准入、执行和恢复必须看同一 Agent 存储,不能混入其他 Durable Object 的 sql。
|
|
113
|
+
constructor(private readonly sql: SqlTaggedTemplate) {}
|
|
114
|
+
|
|
115
|
+
// 按 submissionId 读取一条完整 Submission,不存在时返回 null。
|
|
116
|
+
// Runtime 和 SubmissionLifecycle 在准入、执行、取消、结束与恢复时调用,调用方必须处理 null。
|
|
117
|
+
// 所有列在这里一次投影并统一映射,不能让不同调用方各自拼出不完整的 Submission。
|
|
118
|
+
find(id: string): StoredSubmission | null {
|
|
119
|
+
const row = this.sql<SubmissionRow>`
|
|
120
|
+
SELECT submission_id, request_id, idempotency_key, status, accepted,
|
|
121
|
+
error, created_at, completed_at, assembly_revision,
|
|
122
|
+
assembly_descriptor, assistant_message_id, abort_reason,
|
|
123
|
+
queued_input_json, queued_ui_message_json, user_message_id,
|
|
124
|
+
regenerate_message_id
|
|
125
|
+
FROM pi_submissions
|
|
126
|
+
WHERE submission_id = ${id}
|
|
127
|
+
`[0];
|
|
128
|
+
return row ? mapRow(row) : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 先按对外 requestId 找到 Submission,再返回完整记录。
|
|
132
|
+
// SubmissionLifecycle 用它合并重复请求和按请求停止任务,Runtime 也用它处理客户端事件。
|
|
133
|
+
// request_id 有 UNIQUE 约束,先取主键再复用 find 可以保持唯一映射路径。
|
|
134
|
+
findByRequestId(requestId: string): StoredSubmission | null {
|
|
135
|
+
const id = this.sql<{ submission_id: string }>`
|
|
136
|
+
SELECT submission_id FROM pi_submissions
|
|
137
|
+
WHERE request_id = ${requestId}
|
|
138
|
+
`[0]?.submission_id;
|
|
139
|
+
return id ? this.find(id) : null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 先按幂等键找到 Submission,再返回完整记录。
|
|
143
|
+
// SubmissionLifecycle 在准入前和等待稳定时调用,重试应复用已有 Submission 而不是重复执行。
|
|
144
|
+
// idempotency_key 有 UNIQUE 约束作为最终防线,不能把它降级为普通非唯一字段。
|
|
145
|
+
findByIdempotencyKey(key: string): StoredSubmission | null {
|
|
146
|
+
const id = this.sql<{ submission_id: string }>`
|
|
147
|
+
SELECT submission_id FROM pi_submissions
|
|
148
|
+
WHERE idempotency_key = ${key}
|
|
149
|
+
`[0]?.submission_id;
|
|
150
|
+
return id ? this.find(id) : null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 计算当前还在排队或运行的 Submission 数量。
|
|
154
|
+
// SubmissionLifecycle 在准入新 Turn、查询忙碌状态和等待稳定时调用。
|
|
155
|
+
// 持久化计数能跨 Durable Object 重启保留真实状态,不能换成只反映当前进程的内存标志。
|
|
156
|
+
countUnfinished(): number {
|
|
157
|
+
return (
|
|
158
|
+
this.sql<{ count: number }>`
|
|
159
|
+
SELECT COUNT(*) AS count FROM pi_submissions
|
|
160
|
+
WHERE status IN ('pending', 'running')
|
|
161
|
+
`[0]?.count ?? 0
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
countPending(): number {
|
|
166
|
+
return (
|
|
167
|
+
this.sql<{ count: number }>`
|
|
168
|
+
SELECT COUNT(*) AS count FROM pi_submissions
|
|
169
|
+
WHERE status = 'pending'
|
|
170
|
+
`[0]?.count ?? 0
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
findRunning(): StoredSubmission | null {
|
|
175
|
+
const id = this.sql<{ submission_id: string }>`
|
|
176
|
+
SELECT submission_id FROM pi_submissions
|
|
177
|
+
WHERE status = 'running'
|
|
178
|
+
LIMIT 1
|
|
179
|
+
`[0]?.submission_id;
|
|
180
|
+
return id ? this.find(id) : null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
findNextPending(): StoredSubmission | null {
|
|
184
|
+
const id = this.sql<{ submission_id: string }>`
|
|
185
|
+
SELECT submission_id FROM pi_submissions
|
|
186
|
+
WHERE status = 'pending'
|
|
187
|
+
ORDER BY created_at ASC, rowid ASC
|
|
188
|
+
LIMIT 1
|
|
189
|
+
`[0]?.submission_id;
|
|
190
|
+
return id ? this.find(id) : null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
listPending(): StoredSubmission[] {
|
|
194
|
+
return this.sql<{ submission_id: string }>`
|
|
195
|
+
SELECT submission_id FROM pi_submissions
|
|
196
|
+
WHERE status = 'pending'
|
|
197
|
+
ORDER BY created_at ASC, rowid ASC
|
|
198
|
+
`.flatMap((row) => {
|
|
199
|
+
const submission = this.find(row.submission_id);
|
|
200
|
+
return submission ? [submission] : [];
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 用固定的初始状态写入一条新 Submission。
|
|
205
|
+
// Runtime 在 SubmissionLifecycle 的准入事务中调用,调用方只提供本次请求确定的数据。
|
|
206
|
+
// pending 和 accepted=1 是准入契约,不能交给任意调用方选择而绕过生命周期。
|
|
207
|
+
insert(s: NewSubmission): void {
|
|
208
|
+
this.sql`
|
|
209
|
+
INSERT INTO pi_submissions (
|
|
210
|
+
submission_id, request_id, idempotency_key, status, accepted,
|
|
211
|
+
created_at, assembly_revision, assembly_descriptor,
|
|
212
|
+
assistant_message_id, queued_input_json, queued_ui_message_json,
|
|
213
|
+
user_message_id, regenerate_message_id
|
|
214
|
+
) VALUES (
|
|
215
|
+
${s.submissionId},
|
|
216
|
+
${s.requestId},
|
|
217
|
+
${s.idempotencyKey ?? null},
|
|
218
|
+
'pending',
|
|
219
|
+
1,
|
|
220
|
+
${s.createdAt},
|
|
221
|
+
${s.assemblyRevision},
|
|
222
|
+
${s.assemblyDescriptor},
|
|
223
|
+
${s.assistantMessageId},
|
|
224
|
+
${s.queuedInputJson ?? null},
|
|
225
|
+
${s.queuedUiMessageJson ?? null},
|
|
226
|
+
${s.userMessageId ?? null},
|
|
227
|
+
${s.regenerateMessageId ?? null}
|
|
228
|
+
)
|
|
229
|
+
`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
pinAssembly(id: string, revision: string, descriptor: string): boolean {
|
|
233
|
+
const updated = this.sql<{ submission_id: string }>`
|
|
234
|
+
UPDATE pi_submissions
|
|
235
|
+
SET assembly_revision = ${revision},
|
|
236
|
+
assembly_descriptor = ${descriptor}
|
|
237
|
+
WHERE submission_id = ${id}
|
|
238
|
+
AND assembly_revision = ''
|
|
239
|
+
AND assembly_descriptor = ''
|
|
240
|
+
AND status IN ('pending', 'running')
|
|
241
|
+
RETURNING submission_id
|
|
242
|
+
`;
|
|
243
|
+
return updated.length === 1;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 只在当前状态属于 from 时把 Submission 切换到 to,否则返回 false。
|
|
247
|
+
// SubmissionLifecycle 开始 Turn 时调用,调用方用返回值识别过期或重复的状态切换。
|
|
248
|
+
// Cloudflare SQLite 查询是同步的,两条语句之间不会让其他事件交错;UPDATE 再次带旧状态条件,不能去掉这层幂等保护。
|
|
249
|
+
transition(
|
|
250
|
+
id: string,
|
|
251
|
+
to: SubmissionStatus,
|
|
252
|
+
from: readonly SubmissionStatus[],
|
|
253
|
+
): boolean {
|
|
254
|
+
if (from.length === 0) return false;
|
|
255
|
+
const row = this.sql<{ status: string }>`
|
|
256
|
+
SELECT status FROM pi_submissions WHERE submission_id = ${id}
|
|
257
|
+
`[0];
|
|
258
|
+
if (!row || !(from as readonly string[]).includes(row.status)) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
this.sql`
|
|
262
|
+
UPDATE pi_submissions
|
|
263
|
+
SET status = ${to}
|
|
264
|
+
WHERE submission_id = ${id}
|
|
265
|
+
AND status = ${row.status}
|
|
266
|
+
`;
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
clearQueuedPayload(id: string): void {
|
|
271
|
+
this.sql`
|
|
272
|
+
UPDATE pi_submissions
|
|
273
|
+
SET queued_input_json = NULL,
|
|
274
|
+
queued_ui_message_json = NULL,
|
|
275
|
+
regenerate_message_id = NULL
|
|
276
|
+
WHERE submission_id = ${id}
|
|
277
|
+
`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// 把尚未结束的 Submission 写成终态,并记录错误与完成时间。
|
|
281
|
+
// Runtime 提交 Turn 的最终结果时调用,completedAt 主要供可重现的调用或测试传入。
|
|
282
|
+
// WHERE 排除所有终态来保留第一次结果;待确认:参数类型仍允许 pending/running,调用方当前只传终态。
|
|
283
|
+
updateTerminal(
|
|
284
|
+
id: string,
|
|
285
|
+
status: SubmissionStatus,
|
|
286
|
+
error?: string,
|
|
287
|
+
completedAt?: number,
|
|
288
|
+
): void {
|
|
289
|
+
const ts = completedAt ?? Date.now();
|
|
290
|
+
this.sql`
|
|
291
|
+
UPDATE pi_submissions
|
|
292
|
+
SET status = ${status},
|
|
293
|
+
error = ${error ?? null},
|
|
294
|
+
completed_at = ${ts}
|
|
295
|
+
WHERE submission_id = ${id}
|
|
296
|
+
AND status NOT IN ('completed', 'aborted', 'skipped', 'error')
|
|
297
|
+
`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 给活跃 Submission 记录取消或停止原因。
|
|
301
|
+
// SubmissionLifecycle.cancel 在同一事务内写入终止意图前调用,执行中的 Turn 随后读取它。
|
|
302
|
+
// 只更新 pending/running 防止迟到的取消覆盖已结束记录,不能去掉状态条件。
|
|
303
|
+
updateAbortReason(id: string, reason: string): void {
|
|
304
|
+
this.sql`
|
|
305
|
+
UPDATE pi_submissions
|
|
306
|
+
SET abort_reason = ${reason}
|
|
307
|
+
WHERE submission_id = ${id}
|
|
308
|
+
AND status IN ('pending', 'running')
|
|
309
|
+
`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// 返回所有标准会话消息到 Submission 的关联映射。
|
|
313
|
+
// Transcript.storedMessages 读取 Pi Session 分支时调用,它用 Map 为每条消息补回 submissionId。
|
|
314
|
+
// 关联表由 Pi Session 存储拥有,本方法只读不写;不能在 RuntimeDatabase.clearAll 中单独删它而破坏 Transcript 的所有权。
|
|
315
|
+
listMessageSubmissionLinks(): Map<string, string> {
|
|
316
|
+
const rows = this.sql<{
|
|
317
|
+
message_id: string;
|
|
318
|
+
submission_id: string;
|
|
319
|
+
}>`SELECT message_id, submission_id FROM pi_session_message_submission`;
|
|
320
|
+
return new Map(rows.map((r) => [r.message_id, r.submission_id] as [string, string]));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// #endregion
|