@sixb/pg 0.1.21 → 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.
Files changed (110) hide show
  1. package/README.md +1 -0
  2. package/dist/agents/agent-storage.d.ts +8 -2
  3. package/dist/agents/agent-storage.d.ts.map +1 -1
  4. package/dist/agents/approvals.d.ts +48 -0
  5. package/dist/agents/approvals.d.ts.map +1 -0
  6. package/dist/agents/checkpoints.d.ts +1 -0
  7. package/dist/agents/checkpoints.d.ts.map +1 -1
  8. package/dist/agents/index.d.ts +1 -0
  9. package/dist/agents/index.d.ts.map +1 -1
  10. package/dist/agents/memory.d.ts +19 -0
  11. package/dist/agents/memory.d.ts.map +1 -0
  12. package/dist/agents/messages.d.ts.map +1 -1
  13. package/dist/agents/rows.d.ts +15 -1
  14. package/dist/agents/rows.d.ts.map +1 -1
  15. package/dist/agents/runs.d.ts +3 -1
  16. package/dist/agents/runs.d.ts.map +1 -1
  17. package/dist/agents/settings.d.ts +29 -0
  18. package/dist/agents/settings.d.ts.map +1 -0
  19. package/dist/agents/threads.d.ts +6 -1
  20. package/dist/agents/threads.d.ts.map +1 -1
  21. package/dist/ai-cost-groups.d.ts.map +1 -1
  22. package/dist/auth-storage/auth-storage.d.ts +3 -2
  23. package/dist/auth-storage/auth-storage.d.ts.map +1 -1
  24. package/dist/auth-storage/users.d.ts +8 -4
  25. package/dist/auth-storage/users.d.ts.map +1 -1
  26. package/dist/index.js +3720 -1594
  27. package/dist/index.js.map +37 -31
  28. package/dist/migrations.d.ts.map +1 -1
  29. package/dist/objects/conditional-reader.d.ts +17 -0
  30. package/dist/objects/conditional-reader.d.ts.map +1 -0
  31. package/dist/objects/query-compiler.d.ts +27 -1
  32. package/dist/objects/query-compiler.d.ts.map +1 -1
  33. package/dist/objects/query-indexes.d.ts.map +1 -1
  34. package/dist/objects/read-scope.d.ts +10 -1
  35. package/dist/objects/read-scope.d.ts.map +1 -1
  36. package/dist/objects/reader.d.ts +2 -0
  37. package/dist/objects/reader.d.ts.map +1 -1
  38. package/dist/objects/scoped-reader.d.ts +12 -0
  39. package/dist/objects/scoped-reader.d.ts.map +1 -0
  40. package/dist/objects/selected-reader.d.ts +1 -0
  41. package/dist/objects/selected-reader.d.ts.map +1 -1
  42. package/dist/objects/storage.d.ts +7 -1
  43. package/dist/objects/storage.d.ts.map +1 -1
  44. package/dist/ontology-storage/index.d.ts +2 -0
  45. package/dist/ontology-storage/index.d.ts.map +1 -1
  46. package/dist/ontology-storage/materialization-session.d.ts +6 -0
  47. package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
  48. package/dist/ontology-storage/materialization-writer.d.ts +15 -0
  49. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  50. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  51. package/dist/ontology-storage/unique-values.d.ts +60 -0
  52. package/dist/ontology-storage/unique-values.d.ts.map +1 -0
  53. package/dist/pg-ai-cost-storage.d.ts.map +1 -1
  54. package/dist/pg-ai-usage-storage.d.ts +8 -1
  55. package/dist/pg-ai-usage-storage.d.ts.map +1 -1
  56. package/dist/pg-artifact-storage.d.ts +8 -1
  57. package/dist/pg-artifact-storage.d.ts.map +1 -1
  58. package/dist/pg-execution-storage.d.ts +2 -1
  59. package/dist/pg-execution-storage.d.ts.map +1 -1
  60. package/dist/storage-errors.d.ts +2 -0
  61. package/dist/storage-errors.d.ts.map +1 -1
  62. package/package.json +3 -3
  63. package/src/agents/agent-storage.ts +11 -2
  64. package/src/agents/approvals.ts +400 -0
  65. package/src/agents/checkpoints.ts +8 -4
  66. package/src/agents/index.ts +1 -0
  67. package/src/agents/memory.ts +175 -0
  68. package/src/agents/messages.ts +14 -7
  69. package/src/agents/rows.ts +46 -1
  70. package/src/agents/runs.ts +154 -22
  71. package/src/agents/settings.ts +105 -0
  72. package/src/agents/threads.ts +356 -3
  73. package/src/ai-cost-groups.ts +11 -1
  74. package/src/auth-storage/auth-storage.ts +45 -13
  75. package/src/auth-storage/users.ts +105 -43
  76. package/src/migrations/069-artifact-kinds.sql +4 -0
  77. package/src/migrations/070-artifact-state.sql +16 -0
  78. package/src/migrations/071-agent-thread-participants.sql +16 -0
  79. package/src/migrations/072-forked-context-checkpoints.sql +23 -0
  80. package/src/migrations/073-unique-values.sql +25 -0
  81. package/src/migrations/074-generated-media-usage.sql +21 -0
  82. package/src/migrations/075-model-call-files.sql +11 -0
  83. package/src/migrations/076-agent-memory.sql +33 -0
  84. package/src/migrations/077-artifact-threads.sql +21 -0
  85. package/src/migrations/078-agent-approvals.sql +58 -0
  86. package/src/migrations/079-agent-approval-resume.sql +20 -0
  87. package/src/migrations/080-subagent-threads.sql +145 -0
  88. package/src/migrations/081-subagent-questions.sql +6 -0
  89. package/src/migrations.ts +34 -0
  90. package/src/objects/conditional-reader.ts +44 -0
  91. package/src/objects/prepare-queries.ts +1 -1
  92. package/src/objects/query-compiler.ts +96 -33
  93. package/src/objects/query-indexes.ts +29 -10
  94. package/src/objects/read-scope.ts +140 -25
  95. package/src/objects/reader.ts +50 -6
  96. package/src/objects/scoped-reader.ts +101 -0
  97. package/src/objects/selected-reader.ts +16 -76
  98. package/src/objects/storage.ts +24 -5
  99. package/src/objects/text-count-index.ts +1 -0
  100. package/src/ontology-storage/index.ts +3 -0
  101. package/src/ontology-storage/materialization-session.ts +8 -0
  102. package/src/ontology-storage/materialization-writer.ts +76 -1
  103. package/src/ontology-storage/materializations.ts +20 -1
  104. package/src/ontology-storage/unique-values.ts +274 -0
  105. package/src/pg-ai-cost-storage.ts +62 -12
  106. package/src/pg-ai-usage-storage.ts +90 -1
  107. package/src/pg-artifact-storage.ts +205 -9
  108. package/src/pg-execution-storage.ts +31 -0
  109. package/src/storage-errors.ts +7 -0
  110. package/src/transactions.ts +1 -1
@@ -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 message_count = message_count + 1, last_message_at = ${createdAt}, updated_at = ${createdAt}
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}
@@ -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 (!row.parent_run_id || !row.spawn_key || row.spec === null) {
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
@@ -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
- assertSubagentRunResult,
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 { type AgentRunRow, rowToRunRecord } from "./rows"
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<{ active_run_id: string | null }[]>`
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, parent_run_id, spawn_key, spec,
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}, ${input.createdAt ?? new Date()}
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
- if (input.status === "succeeded" && run.kind === "subagent") {
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
+ }