@sixb/sqlite 0.1.21 → 0.1.23
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/dist/agents/agent-storage.d.ts +8 -2
- package/dist/agents/agent-storage.d.ts.map +1 -1
- package/dist/agents/approvals.d.ts +51 -0
- package/dist/agents/approvals.d.ts.map +1 -0
- package/dist/agents/checkpoints.d.ts +1 -0
- package/dist/agents/checkpoints.d.ts.map +1 -1
- package/dist/agents/index.d.ts +1 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/memory.d.ts +21 -0
- package/dist/agents/memory.d.ts.map +1 -0
- package/dist/agents/messages.d.ts.map +1 -1
- package/dist/agents/rows.d.ts +15 -1
- package/dist/agents/rows.d.ts.map +1 -1
- package/dist/agents/runs.d.ts +14 -1
- package/dist/agents/runs.d.ts.map +1 -1
- package/dist/agents/settings.d.ts +29 -0
- package/dist/agents/settings.d.ts.map +1 -0
- package/dist/agents/threads.d.ts +12 -1
- package/dist/agents/threads.d.ts.map +1 -1
- package/dist/ai-cost-groups.d.ts.map +1 -1
- package/dist/ai-cost-storage.d.ts.map +1 -1
- package/dist/ai-usage-storage.d.ts +4 -1
- package/dist/ai-usage-storage.d.ts.map +1 -1
- package/dist/artifact-storage.d.ts +10 -1
- package/dist/artifact-storage.d.ts.map +1 -1
- package/dist/auth-storage/auth-storage.d.ts +3 -2
- package/dist/auth-storage/auth-storage.d.ts.map +1 -1
- package/dist/auth-storage/users.d.ts +7 -3
- package/dist/auth-storage/users.d.ts.map +1 -1
- package/dist/execution-storage.d.ts +2 -1
- package/dist/execution-storage.d.ts.map +1 -1
- package/dist/index.js +3348 -1515
- package/dist/index.js.map +32 -26
- package/dist/migrations.d.ts +0 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/conditional-reader.d.ts +8 -0
- package/dist/objects/conditional-reader.d.ts.map +1 -0
- package/dist/objects/query-compiler.d.ts +26 -1
- package/dist/objects/query-compiler.d.ts.map +1 -1
- package/dist/objects/read-scope.d.ts +10 -1
- package/dist/objects/read-scope.d.ts.map +1 -1
- package/dist/objects/reader.d.ts +2 -0
- package/dist/objects/reader.d.ts.map +1 -1
- package/dist/objects/scoped-reader.d.ts +15 -0
- package/dist/objects/scoped-reader.d.ts.map +1 -0
- package/dist/objects/selected-reader.d.ts.map +1 -1
- package/dist/objects/storage.d.ts +6 -1
- package/dist/objects/storage.d.ts.map +1 -1
- package/dist/ontology-storage/index.d.ts +2 -0
- package/dist/ontology-storage/index.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-writer.d.ts +2 -0
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
- package/dist/ontology-storage/materializations.d.ts.map +1 -1
- package/dist/ontology-storage/unique-values.d.ts +42 -0
- package/dist/ontology-storage/unique-values.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/agents/agent-storage.ts +11 -2
- package/src/agents/approvals.ts +456 -0
- package/src/agents/checkpoints.ts +9 -4
- package/src/agents/index.ts +1 -0
- package/src/agents/memory.ts +195 -0
- package/src/agents/messages.ts +21 -18
- package/src/agents/rows.ts +46 -1
- package/src/agents/runs.ts +202 -21
- package/src/agents/settings.ts +112 -0
- package/src/agents/threads.ts +385 -1
- package/src/ai-cost-groups.ts +11 -1
- package/src/ai-cost-storage.ts +54 -12
- package/src/ai-usage-storage.ts +79 -2
- package/src/artifact-storage.ts +206 -9
- package/src/auth-storage/auth-storage.ts +58 -26
- package/src/auth-storage/users.ts +106 -60
- package/src/execution-storage.ts +39 -0
- package/src/migrations/067-artifact-kinds.sql +32 -0
- package/src/migrations/068-artifact-state.sql +15 -0
- package/src/migrations/069-agent-thread-participants.sql +16 -0
- package/src/migrations/070-forked-context-checkpoints.sql +45 -0
- package/src/migrations/071-unique-values.sql +22 -0
- package/src/migrations/072-generated-media-usage.sql +32 -0
- package/src/migrations/073-model-call-files.sql +11 -0
- package/src/migrations/074-agent-memory.sql +32 -0
- package/src/migrations/075-artifact-threads.sql +21 -0
- package/src/migrations/076-agent-approvals.sql +59 -0
- package/src/migrations/077-agent-approval-resume.sql +20 -0
- package/src/migrations/078-subagent-threads.sql +241 -0
- package/src/migrations/079-subagent-questions.sql +6 -0
- package/src/migrations.ts +75 -1
- package/src/objects/README.md +7 -0
- package/src/objects/conditional-reader.ts +27 -0
- package/src/objects/query-compiler.ts +36 -6
- package/src/objects/read-scope.ts +140 -23
- package/src/objects/reader.ts +47 -14
- package/src/objects/scoped-reader.ts +103 -0
- package/src/objects/selected-reader.ts +12 -75
- package/src/objects/storage.ts +16 -0
- package/src/ontology-storage/index.ts +3 -0
- package/src/ontology-storage/materialization-writer.ts +14 -0
- package/src/ontology-storage/materializations.ts +16 -1
- package/src/ontology-storage/unique-values.ts +187 -0
- package/src/transactions.ts +1 -1
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import type { Principal } from "@sixb/core"
|
|
3
|
+
import {
|
|
4
|
+
agentApprovalCallKey,
|
|
5
|
+
assertAgentApprovalCaller,
|
|
6
|
+
assertRequestAgentApprovalInput,
|
|
7
|
+
newAgentApprovalRecord,
|
|
8
|
+
} from "@sixb/core/internal/agent-run-storage-provider"
|
|
9
|
+
import {
|
|
10
|
+
type AgentApprovalCall,
|
|
11
|
+
type AgentApprovalCursor,
|
|
12
|
+
type AgentApprovalMode,
|
|
13
|
+
type AgentApprovalOutcome,
|
|
14
|
+
type AgentApprovalRecord,
|
|
15
|
+
type AgentApprovalStore,
|
|
16
|
+
AgentStorageError,
|
|
17
|
+
type DecideAgentApprovalInput,
|
|
18
|
+
type DecideAgentApprovalResult,
|
|
19
|
+
type ListAgentApprovalsInput,
|
|
20
|
+
type ListAgentApprovalsResult,
|
|
21
|
+
type MarkAgentApprovalsReportedInput,
|
|
22
|
+
type RequestAgentApprovalInput,
|
|
23
|
+
type RequestAgentApprovalResult,
|
|
24
|
+
type SettleAgentApprovalsInput,
|
|
25
|
+
type StopAgentApprovalsInput,
|
|
26
|
+
type StopAgentApprovalsResult,
|
|
27
|
+
} from "@sixb/core/storage"
|
|
28
|
+
import { runImmediateTransaction } from "../transactions"
|
|
29
|
+
import { type AgentRunRow, rowToRunRecord } from "./rows"
|
|
30
|
+
|
|
31
|
+
interface AgentApprovalRow {
|
|
32
|
+
readonly project_id: string
|
|
33
|
+
readonly id: string
|
|
34
|
+
readonly thread_id: string
|
|
35
|
+
readonly run_id: string
|
|
36
|
+
readonly caller_run_id: string
|
|
37
|
+
readonly requested_by_type: Principal["type"]
|
|
38
|
+
readonly requested_by_id: string
|
|
39
|
+
readonly mode: AgentApprovalRecord["mode"]
|
|
40
|
+
readonly call_key: string
|
|
41
|
+
readonly call: string
|
|
42
|
+
readonly status: AgentApprovalRecord["status"]
|
|
43
|
+
readonly created_at: string
|
|
44
|
+
readonly expires_at: string
|
|
45
|
+
readonly decided_at: string | null
|
|
46
|
+
readonly decided_by_type: Principal["type"] | null
|
|
47
|
+
readonly decided_by_id: string | null
|
|
48
|
+
readonly outcome: string | null
|
|
49
|
+
readonly reported_in_message_id: string | null
|
|
50
|
+
readonly raised_mode: AgentApprovalMode | null
|
|
51
|
+
readonly turn_stopped_at: string | null
|
|
52
|
+
readonly execution_claimed_at: string | null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const LIVE_CALL = `
|
|
56
|
+
SELECT * FROM agent_approvals
|
|
57
|
+
WHERE project_id = ? AND thread_id = ? AND call_key = ?
|
|
58
|
+
AND status IN ('pending', 'approved', 'denied')
|
|
59
|
+
`
|
|
60
|
+
|
|
61
|
+
export class SqliteAgentApprovalStore implements AgentApprovalStore {
|
|
62
|
+
constructor(private readonly db: Database) {}
|
|
63
|
+
|
|
64
|
+
async request(input: RequestAgentApprovalInput): Promise<RequestAgentApprovalResult> {
|
|
65
|
+
assertRequestAgentApprovalInput(input, "SixbSqlite")
|
|
66
|
+
|
|
67
|
+
return runImmediateTransaction(this.db, () => {
|
|
68
|
+
const threadId = assertAgentApprovalCaller({
|
|
69
|
+
request: input,
|
|
70
|
+
turn: this.run(input.projectId, input.runId),
|
|
71
|
+
caller: this.run(input.projectId, input.callerRunId),
|
|
72
|
+
prefix: "SixbSqlite",
|
|
73
|
+
})
|
|
74
|
+
const callKey = agentApprovalCallKey(input.call)
|
|
75
|
+
const live = this.db
|
|
76
|
+
.query(LIVE_CALL)
|
|
77
|
+
.get(input.projectId, threadId, callKey) as AgentApprovalRow | null
|
|
78
|
+
if (live) return { approval: rowToApproval(live), created: false }
|
|
79
|
+
|
|
80
|
+
if (this.row(input.projectId, input.id)) {
|
|
81
|
+
throw new AgentStorageError(
|
|
82
|
+
"duplicate_id",
|
|
83
|
+
`[SixbSqlite] Agent approval '${input.id}' already exists for project '${input.projectId}'.`
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
const approval = newAgentApprovalRecord(input, threadId)
|
|
87
|
+
this.db
|
|
88
|
+
.query(
|
|
89
|
+
`INSERT INTO agent_approvals (
|
|
90
|
+
project_id, id, thread_id, run_id, caller_run_id, requested_by_type, requested_by_id,
|
|
91
|
+
mode, call_key, call, status, created_at, expires_at
|
|
92
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)`
|
|
93
|
+
)
|
|
94
|
+
.run(
|
|
95
|
+
approval.projectId,
|
|
96
|
+
approval.id,
|
|
97
|
+
approval.threadId,
|
|
98
|
+
approval.runId,
|
|
99
|
+
approval.callerRunId,
|
|
100
|
+
approval.requestedBy.type,
|
|
101
|
+
approval.requestedBy.id,
|
|
102
|
+
approval.mode,
|
|
103
|
+
approval.callKey,
|
|
104
|
+
JSON.stringify(approval.call),
|
|
105
|
+
approval.createdAt.toISOString(),
|
|
106
|
+
approval.expiresAt.toISOString()
|
|
107
|
+
)
|
|
108
|
+
return { approval, created: true }
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async decide(input: DecideAgentApprovalInput): Promise<DecideAgentApprovalResult | null> {
|
|
113
|
+
const decidedAt = (input.decidedAt ?? new Date()).toISOString()
|
|
114
|
+
|
|
115
|
+
return runImmediateTransaction(this.db, () => {
|
|
116
|
+
const current = this.row(input.projectId, input.id)
|
|
117
|
+
if (!current) return null
|
|
118
|
+
if (current.status !== "pending") return { approval: rowToApproval(current), decided: false }
|
|
119
|
+
|
|
120
|
+
if (current.expires_at <= decidedAt) {
|
|
121
|
+
this.db
|
|
122
|
+
.query("UPDATE agent_approvals SET status = 'expired' WHERE project_id = ? AND id = ?")
|
|
123
|
+
.run(input.projectId, input.id)
|
|
124
|
+
} else {
|
|
125
|
+
this.db
|
|
126
|
+
.query(
|
|
127
|
+
`UPDATE agent_approvals
|
|
128
|
+
SET status = ?, decided_at = ?, decided_by_type = ?, decided_by_id = ?, raised_mode = ?
|
|
129
|
+
WHERE project_id = ? AND id = ?`
|
|
130
|
+
)
|
|
131
|
+
.run(
|
|
132
|
+
input.decision,
|
|
133
|
+
decidedAt,
|
|
134
|
+
input.decidedBy.type,
|
|
135
|
+
input.decidedBy.id,
|
|
136
|
+
input.decision === "approved" ? (input.raisedMode ?? null) : null,
|
|
137
|
+
input.projectId,
|
|
138
|
+
input.id
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
const approval = this.require(input.projectId, input.id)
|
|
142
|
+
return { approval, decided: approval.status === input.decision }
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async recordOutcome(input: {
|
|
147
|
+
readonly projectId: string
|
|
148
|
+
readonly id: string
|
|
149
|
+
readonly outcome: AgentApprovalOutcome
|
|
150
|
+
}): Promise<AgentApprovalRecord> {
|
|
151
|
+
return runImmediateTransaction(this.db, () => {
|
|
152
|
+
const current = this.row(input.projectId, input.id)
|
|
153
|
+
if (current?.status !== "approved") {
|
|
154
|
+
throw new AgentStorageError(
|
|
155
|
+
"invalid_state",
|
|
156
|
+
current
|
|
157
|
+
? `[SixbSqlite] Agent approval '${input.id}' is not approved (status '${current.status}').`
|
|
158
|
+
: `[SixbSqlite] Agent approval '${input.id}' not found for project '${input.projectId}'.`
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
if (current.outcome !== null) return rowToApproval(current)
|
|
162
|
+
|
|
163
|
+
this.db
|
|
164
|
+
.query("UPDATE agent_approvals SET outcome = ? WHERE project_id = ? AND id = ?")
|
|
165
|
+
.run(JSON.stringify(input.outcome), input.projectId, input.id)
|
|
166
|
+
return this.require(input.projectId, input.id)
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async settle(input: SettleAgentApprovalsInput): Promise<readonly AgentApprovalRecord[]> {
|
|
171
|
+
const rows = this.db
|
|
172
|
+
.query(
|
|
173
|
+
`UPDATE agent_approvals SET status = ?
|
|
174
|
+
WHERE project_id = ? AND thread_id = ? AND status = 'pending'
|
|
175
|
+
RETURNING *`
|
|
176
|
+
)
|
|
177
|
+
.all(input.status, input.projectId, input.threadId) as AgentApprovalRow[]
|
|
178
|
+
return rows.map(rowToApproval).sort(compareApprovals)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async stop(input: StopAgentApprovalsInput): Promise<StopAgentApprovalsResult> {
|
|
182
|
+
const stoppedAt = (input.stoppedAt ?? new Date()).toISOString()
|
|
183
|
+
const clauses = [
|
|
184
|
+
"project_id = ?",
|
|
185
|
+
"thread_id = ?",
|
|
186
|
+
"reported_in_message_id IS NULL",
|
|
187
|
+
"(status = 'pending' OR turn_stopped_at IS NULL)",
|
|
188
|
+
]
|
|
189
|
+
const params: string[] = [input.projectId, input.threadId]
|
|
190
|
+
if (input.runId !== undefined) {
|
|
191
|
+
clauses.push("run_id = ?")
|
|
192
|
+
params.push(input.runId)
|
|
193
|
+
}
|
|
194
|
+
if (input.requestedBy !== undefined) {
|
|
195
|
+
clauses.push("requested_by_type = ? AND requested_by_id = ?")
|
|
196
|
+
params.push(input.requestedBy.type, input.requestedBy.id)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return runImmediateTransaction(this.db, () => {
|
|
200
|
+
const matched = this.db
|
|
201
|
+
.query(`SELECT * FROM agent_approvals WHERE ${clauses.join(" AND ")}`)
|
|
202
|
+
.all(...params) as AgentApprovalRow[]
|
|
203
|
+
const update = this.db.query(
|
|
204
|
+
`UPDATE agent_approvals
|
|
205
|
+
SET status = CASE WHEN status = 'pending' THEN 'cancelled' ELSE status END,
|
|
206
|
+
turn_stopped_at = COALESCE(turn_stopped_at, ?)
|
|
207
|
+
WHERE project_id = ? AND id = ?`
|
|
208
|
+
)
|
|
209
|
+
for (const row of matched) update.run(stoppedAt, input.projectId, row.id)
|
|
210
|
+
|
|
211
|
+
const cancelled = matched
|
|
212
|
+
.filter((row) => row.status === "pending")
|
|
213
|
+
.map((row) => this.require(input.projectId, row.id))
|
|
214
|
+
const stopped = matched.filter((row) => row.turn_stopped_at === null).length
|
|
215
|
+
return { cancelled: cancelled.sort(compareApprovals), stopped }
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async holdsRun(input: { readonly projectId: string; readonly runId: string }): Promise<boolean> {
|
|
220
|
+
return sqliteHoldsRun(this.db, input.projectId, input.runId)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async claimExecution(input: {
|
|
224
|
+
readonly projectId: string
|
|
225
|
+
readonly id: string
|
|
226
|
+
readonly claimedAt: Date
|
|
227
|
+
readonly staleBefore: Date
|
|
228
|
+
}): Promise<AgentApprovalRecord | null> {
|
|
229
|
+
const row = this.db
|
|
230
|
+
.query(
|
|
231
|
+
`UPDATE agent_approvals SET execution_claimed_at = ?
|
|
232
|
+
WHERE project_id = ? AND id = ? AND status = 'approved' AND outcome IS NULL
|
|
233
|
+
AND COALESCE(execution_claimed_at, decided_at) <= ?
|
|
234
|
+
RETURNING *`
|
|
235
|
+
)
|
|
236
|
+
.get(
|
|
237
|
+
input.claimedAt.toISOString(),
|
|
238
|
+
input.projectId,
|
|
239
|
+
input.id,
|
|
240
|
+
input.staleBefore.toISOString()
|
|
241
|
+
) as AgentApprovalRow | null
|
|
242
|
+
return row ? rowToApproval(row) : null
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async listRepairable(input: {
|
|
246
|
+
readonly projectId: string
|
|
247
|
+
readonly claimableBefore: Date
|
|
248
|
+
readonly after?: AgentApprovalCursor
|
|
249
|
+
readonly limit: number
|
|
250
|
+
}): Promise<readonly AgentApprovalRecord[]> {
|
|
251
|
+
const afterAt = input.after?.createdAt.toISOString() ?? null
|
|
252
|
+
const afterId = input.after?.id ?? ""
|
|
253
|
+
// Each half of the OR matches one partial index.
|
|
254
|
+
const rows = this.db
|
|
255
|
+
.query(
|
|
256
|
+
`SELECT * FROM agent_approvals
|
|
257
|
+
WHERE project_id = ?
|
|
258
|
+
AND (
|
|
259
|
+
(status = 'approved' AND outcome IS NULL
|
|
260
|
+
AND COALESCE(execution_claimed_at, decided_at) <= ?)
|
|
261
|
+
OR (status IN ('approved', 'denied') AND reported_in_message_id IS NULL
|
|
262
|
+
AND turn_stopped_at IS NULL)
|
|
263
|
+
)
|
|
264
|
+
AND (? IS NULL OR created_at > ? OR (created_at = ? AND id > ?))
|
|
265
|
+
ORDER BY created_at, id
|
|
266
|
+
LIMIT ?`
|
|
267
|
+
)
|
|
268
|
+
.all(
|
|
269
|
+
input.projectId,
|
|
270
|
+
input.claimableBefore.toISOString(),
|
|
271
|
+
afterAt,
|
|
272
|
+
afterAt,
|
|
273
|
+
afterAt,
|
|
274
|
+
afterId,
|
|
275
|
+
input.limit
|
|
276
|
+
) as AgentApprovalRow[]
|
|
277
|
+
return rows.map(rowToApproval)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async expireDue(input: {
|
|
281
|
+
readonly projectId: string
|
|
282
|
+
readonly now: Date
|
|
283
|
+
readonly limit?: number
|
|
284
|
+
}): Promise<readonly AgentApprovalRecord[]> {
|
|
285
|
+
const rows = this.db
|
|
286
|
+
.query(
|
|
287
|
+
`UPDATE agent_approvals SET status = 'expired'
|
|
288
|
+
WHERE project_id = ? AND id IN (
|
|
289
|
+
SELECT id FROM agent_approvals
|
|
290
|
+
WHERE project_id = ? AND status = 'pending' AND expires_at <= ?
|
|
291
|
+
ORDER BY created_at, id
|
|
292
|
+
LIMIT ?
|
|
293
|
+
)
|
|
294
|
+
RETURNING *`
|
|
295
|
+
)
|
|
296
|
+
.all(
|
|
297
|
+
input.projectId,
|
|
298
|
+
input.projectId,
|
|
299
|
+
input.now.toISOString(),
|
|
300
|
+
input.limit ?? -1
|
|
301
|
+
) as AgentApprovalRow[]
|
|
302
|
+
return rows.map(rowToApproval).sort(compareApprovals)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async markReported(input: MarkAgentApprovalsReportedInput): Promise<readonly string[]> {
|
|
306
|
+
const ids = [...new Set(input.ids)]
|
|
307
|
+
if (ids.length === 0) return []
|
|
308
|
+
|
|
309
|
+
const rows = this.db
|
|
310
|
+
.query(
|
|
311
|
+
`UPDATE agent_approvals SET reported_in_message_id = ?
|
|
312
|
+
WHERE project_id = ? AND thread_id = ? AND reported_in_message_id IS NULL
|
|
313
|
+
AND (? = 0 OR turn_stopped_at IS NULL)
|
|
314
|
+
AND id IN (${ids.map(() => "?").join(", ")})
|
|
315
|
+
RETURNING id`
|
|
316
|
+
)
|
|
317
|
+
.all(
|
|
318
|
+
input.messageId,
|
|
319
|
+
input.projectId,
|
|
320
|
+
input.threadId,
|
|
321
|
+
input.resuming === true ? 1 : 0,
|
|
322
|
+
...ids
|
|
323
|
+
) as { id: string }[]
|
|
324
|
+
const marked = new Set(rows.map((row) => row.id))
|
|
325
|
+
return ids.filter((id) => marked.has(id))
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async getById(params: {
|
|
329
|
+
readonly projectId: string
|
|
330
|
+
readonly id: string
|
|
331
|
+
}): Promise<AgentApprovalRecord | null> {
|
|
332
|
+
const row = this.row(params.projectId, params.id)
|
|
333
|
+
return row ? rowToApproval(row) : null
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async list(input: ListAgentApprovalsInput): Promise<ListAgentApprovalsResult> {
|
|
337
|
+
if (input.statuses?.length === 0) return { approvals: [], hasMore: false, total: 0 }
|
|
338
|
+
|
|
339
|
+
const { where, params } = listFilter(input)
|
|
340
|
+
const offset = input.offset ?? 0
|
|
341
|
+
return this.db.transaction(() => {
|
|
342
|
+
const counted = this.db
|
|
343
|
+
.query(`SELECT COUNT(*) AS total FROM agent_approvals WHERE ${where}`)
|
|
344
|
+
.get(...params) as { total: number }
|
|
345
|
+
const rows = this.db
|
|
346
|
+
.query(
|
|
347
|
+
`SELECT * FROM agent_approvals WHERE ${where}
|
|
348
|
+
ORDER BY created_at, id LIMIT ? OFFSET ?`
|
|
349
|
+
)
|
|
350
|
+
.all(...params, input.limit ?? -1, offset) as AgentApprovalRow[]
|
|
351
|
+
return {
|
|
352
|
+
approvals: rows.map(rowToApproval),
|
|
353
|
+
hasMore: offset + rows.length < counted.total,
|
|
354
|
+
total: counted.total,
|
|
355
|
+
}
|
|
356
|
+
})()
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private run(projectId: string, id: string) {
|
|
360
|
+
const row = this.db
|
|
361
|
+
.query("SELECT * FROM agent_runs WHERE project_id = ? AND id = ?")
|
|
362
|
+
.get(projectId, id) as AgentRunRow | null
|
|
363
|
+
return row ? rowToRunRecord(row) : null
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private row(projectId: string, id: string): AgentApprovalRow | null {
|
|
367
|
+
return this.db
|
|
368
|
+
.query("SELECT * FROM agent_approvals WHERE project_id = ? AND id = ?")
|
|
369
|
+
.get(projectId, id) as AgentApprovalRow | null
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private require(projectId: string, id: string): AgentApprovalRecord {
|
|
373
|
+
const row = this.row(projectId, id)
|
|
374
|
+
if (!row) throw new Error(`[SixbSqlite] Agent approval '${id}' disappeared.`)
|
|
375
|
+
return rowToApproval(row)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** The `list` predicate, as the in-memory store's `agentApprovalMatches` reads it. */
|
|
380
|
+
function listFilter(input: ListAgentApprovalsInput): {
|
|
381
|
+
readonly where: string
|
|
382
|
+
readonly params: readonly (string | number)[]
|
|
383
|
+
} {
|
|
384
|
+
const clauses = ["project_id = ?"]
|
|
385
|
+
const params: (string | number)[] = [input.projectId]
|
|
386
|
+
if (input.threadId !== undefined) {
|
|
387
|
+
clauses.push("thread_id = ?")
|
|
388
|
+
params.push(input.threadId)
|
|
389
|
+
}
|
|
390
|
+
if (input.runId !== undefined) {
|
|
391
|
+
clauses.push("run_id = ?")
|
|
392
|
+
params.push(input.runId)
|
|
393
|
+
}
|
|
394
|
+
if (input.requestedBy !== undefined) {
|
|
395
|
+
clauses.push("requested_by_type = ? AND requested_by_id = ?")
|
|
396
|
+
params.push(input.requestedBy.type, input.requestedBy.id)
|
|
397
|
+
}
|
|
398
|
+
if (input.statuses !== undefined) {
|
|
399
|
+
clauses.push(`status IN (${input.statuses.map(() => "?").join(", ")})`)
|
|
400
|
+
params.push(...input.statuses)
|
|
401
|
+
}
|
|
402
|
+
if (input.reported !== undefined) {
|
|
403
|
+
clauses.push(`reported_in_message_id IS ${input.reported ? "NOT NULL" : "NULL"}`)
|
|
404
|
+
}
|
|
405
|
+
return { where: clauses.join(" AND "), params }
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function rowToApproval(row: AgentApprovalRow): AgentApprovalRecord {
|
|
409
|
+
return {
|
|
410
|
+
id: row.id,
|
|
411
|
+
projectId: row.project_id,
|
|
412
|
+
threadId: row.thread_id,
|
|
413
|
+
runId: row.run_id,
|
|
414
|
+
callerRunId: row.caller_run_id,
|
|
415
|
+
requestedBy: { type: row.requested_by_type, id: row.requested_by_id },
|
|
416
|
+
mode: row.mode,
|
|
417
|
+
callKey: row.call_key,
|
|
418
|
+
call: JSON.parse(row.call) as AgentApprovalCall,
|
|
419
|
+
status: row.status,
|
|
420
|
+
createdAt: new Date(row.created_at),
|
|
421
|
+
expiresAt: new Date(row.expires_at),
|
|
422
|
+
...(row.decided_at === null ? {} : { decidedAt: new Date(row.decided_at) }),
|
|
423
|
+
...(row.decided_by_type === null || row.decided_by_id === null
|
|
424
|
+
? {}
|
|
425
|
+
: { decidedBy: { type: row.decided_by_type, id: row.decided_by_id } }),
|
|
426
|
+
...(row.outcome === null ? {} : { outcome: JSON.parse(row.outcome) as AgentApprovalOutcome }),
|
|
427
|
+
...(row.reported_in_message_id === null
|
|
428
|
+
? {}
|
|
429
|
+
: { reportedInMessageId: row.reported_in_message_id }),
|
|
430
|
+
...(row.raised_mode === null ? {} : { raisedMode: row.raised_mode }),
|
|
431
|
+
...(row.turn_stopped_at === null ? {} : { turnStoppedAt: new Date(row.turn_stopped_at) }),
|
|
432
|
+
...(row.execution_claimed_at === null
|
|
433
|
+
? {}
|
|
434
|
+
: { executionClaimedAt: new Date(row.execution_claimed_at) }),
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Whether a run holds a call: an approval of its, or one it made for its turn, pending or decided
|
|
440
|
+
* and not reported yet. `holdsRun` and the paused finish of `runs.finish` both ask this.
|
|
441
|
+
*/
|
|
442
|
+
export function sqliteHoldsRun(db: Database, projectId: string, runId: string): boolean {
|
|
443
|
+
const held = db
|
|
444
|
+
.query(
|
|
445
|
+
`SELECT id FROM agent_approvals
|
|
446
|
+
WHERE project_id = ? AND (run_id = ? OR caller_run_id = ?)
|
|
447
|
+
AND status IN ('pending', 'approved', 'denied') AND reported_in_message_id IS NULL
|
|
448
|
+
LIMIT 1`
|
|
449
|
+
)
|
|
450
|
+
.get(projectId, runId, runId)
|
|
451
|
+
return held !== null
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function compareApprovals(left: AgentApprovalRecord, right: AgentApprovalRecord): number {
|
|
455
|
+
return left.createdAt.getTime() - right.createdAt.getTime() || left.id.localeCompare(right.id)
|
|
456
|
+
}
|
|
@@ -54,9 +54,13 @@ export class SqliteAgentContextCheckpointStore implements AgentContextCheckpoint
|
|
|
54
54
|
|
|
55
55
|
const existingForRun = this.db
|
|
56
56
|
.query(
|
|
57
|
-
"SELECT * FROM agent_context_checkpoints WHERE project_id = ? AND created_by_run_id = ?"
|
|
57
|
+
"SELECT * FROM agent_context_checkpoints WHERE project_id = ? AND thread_id = ? AND created_by_run_id = ?"
|
|
58
58
|
)
|
|
59
|
-
.get(
|
|
59
|
+
.get(
|
|
60
|
+
input.projectId,
|
|
61
|
+
input.threadId,
|
|
62
|
+
input.createdByRunId
|
|
63
|
+
) as AgentContextCheckpointRow | null
|
|
60
64
|
if (existingForRun) {
|
|
61
65
|
const record = rowToContextCheckpointRecord(existingForRun)
|
|
62
66
|
if (agentContextCheckpointMatchesCreateInput(record, input)) {
|
|
@@ -150,6 +154,7 @@ export class SqliteAgentContextCheckpointStore implements AgentContextCheckpoint
|
|
|
150
154
|
|
|
151
155
|
async getByRunIds(input: {
|
|
152
156
|
readonly projectId: string
|
|
157
|
+
readonly threadId: string
|
|
153
158
|
readonly runIds: readonly string[]
|
|
154
159
|
}): Promise<readonly AgentContextCheckpointRecord[]> {
|
|
155
160
|
const runIds = [...new Set(input.runIds)]
|
|
@@ -162,9 +167,9 @@ export class SqliteAgentContextCheckpointStore implements AgentContextCheckpoint
|
|
|
162
167
|
rows.push(
|
|
163
168
|
...(this.db
|
|
164
169
|
.query(
|
|
165
|
-
`SELECT * FROM agent_context_checkpoints WHERE project_id = ? AND created_by_run_id IN (${placeholders})`
|
|
170
|
+
`SELECT * FROM agent_context_checkpoints WHERE project_id = ? AND thread_id = ? AND created_by_run_id IN (${placeholders})`
|
|
166
171
|
)
|
|
167
|
-
.all(input.projectId, ...batch) as AgentContextCheckpointRow[])
|
|
172
|
+
.all(input.projectId, input.threadId, ...batch) as AgentContextCheckpointRow[])
|
|
168
173
|
)
|
|
169
174
|
}
|
|
170
175
|
const byRunId = new Map(
|
package/src/agents/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export type { SqliteAgentStorageOptions } from "./agent-storage"
|
|
2
2
|
export { SqliteAgentStorage } from "./agent-storage"
|
|
3
3
|
export { SqliteAgentContextCheckpointStore } from "./checkpoints"
|
|
4
|
+
export { SqliteAgentMemoryStore } from "./memory"
|
|
4
5
|
export { SqliteAgentMessageStore } from "./messages"
|
|
5
6
|
export { SqliteAgentRunStore } from "./runs"
|
|
6
7
|
export { SqliteAgentThreadStore } from "./threads"
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import type { Principal } from "@sixb/core"
|
|
3
|
+
import {
|
|
4
|
+
agentMemoryConflicts,
|
|
5
|
+
agentMemoryHistoryCutoff,
|
|
6
|
+
assertSaveAgentMemoryInput,
|
|
7
|
+
type LatestAgentMemoryFile,
|
|
8
|
+
} from "@sixb/core/internal/agent-run-storage-provider"
|
|
9
|
+
import type {
|
|
10
|
+
AgentMemoryRepoRecord,
|
|
11
|
+
AgentMemoryRevisionRecord,
|
|
12
|
+
AgentMemoryStore,
|
|
13
|
+
ListAgentMemoryRevisionsInput,
|
|
14
|
+
ListAgentMemoryRevisionsResult,
|
|
15
|
+
SaveAgentMemoryInput,
|
|
16
|
+
SaveAgentMemoryResult,
|
|
17
|
+
} from "@sixb/core/storage"
|
|
18
|
+
import { runImmediateTransaction } from "../transactions"
|
|
19
|
+
|
|
20
|
+
interface RevisionRow {
|
|
21
|
+
readonly revision: number
|
|
22
|
+
readonly message: string
|
|
23
|
+
readonly author_principal_type: Principal["type"]
|
|
24
|
+
readonly author_principal_id: string
|
|
25
|
+
readonly source: string | null
|
|
26
|
+
readonly created_at: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Each path's highest-revision version, the deleted ones included. */
|
|
30
|
+
const LATEST_FILES = `
|
|
31
|
+
SELECT f.path, f.revision, f.contents FROM agent_memory_files f
|
|
32
|
+
WHERE f.project_id = ? AND f.repo_id = ? AND f.revision = (
|
|
33
|
+
SELECT MAX(l.revision) FROM agent_memory_files l
|
|
34
|
+
WHERE l.project_id = f.project_id AND l.repo_id = f.repo_id AND l.path = f.path
|
|
35
|
+
)
|
|
36
|
+
`
|
|
37
|
+
|
|
38
|
+
export class SqliteAgentMemoryStore implements AgentMemoryStore {
|
|
39
|
+
constructor(private readonly db: Database) {}
|
|
40
|
+
|
|
41
|
+
async get(input: {
|
|
42
|
+
projectId: string
|
|
43
|
+
repoId: string
|
|
44
|
+
asOf?: Date
|
|
45
|
+
revision?: number
|
|
46
|
+
}): Promise<AgentMemoryRepoRecord> {
|
|
47
|
+
return this.db.transaction(() => {
|
|
48
|
+
const revision = this.head(input.projectId, input.repoId, input.asOf, input.revision)
|
|
49
|
+
const files = this.db
|
|
50
|
+
.query(
|
|
51
|
+
`SELECT f.path, f.contents FROM agent_memory_files f
|
|
52
|
+
WHERE f.project_id = ? AND f.repo_id = ? AND f.contents IS NOT NULL AND f.revision = (
|
|
53
|
+
SELECT MAX(l.revision) FROM agent_memory_files l
|
|
54
|
+
WHERE l.project_id = f.project_id AND l.repo_id = f.repo_id AND l.path = f.path
|
|
55
|
+
AND l.revision <= ?
|
|
56
|
+
)
|
|
57
|
+
ORDER BY f.path`
|
|
58
|
+
)
|
|
59
|
+
.all(input.projectId, input.repoId, revision) as { path: string; contents: string }[]
|
|
60
|
+
return { revision, files: files.map(({ path, contents }) => ({ path, contents })) }
|
|
61
|
+
})()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async save(input: SaveAgentMemoryInput): Promise<SaveAgentMemoryResult> {
|
|
65
|
+
assertSaveAgentMemoryInput(input, "SixbSqlite")
|
|
66
|
+
return runImmediateTransaction(this.db, () => {
|
|
67
|
+
const head = this.head(input.projectId, input.repoId)
|
|
68
|
+
const paths = input.changes.map((change) => change.path)
|
|
69
|
+
const latest = this.db
|
|
70
|
+
.query(`${LATEST_FILES} AND f.path IN (${paths.map(() => "?").join(", ")})`)
|
|
71
|
+
.all(input.projectId, input.repoId, ...paths) as (LatestAgentMemoryFile & {
|
|
72
|
+
path: string
|
|
73
|
+
})[]
|
|
74
|
+
const conflicts = agentMemoryConflicts(
|
|
75
|
+
input,
|
|
76
|
+
head,
|
|
77
|
+
new Map(latest.map((file) => [file.path, file]))
|
|
78
|
+
)
|
|
79
|
+
if (conflicts.length > 0) return { saved: false, revision: head, conflicts }
|
|
80
|
+
|
|
81
|
+
const revision = head + 1
|
|
82
|
+
const createdAt = input.createdAt ?? new Date()
|
|
83
|
+
this.db
|
|
84
|
+
.query(
|
|
85
|
+
`INSERT INTO agent_memory_revisions (
|
|
86
|
+
project_id, repo_id, revision, message, author_principal_type, author_principal_id,
|
|
87
|
+
source, created_at
|
|
88
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
89
|
+
)
|
|
90
|
+
.run(
|
|
91
|
+
input.projectId,
|
|
92
|
+
input.repoId,
|
|
93
|
+
revision,
|
|
94
|
+
input.message,
|
|
95
|
+
input.author.type,
|
|
96
|
+
input.author.id,
|
|
97
|
+
input.source ?? null,
|
|
98
|
+
createdAt.toISOString()
|
|
99
|
+
)
|
|
100
|
+
const insertFile = this.db.query(
|
|
101
|
+
`INSERT INTO agent_memory_files (project_id, repo_id, path, revision, contents)
|
|
102
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
103
|
+
)
|
|
104
|
+
for (const change of input.changes) {
|
|
105
|
+
insertFile.run(input.projectId, input.repoId, change.path, revision, change.contents)
|
|
106
|
+
}
|
|
107
|
+
this.db
|
|
108
|
+
.query(
|
|
109
|
+
`DELETE FROM agent_memory_files AS f
|
|
110
|
+
WHERE f.project_id = ? AND f.repo_id = ? AND f.revision IN (
|
|
111
|
+
SELECT revision FROM agent_memory_revisions
|
|
112
|
+
WHERE project_id = ? AND repo_id = ? AND created_at < ?
|
|
113
|
+
) AND EXISTS (
|
|
114
|
+
SELECT 1 FROM agent_memory_files n
|
|
115
|
+
WHERE n.project_id = f.project_id AND n.repo_id = f.repo_id AND n.path = f.path
|
|
116
|
+
AND n.revision > f.revision
|
|
117
|
+
)`
|
|
118
|
+
)
|
|
119
|
+
.run(
|
|
120
|
+
input.projectId,
|
|
121
|
+
input.repoId,
|
|
122
|
+
input.projectId,
|
|
123
|
+
input.repoId,
|
|
124
|
+
agentMemoryHistoryCutoff(createdAt).toISOString()
|
|
125
|
+
)
|
|
126
|
+
return { saved: true, revision }
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async listRevisions(
|
|
131
|
+
input: ListAgentMemoryRevisionsInput
|
|
132
|
+
): Promise<ListAgentMemoryRevisionsResult> {
|
|
133
|
+
return this.db.transaction(() => {
|
|
134
|
+
const { total } = this.db
|
|
135
|
+
.query(
|
|
136
|
+
"SELECT COUNT(*) AS total FROM agent_memory_revisions WHERE project_id = ? AND repo_id = ?"
|
|
137
|
+
)
|
|
138
|
+
.get(input.projectId, input.repoId) as { total: number }
|
|
139
|
+
const offset = input.offset ?? 0
|
|
140
|
+
const rows = this.db
|
|
141
|
+
.query(
|
|
142
|
+
`SELECT revision, message, author_principal_type, author_principal_id, source, created_at
|
|
143
|
+
FROM agent_memory_revisions WHERE project_id = ? AND repo_id = ?
|
|
144
|
+
ORDER BY revision DESC LIMIT ? OFFSET ?`
|
|
145
|
+
)
|
|
146
|
+
.all(input.projectId, input.repoId, input.limit ?? -1, offset) as RevisionRow[]
|
|
147
|
+
const changes = this.db
|
|
148
|
+
.query(
|
|
149
|
+
`SELECT revision, path, contents IS NULL AS deleted FROM agent_memory_files
|
|
150
|
+
WHERE project_id = ? AND repo_id = ? AND revision IN (${rows.map(() => "?").join(", ")})
|
|
151
|
+
ORDER BY path`
|
|
152
|
+
)
|
|
153
|
+
.all(input.projectId, input.repoId, ...rows.map((row) => row.revision)) as {
|
|
154
|
+
revision: number
|
|
155
|
+
path: string
|
|
156
|
+
deleted: number
|
|
157
|
+
}[]
|
|
158
|
+
const revisions = rows.map(
|
|
159
|
+
(row): AgentMemoryRevisionRecord => ({
|
|
160
|
+
revision: row.revision,
|
|
161
|
+
message: row.message,
|
|
162
|
+
author: { type: row.author_principal_type, id: row.author_principal_id },
|
|
163
|
+
...(row.source === null ? {} : { source: row.source }),
|
|
164
|
+
changes: changes
|
|
165
|
+
.filter((change) => change.revision === row.revision)
|
|
166
|
+
.map((change) => ({ path: change.path, deleted: change.deleted === 1 })),
|
|
167
|
+
createdAt: new Date(row.created_at),
|
|
168
|
+
})
|
|
169
|
+
)
|
|
170
|
+
return { revisions, hasMore: offset + rows.length < total, total }
|
|
171
|
+
})()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async delete(input: { projectId: string; repoId: string }): Promise<void> {
|
|
175
|
+
// File versions go with their revisions through ON DELETE CASCADE.
|
|
176
|
+
this.db
|
|
177
|
+
.query("DELETE FROM agent_memory_revisions WHERE project_id = ? AND repo_id = ?")
|
|
178
|
+
.run(input.projectId, input.repoId)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The highest revision, or the highest saved by `asOf` and up to `upTo`. */
|
|
182
|
+
private head(projectId: string, repoId: string, asOf?: Date, upTo?: number): number {
|
|
183
|
+
const at = asOf?.toISOString() ?? null
|
|
184
|
+
const row = this.db
|
|
185
|
+
.query(
|
|
186
|
+
`SELECT COALESCE(MAX(revision), 0) AS revision FROM agent_memory_revisions
|
|
187
|
+
WHERE project_id = ? AND repo_id = ? AND (? IS NULL OR created_at <= ?)
|
|
188
|
+
AND (? IS NULL OR revision <= ?)`
|
|
189
|
+
)
|
|
190
|
+
.get(projectId, repoId, at, at, upTo ?? null, upTo ?? null) as {
|
|
191
|
+
revision: number
|
|
192
|
+
}
|
|
193
|
+
return row.revision
|
|
194
|
+
}
|
|
195
|
+
}
|