@sixb/pg 0.1.20 → 0.1.22
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/README.md +1 -0
- 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 +48 -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 +19 -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 +3 -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 +10 -1
- package/dist/agents/threads.d.ts.map +1 -1
- package/dist/ai-cost-groups.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/sessions.d.ts +2 -1
- package/dist/auth-storage/sessions.d.ts.map +1 -1
- package/dist/auth-storage/users.d.ts +8 -4
- package/dist/auth-storage/users.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6001 -2923
- package/dist/index.js.map +45 -37
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/conditional-reader.d.ts +17 -0
- package/dist/objects/conditional-reader.d.ts.map +1 -0
- package/dist/objects/query-compiler.d.ts +27 -1
- package/dist/objects/query-compiler.d.ts.map +1 -1
- package/dist/objects/query-indexes.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 +12 -0
- package/dist/objects/scoped-reader.d.ts.map +1 -0
- package/dist/objects/selected-reader.d.ts +1 -0
- package/dist/objects/selected-reader.d.ts.map +1 -1
- package/dist/objects/storage.d.ts +7 -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-session.d.ts +6 -0
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-writer.d.ts +15 -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/replacement-plans.d.ts.map +1 -1
- package/dist/ontology-storage/sources.d.ts +2 -1
- package/dist/ontology-storage/sources.d.ts.map +1 -1
- package/dist/ontology-storage/unique-values.d.ts +60 -0
- package/dist/ontology-storage/unique-values.d.ts.map +1 -0
- package/dist/pg-action-run-storage.d.ts +4 -0
- package/dist/pg-action-run-storage.d.ts.map +1 -1
- package/dist/pg-activity-storage.d.ts +22 -0
- package/dist/pg-activity-storage.d.ts.map +1 -0
- package/dist/pg-ai-cost-storage.d.ts.map +1 -1
- package/dist/pg-ai-usage-storage.d.ts +8 -1
- package/dist/pg-ai-usage-storage.d.ts.map +1 -1
- package/dist/pg-artifact-storage.d.ts +26 -0
- package/dist/pg-artifact-storage.d.ts.map +1 -0
- package/dist/pg-execution-storage.d.ts +3 -1
- package/dist/pg-execution-storage.d.ts.map +1 -1
- package/dist/pg-workflow-run-storage.d.ts +4 -0
- package/dist/pg-workflow-run-storage.d.ts.map +1 -1
- package/dist/share-grant-storage.d.ts +2 -1
- package/dist/share-grant-storage.d.ts.map +1 -1
- package/dist/storage-errors.d.ts +2 -0
- package/dist/storage-errors.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agents/agent-storage.ts +11 -2
- package/src/agents/approvals.ts +400 -0
- package/src/agents/checkpoints.ts +8 -4
- package/src/agents/index.ts +1 -0
- package/src/agents/memory.ts +175 -0
- package/src/agents/messages.ts +14 -7
- package/src/agents/rows.ts +46 -1
- package/src/agents/runs.ts +154 -22
- package/src/agents/settings.ts +105 -0
- package/src/agents/threads.ts +379 -3
- package/src/ai-cost-groups.ts +11 -1
- package/src/auth-storage/auth-storage.ts +45 -13
- package/src/auth-storage/sessions.ts +20 -0
- package/src/auth-storage/users.ts +105 -43
- package/src/index.ts +10 -0
- package/src/migrations/063-artifacts.sql +67 -0
- package/src/migrations/064-drop-output-diagnostics.sql +14 -0
- package/src/migrations/065-plan-work-payload-statistics.sql +5 -0
- package/src/migrations/066-activity.sql +61 -0
- package/src/migrations/067-activity-timeline.sql +15 -0
- package/src/migrations/068-activity-interactions.sql +20 -0
- package/src/migrations/069-artifact-kinds.sql +4 -0
- package/src/migrations/070-artifact-state.sql +16 -0
- package/src/migrations/071-agent-thread-participants.sql +16 -0
- package/src/migrations/072-forked-context-checkpoints.sql +23 -0
- package/src/migrations/073-unique-values.sql +25 -0
- package/src/migrations/074-generated-media-usage.sql +21 -0
- package/src/migrations/075-model-call-files.sql +11 -0
- package/src/migrations/076-agent-memory.sql +33 -0
- package/src/migrations/077-artifact-threads.sql +21 -0
- package/src/migrations/078-agent-approvals.sql +58 -0
- package/src/migrations/079-agent-approval-resume.sql +20 -0
- package/src/migrations/080-subagent-threads.sql +145 -0
- package/src/migrations/081-subagent-questions.sql +6 -0
- package/src/migrations.ts +52 -0
- package/src/objects/conditional-reader.ts +44 -0
- package/src/objects/prepare-queries.ts +1 -1
- package/src/objects/query-compiler.ts +96 -33
- package/src/objects/query-indexes.ts +29 -10
- package/src/objects/read-scope.ts +140 -25
- package/src/objects/reader.ts +50 -6
- package/src/objects/scoped-reader.ts +101 -0
- package/src/objects/selected-reader.ts +16 -76
- package/src/objects/storage.ts +24 -5
- package/src/objects/text-count-index.ts +1 -0
- package/src/ontology-storage/index.ts +3 -0
- package/src/ontology-storage/materialization-session.ts +8 -0
- package/src/ontology-storage/materialization-writer.ts +76 -1
- package/src/ontology-storage/materializations.ts +20 -1
- package/src/ontology-storage/replacement-plans.ts +25 -7
- package/src/ontology-storage/sources.ts +37 -0
- package/src/ontology-storage/unique-values.ts +274 -0
- package/src/pg-action-run-storage.ts +23 -0
- package/src/pg-activity-storage.ts +486 -0
- package/src/pg-ai-cost-storage.ts +62 -12
- package/src/pg-ai-usage-storage.ts +90 -1
- package/src/pg-artifact-storage.ts +521 -0
- package/src/pg-execution-storage.ts +83 -0
- package/src/pg-workflow-run-storage.ts +23 -0
- package/src/share-grant-storage.ts +21 -0
- package/src/storage-errors.ts +7 -0
- package/src/transactions.ts +1 -1
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { Principal } from "@sixb/core"
|
|
2
|
+
import {
|
|
3
|
+
agentMemoryConflicts,
|
|
4
|
+
agentMemoryHistoryCutoff,
|
|
5
|
+
assertSaveAgentMemoryInput,
|
|
6
|
+
} from "@sixb/core/internal/agent-run-storage-provider"
|
|
7
|
+
import type {
|
|
8
|
+
AgentMemoryRepoRecord,
|
|
9
|
+
AgentMemoryRevisionRecord,
|
|
10
|
+
AgentMemoryStore,
|
|
11
|
+
ListAgentMemoryRevisionsInput,
|
|
12
|
+
ListAgentMemoryRevisionsResult,
|
|
13
|
+
SaveAgentMemoryInput,
|
|
14
|
+
SaveAgentMemoryResult,
|
|
15
|
+
} from "@sixb/core/storage"
|
|
16
|
+
import { lockAdvisoryKeys, type PgStoreClient, runPgTransaction } from "../transactions"
|
|
17
|
+
|
|
18
|
+
interface RevisionRow {
|
|
19
|
+
readonly revision: number
|
|
20
|
+
readonly message: string
|
|
21
|
+
readonly author_principal_type: Principal["type"]
|
|
22
|
+
readonly author_principal_id: string
|
|
23
|
+
readonly source: string | null
|
|
24
|
+
readonly created_at: Date
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class PgAgentMemoryStore implements AgentMemoryStore {
|
|
28
|
+
constructor(private readonly sql: PgStoreClient) {}
|
|
29
|
+
|
|
30
|
+
async get(input: {
|
|
31
|
+
projectId: string
|
|
32
|
+
repoId: string
|
|
33
|
+
asOf?: Date
|
|
34
|
+
revision?: number
|
|
35
|
+
}): Promise<AgentMemoryRepoRecord> {
|
|
36
|
+
const asOf = input.asOf ?? null
|
|
37
|
+
const upTo = input.revision ?? null
|
|
38
|
+
// One statement, so the head and the files come from one snapshot.
|
|
39
|
+
const rows = await this.sql<{ head: number; path: string | null; contents: string | null }[]>`
|
|
40
|
+
WITH head AS (
|
|
41
|
+
SELECT COALESCE(MAX(revision), 0)::int AS revision FROM agent_memory_revisions
|
|
42
|
+
WHERE project_id = ${input.projectId} AND repo_id = ${input.repoId}
|
|
43
|
+
AND (${asOf}::timestamptz IS NULL OR created_at <= ${asOf}::timestamptz)
|
|
44
|
+
AND (${upTo}::int IS NULL OR revision <= ${upTo}::int)
|
|
45
|
+
), latest AS (
|
|
46
|
+
SELECT DISTINCT ON (f.path) f.path, f.contents FROM agent_memory_files f, head
|
|
47
|
+
WHERE f.project_id = ${input.projectId} AND f.repo_id = ${input.repoId}
|
|
48
|
+
AND f.revision <= head.revision
|
|
49
|
+
ORDER BY f.path, f.revision DESC
|
|
50
|
+
)
|
|
51
|
+
SELECT head.revision AS head, latest.path, latest.contents
|
|
52
|
+
FROM head
|
|
53
|
+
LEFT JOIN latest ON latest.contents IS NOT NULL
|
|
54
|
+
ORDER BY latest.path
|
|
55
|
+
`
|
|
56
|
+
return {
|
|
57
|
+
revision: rows[0]?.head ?? 0,
|
|
58
|
+
files: rows.flatMap(({ path, contents }) =>
|
|
59
|
+
path === null || contents === null ? [] : [{ path, contents }]
|
|
60
|
+
),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async save(input: SaveAgentMemoryInput): Promise<SaveAgentMemoryResult> {
|
|
65
|
+
assertSaveAgentMemoryInput(input, "SixbPg")
|
|
66
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
67
|
+
// A repo's first save has no row to lock, so saves to one repo take its advisory lock.
|
|
68
|
+
await lockAdvisoryKeys(tx, [`agent-memory:${input.projectId}:${input.repoId}`])
|
|
69
|
+
const [{ head } = { head: 0 }] = await tx<{ head: number }[]>`
|
|
70
|
+
SELECT COALESCE(MAX(revision), 0)::int AS head FROM agent_memory_revisions
|
|
71
|
+
WHERE project_id = ${input.projectId} AND repo_id = ${input.repoId}
|
|
72
|
+
`
|
|
73
|
+
const paths = input.changes.map((change) => change.path)
|
|
74
|
+
const latest = await tx<{ path: string; revision: number; contents: string | null }[]>`
|
|
75
|
+
SELECT DISTINCT ON (path) path, revision, contents FROM agent_memory_files
|
|
76
|
+
WHERE project_id = ${input.projectId} AND repo_id = ${input.repoId}
|
|
77
|
+
AND path IN ${tx(paths)}
|
|
78
|
+
ORDER BY path, revision DESC
|
|
79
|
+
`
|
|
80
|
+
const conflicts = agentMemoryConflicts(
|
|
81
|
+
input,
|
|
82
|
+
head,
|
|
83
|
+
new Map(latest.map((file) => [file.path, file]))
|
|
84
|
+
)
|
|
85
|
+
if (conflicts.length > 0) return { saved: false, revision: head, conflicts }
|
|
86
|
+
|
|
87
|
+
const revision = head + 1
|
|
88
|
+
const createdAt = input.createdAt ?? new Date()
|
|
89
|
+
await tx`
|
|
90
|
+
INSERT INTO agent_memory_revisions (
|
|
91
|
+
project_id, repo_id, revision, message, author_principal_type, author_principal_id,
|
|
92
|
+
source, created_at
|
|
93
|
+
) VALUES (
|
|
94
|
+
${input.projectId}, ${input.repoId}, ${revision}, ${input.message},
|
|
95
|
+
${input.author.type}, ${input.author.id}, ${input.source ?? null}, ${createdAt}
|
|
96
|
+
)
|
|
97
|
+
`
|
|
98
|
+
await tx`
|
|
99
|
+
INSERT INTO agent_memory_files (project_id, repo_id, path, revision, contents)
|
|
100
|
+
SELECT ${input.projectId}, ${input.repoId}, change.path, ${revision}, change.contents
|
|
101
|
+
FROM jsonb_to_recordset(${JSON.stringify(input.changes)}::text::jsonb)
|
|
102
|
+
AS change(path TEXT, contents TEXT)
|
|
103
|
+
`
|
|
104
|
+
await tx`
|
|
105
|
+
DELETE FROM agent_memory_files AS f
|
|
106
|
+
WHERE f.project_id = ${input.projectId} AND f.repo_id = ${input.repoId}
|
|
107
|
+
AND f.revision IN (
|
|
108
|
+
SELECT revision FROM agent_memory_revisions
|
|
109
|
+
WHERE project_id = ${input.projectId} AND repo_id = ${input.repoId}
|
|
110
|
+
AND created_at < ${agentMemoryHistoryCutoff(createdAt)}
|
|
111
|
+
)
|
|
112
|
+
AND EXISTS (
|
|
113
|
+
SELECT 1 FROM agent_memory_files n
|
|
114
|
+
WHERE n.project_id = f.project_id AND n.repo_id = f.repo_id AND n.path = f.path
|
|
115
|
+
AND n.revision > f.revision
|
|
116
|
+
)
|
|
117
|
+
`
|
|
118
|
+
return { saved: true, revision }
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async listRevisions(
|
|
123
|
+
input: ListAgentMemoryRevisionsInput
|
|
124
|
+
): Promise<ListAgentMemoryRevisionsResult> {
|
|
125
|
+
return runPgTransaction(
|
|
126
|
+
this.sql,
|
|
127
|
+
async (tx) => {
|
|
128
|
+
const [counted] = await tx<{ total: number }[]>`
|
|
129
|
+
SELECT COUNT(*)::int AS total FROM agent_memory_revisions
|
|
130
|
+
WHERE project_id = ${input.projectId} AND repo_id = ${input.repoId}
|
|
131
|
+
`
|
|
132
|
+
const offset = input.offset ?? 0
|
|
133
|
+
const rows = await tx<RevisionRow[]>`
|
|
134
|
+
SELECT revision, message, author_principal_type, author_principal_id, source, created_at
|
|
135
|
+
FROM agent_memory_revisions
|
|
136
|
+
WHERE project_id = ${input.projectId} AND repo_id = ${input.repoId}
|
|
137
|
+
ORDER BY revision DESC
|
|
138
|
+
LIMIT ${input.limit ?? null} OFFSET ${offset}
|
|
139
|
+
`
|
|
140
|
+
const changes =
|
|
141
|
+
rows.length === 0
|
|
142
|
+
? []
|
|
143
|
+
: await tx<{ revision: number; path: string; deleted: boolean }[]>`
|
|
144
|
+
SELECT revision, path, contents IS NULL AS deleted FROM agent_memory_files
|
|
145
|
+
WHERE project_id = ${input.projectId} AND repo_id = ${input.repoId}
|
|
146
|
+
AND revision IN ${tx(rows.map((row) => row.revision))}
|
|
147
|
+
ORDER BY path
|
|
148
|
+
`
|
|
149
|
+
const total = counted?.total ?? 0
|
|
150
|
+
const revisions = rows.map(
|
|
151
|
+
(row): AgentMemoryRevisionRecord => ({
|
|
152
|
+
revision: row.revision,
|
|
153
|
+
message: row.message,
|
|
154
|
+
author: { type: row.author_principal_type, id: row.author_principal_id },
|
|
155
|
+
...(row.source === null ? {} : { source: row.source }),
|
|
156
|
+
changes: changes
|
|
157
|
+
.filter((change) => change.revision === row.revision)
|
|
158
|
+
.map((change) => ({ path: change.path, deleted: change.deleted })),
|
|
159
|
+
createdAt: new Date(row.created_at),
|
|
160
|
+
})
|
|
161
|
+
)
|
|
162
|
+
return { revisions, hasMore: offset + rows.length < total, total }
|
|
163
|
+
},
|
|
164
|
+
{ isolation: "repeatableRead" }
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async delete(input: { projectId: string; repoId: string }): Promise<void> {
|
|
169
|
+
// File versions go with their revisions through ON DELETE CASCADE.
|
|
170
|
+
await this.sql`
|
|
171
|
+
DELETE FROM agent_memory_revisions
|
|
172
|
+
WHERE project_id = ${input.projectId} AND repo_id = ${input.repoId}
|
|
173
|
+
`
|
|
174
|
+
}
|
|
175
|
+
}
|
package/src/agents/messages.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { countsAsThreadMessage } from "@sixb/core/internal/agent-run-storage-provider"
|
|
1
2
|
import { AGENT_MESSAGE_CONTENT_VERSION } from "@sixb/core/internal/agents"
|
|
2
3
|
import {
|
|
3
4
|
type AgentMessageRecord,
|
|
@@ -91,7 +92,10 @@ export class PgAgentMessageStore implements AgentMessageStore {
|
|
|
91
92
|
|
|
92
93
|
await tx`
|
|
93
94
|
UPDATE agent_threads
|
|
94
|
-
SET
|
|
95
|
+
SET
|
|
96
|
+
message_count = message_count + ${countsAsThreadMessage(input) ? 1 : 0},
|
|
97
|
+
last_message_at = ${createdAt},
|
|
98
|
+
updated_at = ${createdAt}
|
|
95
99
|
WHERE project_id = ${input.projectId} AND id = ${input.threadId}
|
|
96
100
|
`
|
|
97
101
|
|
|
@@ -124,25 +128,28 @@ export class PgAgentMessageStore implements AgentMessageStore {
|
|
|
124
128
|
)
|
|
125
129
|
}
|
|
126
130
|
|
|
127
|
-
const deleted = await tx<{ id: string }[]>`
|
|
131
|
+
const deleted = await tx<{ id: string; parts: AgentMessageRecord["parts"] | string }[]>`
|
|
128
132
|
DELETE FROM agent_messages
|
|
129
133
|
WHERE project_id = ${input.projectId}
|
|
130
134
|
AND thread_id = ${input.threadId}
|
|
131
135
|
AND run_id = ${input.runId}
|
|
132
|
-
RETURNING id
|
|
136
|
+
RETURNING id, parts
|
|
133
137
|
`
|
|
134
138
|
if (deleted.length === 0) {
|
|
135
139
|
return 0
|
|
136
140
|
}
|
|
141
|
+
// The thread counts only the messages the conversation shows: take back the ones it counted.
|
|
142
|
+
const counted = deleted.filter((row) =>
|
|
143
|
+
countsAsThreadMessage({
|
|
144
|
+
parts: typeof row.parts === "string" ? JSON.parse(row.parts) : row.parts,
|
|
145
|
+
})
|
|
146
|
+
).length
|
|
137
147
|
|
|
138
148
|
const updatedAt = new Date()
|
|
139
149
|
await tx`
|
|
140
150
|
UPDATE agent_threads
|
|
141
151
|
SET
|
|
142
|
-
message_count = (
|
|
143
|
-
SELECT COUNT(*) FROM agent_messages
|
|
144
|
-
WHERE project_id = ${input.projectId} AND thread_id = ${input.threadId}
|
|
145
|
-
),
|
|
152
|
+
message_count = GREATEST(message_count - ${counted}, 0),
|
|
146
153
|
last_message_at = (
|
|
147
154
|
SELECT created_at FROM agent_messages
|
|
148
155
|
WHERE project_id = ${input.projectId} AND thread_id = ${input.threadId}
|
package/src/agents/rows.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type AgentMessageRecord,
|
|
8
8
|
type AgentRunDiagnostic,
|
|
9
9
|
type AgentRunRecord,
|
|
10
|
+
type AgentThreadParticipantRecord,
|
|
10
11
|
type AgentThreadRecord,
|
|
11
12
|
type AgentThreadSandboxState,
|
|
12
13
|
type ConversationAgentRunSpec,
|
|
@@ -22,6 +23,10 @@ import type { SQLClient, SqlParameter } from "../pg-client"
|
|
|
22
23
|
export interface AgentThreadRow {
|
|
23
24
|
project_id: string
|
|
24
25
|
id: string
|
|
26
|
+
kind: AgentThreadRecord["kind"]
|
|
27
|
+
parent_thread_id: string | null
|
|
28
|
+
parent_run_id: string | null
|
|
29
|
+
spawn_key: string | null
|
|
25
30
|
owner_principal_type: Principal["type"]
|
|
26
31
|
owner_principal_id: string
|
|
27
32
|
title: string | null
|
|
@@ -35,6 +40,16 @@ export interface AgentThreadRow {
|
|
|
35
40
|
updated_at: Date | string
|
|
36
41
|
}
|
|
37
42
|
|
|
43
|
+
export interface AgentThreadParticipantRow {
|
|
44
|
+
project_id: string
|
|
45
|
+
thread_id: string
|
|
46
|
+
principal_type: "user"
|
|
47
|
+
principal_id: string
|
|
48
|
+
added_at: Date | string
|
|
49
|
+
added_by_type: Principal["type"]
|
|
50
|
+
added_by_id: string
|
|
51
|
+
}
|
|
52
|
+
|
|
38
53
|
export interface AgentRunRow {
|
|
39
54
|
project_id: string
|
|
40
55
|
id: string
|
|
@@ -98,6 +113,16 @@ export function rowToThreadRecord(row: AgentThreadRow): AgentThreadRecord {
|
|
|
98
113
|
return {
|
|
99
114
|
id: row.id,
|
|
100
115
|
projectId: row.project_id,
|
|
116
|
+
kind: row.kind,
|
|
117
|
+
...(row.parent_thread_id && row.parent_run_id && row.spawn_key
|
|
118
|
+
? {
|
|
119
|
+
parent: {
|
|
120
|
+
threadId: row.parent_thread_id,
|
|
121
|
+
spawnedBy: row.parent_run_id,
|
|
122
|
+
key: row.spawn_key,
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
: {}),
|
|
101
126
|
ownerPrincipal: { type: row.owner_principal_type, id: row.owner_principal_id },
|
|
102
127
|
title: row.title ?? undefined,
|
|
103
128
|
...(row.sandbox_state == null
|
|
@@ -160,12 +185,20 @@ export function rowToRunRecord(row: AgentRunRow): AgentRunRecord {
|
|
|
160
185
|
...(row.spec === null ? {} : { spec: parseJsonColumn<ConversationAgentRunSpec>(row.spec) }),
|
|
161
186
|
}
|
|
162
187
|
}
|
|
163
|
-
if (
|
|
188
|
+
if (
|
|
189
|
+
!row.thread_id ||
|
|
190
|
+
!row.trigger_message_id ||
|
|
191
|
+
!row.parent_run_id ||
|
|
192
|
+
!row.spawn_key ||
|
|
193
|
+
row.spec === null
|
|
194
|
+
) {
|
|
164
195
|
throw new Error(`[SixbPg] Subagent run '${row.id}' has invalid persisted fields.`)
|
|
165
196
|
}
|
|
166
197
|
return {
|
|
167
198
|
...base,
|
|
168
199
|
kind: "subagent",
|
|
200
|
+
threadId: row.thread_id,
|
|
201
|
+
triggerMessageId: row.trigger_message_id,
|
|
169
202
|
parentRunId: row.parent_run_id,
|
|
170
203
|
spawnKey: row.spawn_key,
|
|
171
204
|
spec: parseJsonColumn<SubagentRunSpec>(row.spec),
|
|
@@ -220,6 +253,18 @@ export function rowToContextCheckpointRecord(
|
|
|
220
253
|
}
|
|
221
254
|
}
|
|
222
255
|
|
|
256
|
+
export function rowToParticipantRecord(
|
|
257
|
+
row: AgentThreadParticipantRow
|
|
258
|
+
): AgentThreadParticipantRecord {
|
|
259
|
+
return {
|
|
260
|
+
projectId: row.project_id,
|
|
261
|
+
threadId: row.thread_id,
|
|
262
|
+
principal: { type: "user", id: row.principal_id },
|
|
263
|
+
addedAt: new Date(row.added_at),
|
|
264
|
+
addedBy: { type: row.added_by_type, id: row.added_by_id },
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
223
268
|
function principalFromColumns(
|
|
224
269
|
type: Principal["type"] | null | undefined,
|
|
225
270
|
id: string | null | undefined
|
package/src/agents/runs.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
+
agentRunMayHoldCalls,
|
|
2
3
|
assertAgentRunExecution,
|
|
4
|
+
assertAgentRunFinishPayload,
|
|
5
|
+
assertAwaitingApprovalFinish,
|
|
3
6
|
assertConversationAgentRunSpec,
|
|
7
|
+
assertConversationThreadRow,
|
|
4
8
|
assertCreateSubagentRunInput,
|
|
5
|
-
|
|
9
|
+
assertSubagentThreadTakesRun,
|
|
10
|
+
newSubagentThreadRecord,
|
|
6
11
|
subagentRunMatchesCreateInput,
|
|
7
12
|
} from "@sixb/core/internal/agent-run-storage-provider"
|
|
13
|
+
import { AGENT_MESSAGE_CONTENT_VERSION } from "@sixb/core/internal/agents"
|
|
8
14
|
import { serializeSixbFailure } from "@sixb/core/internal/errors"
|
|
9
15
|
import {
|
|
10
16
|
AGENT_RUN_FAILURE_CODES,
|
|
@@ -20,6 +26,7 @@ import {
|
|
|
20
26
|
type FinishQueuedAgentRunInput,
|
|
21
27
|
type ListAgentRunsInput,
|
|
22
28
|
type ListAgentRunsResult,
|
|
29
|
+
type ListThreadSubagentsInput,
|
|
23
30
|
type ReclaimAgentRunInput,
|
|
24
31
|
type StartAgentRunInput,
|
|
25
32
|
type SubagentRunRecord,
|
|
@@ -28,7 +35,8 @@ import type { SQLClient, SqlParameter } from "../pg-client"
|
|
|
28
35
|
import { appendRunListFilters, hasEmptyStatuses, queryRunList } from "../run-list-query"
|
|
29
36
|
import { isUniqueViolation } from "../storage-errors"
|
|
30
37
|
import { type PgStoreClient, runPgTransaction } from "../transactions"
|
|
31
|
-
import {
|
|
38
|
+
import { pgHoldsRun } from "./approvals"
|
|
39
|
+
import { type AgentRunRow, type AgentThreadRow, rowToRunRecord, rowToThreadRecord } from "./rows"
|
|
32
40
|
|
|
33
41
|
const PG_RUN_ID_BATCH_SIZE = 10_000
|
|
34
42
|
|
|
@@ -50,8 +58,8 @@ export class PgAgentRunStore implements AgentRunStore {
|
|
|
50
58
|
|
|
51
59
|
try {
|
|
52
60
|
return await runPgTransaction(this.sql, async (tx) => {
|
|
53
|
-
const [thread] = await tx<
|
|
54
|
-
SELECT active_run_id FROM agent_threads
|
|
61
|
+
const [thread] = await tx<Pick<AgentThreadRow, "id" | "kind" | "active_run_id">[]>`
|
|
62
|
+
SELECT id, kind, active_run_id FROM agent_threads
|
|
55
63
|
WHERE project_id = ${input.projectId} AND id = ${input.threadId}
|
|
56
64
|
FOR UPDATE
|
|
57
65
|
`
|
|
@@ -62,6 +70,7 @@ export class PgAgentRunStore implements AgentRunStore {
|
|
|
62
70
|
`[SixbPg] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`
|
|
63
71
|
)
|
|
64
72
|
}
|
|
73
|
+
assertConversationThreadRow(thread, "takes turns", "SixbPg")
|
|
65
74
|
if (thread.active_run_id !== null) {
|
|
66
75
|
throw new AgentStorageError(
|
|
67
76
|
"active_run_exists",
|
|
@@ -190,18 +199,33 @@ export class PgAgentRunStore implements AgentRunStore {
|
|
|
190
199
|
)
|
|
191
200
|
}
|
|
192
201
|
|
|
202
|
+
const createdAt = input.createdAt ?? new Date()
|
|
203
|
+
await claimChildThread(tx, input, parent, createdAt)
|
|
193
204
|
const [row] = await tx<AgentRunRow[]>`
|
|
194
205
|
INSERT INTO agent_runs (
|
|
195
|
-
project_id, id, execution_id, kind,
|
|
196
|
-
status, attempt, created_at
|
|
206
|
+
project_id, id, execution_id, kind, thread_id, trigger_message_id, parent_run_id,
|
|
207
|
+
spawn_key, spec, status, attempt, created_at
|
|
197
208
|
) VALUES (
|
|
198
209
|
${input.projectId}, ${input.id}, ${input.executionId}, ${"subagent"},
|
|
199
|
-
${input.parentRunId}, ${input.spawnKey},
|
|
210
|
+
${input.thread.id}, ${input.trigger.id}, ${input.parentRunId}, ${input.spawnKey},
|
|
200
211
|
${JSON.stringify(input.spec)}::text::jsonb,
|
|
201
|
-
${"queued"}, ${0}, ${
|
|
212
|
+
${"queued"}, ${0}, ${createdAt}
|
|
202
213
|
)
|
|
203
214
|
RETURNING *
|
|
204
215
|
`
|
|
216
|
+
await tx`
|
|
217
|
+
INSERT INTO agent_messages (
|
|
218
|
+
project_id, id, thread_id, run_id, role, seq, parts, metadata, content_version,
|
|
219
|
+
created_at
|
|
220
|
+
) VALUES (
|
|
221
|
+
${input.projectId}, ${input.trigger.id}, ${input.thread.id}, ${input.id}, ${"user"},
|
|
222
|
+
(SELECT COALESCE(MAX(seq), 0) + 1 FROM agent_messages
|
|
223
|
+
WHERE project_id = ${input.projectId} AND thread_id = ${input.thread.id}),
|
|
224
|
+
${JSON.stringify(input.trigger.parts)}::text::jsonb,
|
|
225
|
+
${input.trigger.metadata === undefined ? null : JSON.stringify(input.trigger.metadata)}::text::jsonb,
|
|
226
|
+
${AGENT_MESSAGE_CONTENT_VERSION}, ${createdAt}
|
|
227
|
+
)
|
|
228
|
+
`
|
|
205
229
|
const created = rowToRunRecord(row)
|
|
206
230
|
if (created.kind !== "subagent") {
|
|
207
231
|
throw new Error(`[SixbPg] Subagent insert '${input.id}' returned the wrong run kind.`)
|
|
@@ -309,19 +333,7 @@ export class PgAgentRunStore implements AgentRunStore {
|
|
|
309
333
|
return runPgTransaction(this.sql, async (tx) => {
|
|
310
334
|
const run = await this.lockRunning(tx, input.projectId, input.id)
|
|
311
335
|
|
|
312
|
-
|
|
313
|
-
assertSubagentRunResult(input.result, run.id, "SixbPg")
|
|
314
|
-
}
|
|
315
|
-
if (
|
|
316
|
-
input.status === "succeeded" &&
|
|
317
|
-
run.kind === "conversation" &&
|
|
318
|
-
input.result !== undefined
|
|
319
|
-
) {
|
|
320
|
-
throw new AgentStorageError(
|
|
321
|
-
"invalid_input",
|
|
322
|
-
`[SixbPg] Conversational Agent run '${run.id}' cannot persist a subagent result.`
|
|
323
|
-
)
|
|
324
|
-
}
|
|
336
|
+
assertAgentRunFinishPayload(run.kind, input, run.id, "SixbPg")
|
|
325
337
|
|
|
326
338
|
if (run.execution_token !== input.executionToken) {
|
|
327
339
|
throw new AgentStorageError(
|
|
@@ -329,6 +341,14 @@ export class PgAgentRunStore implements AgentRunStore {
|
|
|
329
341
|
`[SixbPg] Execution token is no longer current on agent run '${input.id}'.`
|
|
330
342
|
)
|
|
331
343
|
}
|
|
344
|
+
if (input.finishReason === "awaiting_approval") {
|
|
345
|
+
assertAwaitingApprovalFinish({
|
|
346
|
+
runId: input.id,
|
|
347
|
+
status: input.status,
|
|
348
|
+
holdsApproval: await pgHoldsRun(tx, input.projectId, input.id),
|
|
349
|
+
prefix: "SixbPg",
|
|
350
|
+
})
|
|
351
|
+
}
|
|
332
352
|
|
|
333
353
|
const [row] = await tx<AgentRunRow[]>`
|
|
334
354
|
UPDATE agent_runs
|
|
@@ -347,6 +367,9 @@ export class PgAgentRunStore implements AgentRunStore {
|
|
|
347
367
|
`
|
|
348
368
|
|
|
349
369
|
await this.releaseThread(tx, run, completedAt)
|
|
370
|
+
if (agentRunMayHoldCalls(rowToRunRecord(run))) {
|
|
371
|
+
await settleFinishedRunApprovals(tx, run.kind, input, completedAt)
|
|
372
|
+
}
|
|
350
373
|
|
|
351
374
|
return rowToRunRecord(row)
|
|
352
375
|
})
|
|
@@ -387,6 +410,30 @@ export class PgAgentRunStore implements AgentRunStore {
|
|
|
387
410
|
})
|
|
388
411
|
}
|
|
389
412
|
|
|
413
|
+
async listThreadSubagents(
|
|
414
|
+
input: ListThreadSubagentsInput
|
|
415
|
+
): Promise<readonly SubagentRunRecord[]> {
|
|
416
|
+
// Reached through the turns of the conversation, by thread, then each turn's children by key
|
|
417
|
+
// or, for a finish reason, by `idx_agent_runs_paused_on_question`.
|
|
418
|
+
const rows = await this.sql<AgentRunRow[]>`
|
|
419
|
+
SELECT child.* FROM agent_runs turn
|
|
420
|
+
JOIN agent_runs child
|
|
421
|
+
ON child.project_id = turn.project_id AND child.parent_run_id = turn.id
|
|
422
|
+
WHERE turn.project_id = ${input.projectId}
|
|
423
|
+
AND turn.thread_id = ${input.threadId}
|
|
424
|
+
AND child.kind = 'subagent'
|
|
425
|
+
${input.spawnKey === undefined ? this.sql`` : this.sql`AND child.spawn_key = ${input.spawnKey}`}
|
|
426
|
+
${input.finishReason === undefined ? this.sql`` : this.sql`AND child.finish_reason = ${input.finishReason}`}
|
|
427
|
+
ORDER BY COALESCE(turn.started_at, turn.created_at) DESC, turn.id DESC,
|
|
428
|
+
child.created_at DESC, child.id DESC
|
|
429
|
+
LIMIT ${input.limit ?? 100}
|
|
430
|
+
`
|
|
431
|
+
return rows.flatMap((row) => {
|
|
432
|
+
const run = rowToRunRecord(row)
|
|
433
|
+
return run.kind === "subagent" ? [run] : []
|
|
434
|
+
})
|
|
435
|
+
}
|
|
436
|
+
|
|
390
437
|
async list(input: ListAgentRunsInput): Promise<ListAgentRunsResult> {
|
|
391
438
|
if (hasEmptyStatuses(input) || input.kinds?.length === 0) {
|
|
392
439
|
return { runs: [], hasMore: false, total: 0 }
|
|
@@ -466,11 +513,96 @@ export class PgAgentRunStore implements AgentRunStore {
|
|
|
466
513
|
return row
|
|
467
514
|
}
|
|
468
515
|
|
|
516
|
+
/** A turn releases its conversation, a child run its own thread. */
|
|
469
517
|
private async releaseThread(sql: SQLClient, run: AgentRunRow, completedAt: Date): Promise<void> {
|
|
470
|
-
if (run.kind !== "conversation") return
|
|
471
518
|
await sql`
|
|
472
519
|
UPDATE agent_threads SET active_run_id = ${null}, updated_at = ${completedAt}
|
|
473
520
|
WHERE project_id = ${run.project_id} AND id = ${run.thread_id} AND active_run_id = ${run.id}
|
|
474
521
|
`
|
|
475
522
|
}
|
|
476
523
|
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* The child's thread: its first run creates it, owned by the conversation's owner; a later run
|
|
527
|
+
* takes it while it is idle. Either way the run is its active run. Locked after the parent run, in
|
|
528
|
+
* the run -> thread order every writer follows.
|
|
529
|
+
*/
|
|
530
|
+
async function claimChildThread(
|
|
531
|
+
tx: SQLClient,
|
|
532
|
+
input: CreateSubagentRunInput,
|
|
533
|
+
parent: AgentRunRow,
|
|
534
|
+
createdAt: Date
|
|
535
|
+
): Promise<void> {
|
|
536
|
+
if (!input.thread.create) {
|
|
537
|
+
const [row] = await tx<AgentThreadRow[]>`
|
|
538
|
+
SELECT * FROM agent_threads
|
|
539
|
+
WHERE project_id = ${input.projectId} AND id = ${input.thread.id}
|
|
540
|
+
FOR UPDATE
|
|
541
|
+
`
|
|
542
|
+
assertSubagentThreadTakesRun(
|
|
543
|
+
row ? rowToThreadRecord(row) : null,
|
|
544
|
+
input,
|
|
545
|
+
parent.thread_id ?? "",
|
|
546
|
+
"SixbPg"
|
|
547
|
+
)
|
|
548
|
+
await tx`
|
|
549
|
+
UPDATE agent_threads
|
|
550
|
+
SET active_run_id = ${input.id}, message_count = message_count + 1,
|
|
551
|
+
last_message_at = ${createdAt}, updated_at = ${createdAt}
|
|
552
|
+
WHERE project_id = ${input.projectId} AND id = ${input.thread.id}
|
|
553
|
+
`
|
|
554
|
+
return
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const [conversation] = await tx<AgentThreadRow[]>`
|
|
558
|
+
SELECT * FROM agent_threads WHERE project_id = ${input.projectId} AND id = ${parent.thread_id}
|
|
559
|
+
`
|
|
560
|
+
if (!conversation) {
|
|
561
|
+
throw new AgentStorageError(
|
|
562
|
+
"thread_not_found",
|
|
563
|
+
`[SixbPg] Agent thread '${parent.thread_id}' not found for project '${input.projectId}'.`
|
|
564
|
+
)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const thread = newSubagentThreadRecord(input, rowToThreadRecord(conversation), createdAt)
|
|
568
|
+
await tx`
|
|
569
|
+
INSERT INTO agent_threads (
|
|
570
|
+
project_id, id, kind, parent_thread_id, parent_run_id, spawn_key, owner_principal_type,
|
|
571
|
+
owner_principal_id, status, active_run_id, last_message_at, message_count, created_at,
|
|
572
|
+
updated_at
|
|
573
|
+
) VALUES (
|
|
574
|
+
${thread.projectId}, ${thread.id}, ${"subagent"}, ${conversation.id}, ${input.parentRunId},
|
|
575
|
+
${input.spawnKey}, ${thread.ownerPrincipal.type}, ${thread.ownerPrincipal.id},
|
|
576
|
+
${thread.status}, ${input.id}, ${createdAt}, ${thread.messageCount}, ${createdAt},
|
|
577
|
+
${createdAt}
|
|
578
|
+
)
|
|
579
|
+
`
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* A turn that pauses keeps its calls for their decision. A turn that ends any other way cancels
|
|
584
|
+
* the ones still waiting, a late one included, and never resumes on the rest. A child that fails
|
|
585
|
+
* or is cancelled takes its own pending calls with it.
|
|
586
|
+
*/
|
|
587
|
+
async function settleFinishedRunApprovals(
|
|
588
|
+
tx: SQLClient,
|
|
589
|
+
kind: AgentRunRecord["kind"],
|
|
590
|
+
input: FinishAgentRunInput,
|
|
591
|
+
completedAt: Date
|
|
592
|
+
): Promise<void> {
|
|
593
|
+
if (input.finishReason === "awaiting_approval") return
|
|
594
|
+
const turn = kind === "conversation"
|
|
595
|
+
if (!turn && input.status === "succeeded") return
|
|
596
|
+
|
|
597
|
+
await tx`
|
|
598
|
+
UPDATE agent_approvals SET status = 'cancelled'
|
|
599
|
+
WHERE project_id = ${input.projectId} AND status = 'pending'
|
|
600
|
+
AND (run_id = ${input.id} OR caller_run_id = ${input.id})
|
|
601
|
+
`
|
|
602
|
+
if (!turn) return
|
|
603
|
+
await tx`
|
|
604
|
+
UPDATE agent_approvals SET turn_stopped_at = ${completedAt}
|
|
605
|
+
WHERE project_id = ${input.projectId} AND run_id = ${input.id}
|
|
606
|
+
AND reported_in_message_id IS NULL AND turn_stopped_at IS NULL
|
|
607
|
+
`
|
|
608
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { assertAgentApprovalModeInput } from "@sixb/core/internal/agent-run-storage-provider"
|
|
2
|
+
import {
|
|
3
|
+
type AgentApprovalMode,
|
|
4
|
+
type AgentApprovalModeStore,
|
|
5
|
+
AgentStorageError,
|
|
6
|
+
type Principal,
|
|
7
|
+
} from "@sixb/core/storage"
|
|
8
|
+
import { isForeignKeyViolation } from "../storage-errors"
|
|
9
|
+
import { type PgStoreClient, runPgTransaction } from "../transactions"
|
|
10
|
+
|
|
11
|
+
export class PgAgentApprovalModeStore implements AgentApprovalModeStore {
|
|
12
|
+
constructor(private readonly sql: PgStoreClient) {}
|
|
13
|
+
|
|
14
|
+
async getDefault(input: {
|
|
15
|
+
readonly projectId: string
|
|
16
|
+
readonly principal: Principal
|
|
17
|
+
}): Promise<AgentApprovalMode | null> {
|
|
18
|
+
const [row] = await this.sql<{ approval_mode: AgentApprovalMode }[]>`
|
|
19
|
+
SELECT approval_mode FROM agent_user_settings
|
|
20
|
+
WHERE project_id = ${input.projectId} AND principal_type = ${input.principal.type}
|
|
21
|
+
AND principal_id = ${input.principal.id}
|
|
22
|
+
`
|
|
23
|
+
return row?.approval_mode ?? null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async setDefault(input: {
|
|
27
|
+
readonly projectId: string
|
|
28
|
+
readonly principal: Principal
|
|
29
|
+
readonly mode: AgentApprovalMode
|
|
30
|
+
readonly updatedAt?: Date
|
|
31
|
+
}): Promise<void> {
|
|
32
|
+
assertAgentApprovalModeInput(input.mode, "SixbPg")
|
|
33
|
+
await this.sql`
|
|
34
|
+
INSERT INTO agent_user_settings (
|
|
35
|
+
project_id, principal_type, principal_id, approval_mode, updated_at
|
|
36
|
+
) VALUES (
|
|
37
|
+
${input.projectId}, ${input.principal.type}, ${input.principal.id}, ${input.mode},
|
|
38
|
+
${input.updatedAt ?? new Date()}
|
|
39
|
+
)
|
|
40
|
+
ON CONFLICT (project_id, principal_type, principal_id)
|
|
41
|
+
DO UPDATE SET approval_mode = EXCLUDED.approval_mode, updated_at = EXCLUDED.updated_at
|
|
42
|
+
`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async getForThread(input: {
|
|
46
|
+
readonly projectId: string
|
|
47
|
+
readonly threadId: string
|
|
48
|
+
readonly principal: Principal
|
|
49
|
+
}): Promise<AgentApprovalMode | null> {
|
|
50
|
+
const [row] = await this.sql<{ approval_mode: AgentApprovalMode }[]>`
|
|
51
|
+
SELECT approval_mode FROM agent_thread_user_settings
|
|
52
|
+
WHERE project_id = ${input.projectId} AND thread_id = ${input.threadId}
|
|
53
|
+
AND principal_type = ${input.principal.type} AND principal_id = ${input.principal.id}
|
|
54
|
+
`
|
|
55
|
+
return row?.approval_mode ?? null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async setForThread(input: {
|
|
59
|
+
readonly projectId: string
|
|
60
|
+
readonly threadId: string
|
|
61
|
+
readonly principal: Principal
|
|
62
|
+
readonly mode: AgentApprovalMode | null
|
|
63
|
+
readonly updatedAt?: Date
|
|
64
|
+
}): Promise<void> {
|
|
65
|
+
if (input.mode !== null) assertAgentApprovalModeInput(input.mode, "SixbPg")
|
|
66
|
+
try {
|
|
67
|
+
await runPgTransaction(this.sql, async (tx) => {
|
|
68
|
+
const [thread] = await tx<{ id: string }[]>`
|
|
69
|
+
SELECT id FROM agent_threads
|
|
70
|
+
WHERE project_id = ${input.projectId} AND id = ${input.threadId}
|
|
71
|
+
`
|
|
72
|
+
if (!thread) throw threadNotFound(input)
|
|
73
|
+
if (input.mode === null) {
|
|
74
|
+
await tx`
|
|
75
|
+
DELETE FROM agent_thread_user_settings
|
|
76
|
+
WHERE project_id = ${input.projectId} AND thread_id = ${input.threadId}
|
|
77
|
+
AND principal_type = ${input.principal.type} AND principal_id = ${input.principal.id}
|
|
78
|
+
`
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
await tx`
|
|
82
|
+
INSERT INTO agent_thread_user_settings (
|
|
83
|
+
project_id, thread_id, principal_type, principal_id, approval_mode, updated_at
|
|
84
|
+
) VALUES (
|
|
85
|
+
${input.projectId}, ${input.threadId}, ${input.principal.type}, ${input.principal.id},
|
|
86
|
+
${input.mode}, ${input.updatedAt ?? new Date()}
|
|
87
|
+
)
|
|
88
|
+
ON CONFLICT (project_id, thread_id, principal_type, principal_id)
|
|
89
|
+
DO UPDATE SET approval_mode = EXCLUDED.approval_mode, updated_at = EXCLUDED.updated_at
|
|
90
|
+
`
|
|
91
|
+
})
|
|
92
|
+
} catch (error) {
|
|
93
|
+
// A thread deleted between the read and the insert.
|
|
94
|
+
if (!isForeignKeyViolation(error)) throw error
|
|
95
|
+
throw threadNotFound(input)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function threadNotFound(input: { readonly projectId: string; readonly threadId: string }) {
|
|
101
|
+
return new AgentStorageError(
|
|
102
|
+
"thread_not_found",
|
|
103
|
+
`[SixbPg] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`
|
|
104
|
+
)
|
|
105
|
+
}
|