@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.
- 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 +6 -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/users.d.ts +8 -4
- package/dist/auth-storage/users.d.ts.map +1 -1
- package/dist/index.js +3720 -1594
- package/dist/index.js.map +37 -31
- 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/unique-values.d.ts +60 -0
- package/dist/ontology-storage/unique-values.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 +8 -1
- package/dist/pg-artifact-storage.d.ts.map +1 -1
- package/dist/pg-execution-storage.d.ts +2 -1
- package/dist/pg-execution-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 +356 -3
- package/src/ai-cost-groups.ts +11 -1
- package/src/auth-storage/auth-storage.ts +45 -13
- package/src/auth-storage/users.ts +105 -43
- 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 +34 -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/unique-values.ts +274 -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 +205 -9
- package/src/pg-execution-storage.ts +31 -0
- package/src/storage-errors.ts +7 -0
- package/src/transactions.ts +1 -1
|
@@ -4,11 +4,12 @@ import type {
|
|
|
4
4
|
ListAuthUsersInput,
|
|
5
5
|
ListAuthUsersResult,
|
|
6
6
|
UpdateAuthUserProfileInput,
|
|
7
|
+
UpdateAuthUserResult,
|
|
7
8
|
UpdateAuthUserStatusInput,
|
|
8
9
|
UserRecord,
|
|
9
10
|
} from "@sixb/core/storage"
|
|
10
11
|
import { AuthStorageError } from "@sixb/core/storage"
|
|
11
|
-
import type
|
|
12
|
+
import { type PgStoreClient, runPgTransaction } from "../transactions"
|
|
12
13
|
import type { PgAuthUserRow } from "./rows"
|
|
13
14
|
import { rowToUserRecord } from "./rows"
|
|
14
15
|
import {
|
|
@@ -90,6 +91,23 @@ export class PgAuthUserStore implements AuthUserStore {
|
|
|
90
91
|
return row ? rowToUserRecord(row) : null
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
async getByIds(params: {
|
|
95
|
+
readonly projectId: string
|
|
96
|
+
readonly ids: readonly string[]
|
|
97
|
+
}): Promise<readonly UserRecord[]> {
|
|
98
|
+
const ids = [...new Set(params.ids)]
|
|
99
|
+
if (ids.length === 0) return []
|
|
100
|
+
|
|
101
|
+
const rows = await this.sql<PgAuthUserRow[]>`
|
|
102
|
+
SELECT *
|
|
103
|
+
FROM auth_users
|
|
104
|
+
WHERE project_id = ${params.projectId}
|
|
105
|
+
AND id IN ${this.sql(ids)}
|
|
106
|
+
`
|
|
107
|
+
|
|
108
|
+
return rows.map(rowToUserRecord)
|
|
109
|
+
}
|
|
110
|
+
|
|
93
111
|
async getByEmail(params: {
|
|
94
112
|
readonly projectId: string
|
|
95
113
|
readonly email: string
|
|
@@ -98,57 +116,83 @@ export class PgAuthUserStore implements AuthUserStore {
|
|
|
98
116
|
return row ? rowToUserRecord(row) : null
|
|
99
117
|
}
|
|
100
118
|
|
|
101
|
-
async updateProfile(input: UpdateAuthUserProfileInput): Promise<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
119
|
+
async updateProfile(input: UpdateAuthUserProfileInput): Promise<UpdateAuthUserResult> {
|
|
120
|
+
// The row stays locked until the update, so the user it returns as previous is the one it
|
|
121
|
+
// changed, and an omitted field keeps whatever a concurrent update wrote to it.
|
|
122
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
123
|
+
const existing = await getUserRowById(
|
|
124
|
+
tx,
|
|
125
|
+
{
|
|
126
|
+
projectId: input.projectId,
|
|
127
|
+
id: input.id,
|
|
128
|
+
},
|
|
129
|
+
{ forUpdate: true }
|
|
111
130
|
)
|
|
112
|
-
}
|
|
113
131
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
WHERE project_id = ${input.projectId}
|
|
121
|
-
AND id = ${input.id}
|
|
122
|
-
RETURNING *
|
|
123
|
-
`
|
|
132
|
+
if (!existing) {
|
|
133
|
+
throw new AuthStorageError(
|
|
134
|
+
"missing_user",
|
|
135
|
+
`[Sixb] User '${input.id}' not found for project '${input.projectId}'.`
|
|
136
|
+
)
|
|
137
|
+
}
|
|
124
138
|
|
|
125
|
-
|
|
126
|
-
|
|
139
|
+
const previousUser = rowToUserRecord(existing)
|
|
140
|
+
if (!changesProfile(previousUser, input)) {
|
|
141
|
+
return { user: previousUser, previousUser }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const updatedAt = dateOrNow(input.updatedAt)
|
|
145
|
+
const [row] = await tx<PgAuthUserRow[]>`
|
|
146
|
+
UPDATE auth_users
|
|
147
|
+
SET display_name = CASE
|
|
148
|
+
WHEN ${input.displayName !== undefined}::boolean THEN ${input.displayName ?? null}::text
|
|
149
|
+
ELSE display_name
|
|
150
|
+
END,
|
|
151
|
+
avatar_url = CASE
|
|
152
|
+
WHEN ${input.avatarUrl !== undefined}::boolean THEN ${input.avatarUrl ?? null}::text
|
|
153
|
+
ELSE avatar_url
|
|
154
|
+
END,
|
|
155
|
+
updated_at = ${updatedAt}
|
|
156
|
+
WHERE project_id = ${input.projectId}
|
|
157
|
+
AND id = ${input.id}
|
|
158
|
+
RETURNING *
|
|
159
|
+
`
|
|
127
160
|
|
|
128
|
-
|
|
129
|
-
const existing = await getUserRowById(this.sql, {
|
|
130
|
-
projectId: input.projectId,
|
|
131
|
-
id: input.id,
|
|
161
|
+
return { user: rowToUserRecord(row), previousUser }
|
|
132
162
|
})
|
|
163
|
+
}
|
|
133
164
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
165
|
+
async updateStatus(input: UpdateAuthUserStatusInput): Promise<UpdateAuthUserResult> {
|
|
166
|
+
// Locked like a profile update, so two status changes cannot both report the same previous one.
|
|
167
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
168
|
+
const existing = await getUserRowById(
|
|
169
|
+
tx,
|
|
170
|
+
{
|
|
171
|
+
projectId: input.projectId,
|
|
172
|
+
id: input.id,
|
|
173
|
+
},
|
|
174
|
+
{ forUpdate: true }
|
|
138
175
|
)
|
|
139
|
-
}
|
|
140
176
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
177
|
+
if (!existing) {
|
|
178
|
+
throw new AuthStorageError(
|
|
179
|
+
"missing_user",
|
|
180
|
+
`[Sixb] User '${input.id}' not found for project '${input.projectId}'.`
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const updatedAt = dateOrNow(input.updatedAt)
|
|
185
|
+
const [row] = await tx<PgAuthUserRow[]>`
|
|
186
|
+
UPDATE auth_users
|
|
187
|
+
SET status = ${input.status},
|
|
188
|
+
updated_at = ${updatedAt}
|
|
189
|
+
WHERE project_id = ${input.projectId}
|
|
190
|
+
AND id = ${input.id}
|
|
191
|
+
RETURNING *
|
|
192
|
+
`
|
|
150
193
|
|
|
151
|
-
|
|
194
|
+
return { user: rowToUserRecord(row), previousUser: rowToUserRecord(existing) }
|
|
195
|
+
})
|
|
152
196
|
}
|
|
153
197
|
|
|
154
198
|
async list(input: ListAuthUsersInput): Promise<ListAuthUsersResult> {
|
|
@@ -165,6 +209,15 @@ export class PgAuthUserStore implements AuthUserStore {
|
|
|
165
209
|
params.push(...input.statuses)
|
|
166
210
|
}
|
|
167
211
|
|
|
212
|
+
// `strpos` matches literally, so `%` and `_` need no escaping. Both sides fold through `lower()`.
|
|
213
|
+
if (input.search !== undefined) {
|
|
214
|
+
const index = nextIndex++
|
|
215
|
+
whereClauses.push(
|
|
216
|
+
`(strpos(lower(coalesce(display_name, '')), lower($${index})) > 0 OR strpos(lower(email), lower($${index})) > 0)`
|
|
217
|
+
)
|
|
218
|
+
params.push(input.search)
|
|
219
|
+
}
|
|
220
|
+
|
|
168
221
|
const where = `WHERE ${whereClauses.join(" AND ")}`
|
|
169
222
|
const order = input.order === "asc" ? "ASC" : "DESC"
|
|
170
223
|
const [totalRow] = await this.sql.unsafe<{ readonly count: string | number }[]>(
|
|
@@ -194,3 +247,12 @@ export class PgAuthUserStore implements AuthUserStore {
|
|
|
194
247
|
}
|
|
195
248
|
}
|
|
196
249
|
}
|
|
250
|
+
|
|
251
|
+
// Omitted keeps the stored value, and `null` clears it.
|
|
252
|
+
function changesProfile(user: UserRecord, input: UpdateAuthUserProfileInput): boolean {
|
|
253
|
+
const displayName =
|
|
254
|
+
input.displayName === undefined ? user.displayName : (input.displayName ?? undefined)
|
|
255
|
+
const avatarUrl = input.avatarUrl === undefined ? user.avatarUrl : (input.avatarUrl ?? undefined)
|
|
256
|
+
|
|
257
|
+
return displayName !== user.displayName || avatarUrl !== user.avatarUrl
|
|
258
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
-- Artifacts take two more kinds: pages, and the canvases that lay pages out.
|
|
2
|
+
ALTER TABLE artifacts DROP CONSTRAINT artifacts_kind_check;
|
|
3
|
+
ALTER TABLE artifacts ADD CONSTRAINT artifacts_kind_check
|
|
4
|
+
CHECK (kind IN ('document', 'sheet', 'slides', 'page', 'canvas', 'file'));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- What a page keeps for each of its readers with `sixb.state`: JSON text by key, for one artifact
|
|
2
|
+
-- and one principal, across the artifact's versions. The value stays text, as written, so its
|
|
3
|
+
-- size is the one the reader's limits count; `size_bytes` is its UTF-8 length. Deleting the
|
|
4
|
+
-- artifact cascades here.
|
|
5
|
+
CREATE TABLE artifact_state (
|
|
6
|
+
project_id TEXT NOT NULL,
|
|
7
|
+
artifact_id TEXT NOT NULL,
|
|
8
|
+
principal_type TEXT NOT NULL CHECK (principal_type IN ('user', 'serviceAccount', 'system')),
|
|
9
|
+
principal_id TEXT NOT NULL CHECK (length(principal_id) > 0),
|
|
10
|
+
key TEXT NOT NULL CHECK (length(key) > 0),
|
|
11
|
+
value TEXT NOT NULL,
|
|
12
|
+
size_bytes INTEGER NOT NULL CHECK (size_bytes >= 0),
|
|
13
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
14
|
+
PRIMARY KEY (project_id, artifact_id, principal_type, principal_id, key),
|
|
15
|
+
FOREIGN KEY (project_id, artifact_id) REFERENCES artifacts (project_id, id) ON DELETE CASCADE
|
|
16
|
+
);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- The users a thread's owner added to the conversation. Only users take part: only a signed-in
|
|
2
|
+
-- user talks to the Agent. Listing the threads shared with someone reads the principal index.
|
|
3
|
+
CREATE TABLE agent_thread_participants (
|
|
4
|
+
project_id TEXT NOT NULL,
|
|
5
|
+
thread_id TEXT NOT NULL,
|
|
6
|
+
principal_type TEXT NOT NULL CHECK (principal_type = 'user'),
|
|
7
|
+
principal_id TEXT NOT NULL CHECK (length(principal_id) > 0),
|
|
8
|
+
added_at TIMESTAMPTZ NOT NULL,
|
|
9
|
+
added_by_type TEXT NOT NULL CHECK (added_by_type IN ('user', 'serviceAccount', 'system')),
|
|
10
|
+
added_by_id TEXT NOT NULL CHECK (length(added_by_id) > 0),
|
|
11
|
+
PRIMARY KEY (project_id, thread_id, principal_type, principal_id),
|
|
12
|
+
FOREIGN KEY (project_id, thread_id) REFERENCES agent_threads (project_id, id) ON DELETE CASCADE
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
CREATE INDEX idx_agent_thread_participants_principal
|
|
16
|
+
ON agent_thread_participants (project_id, principal_type, principal_id, thread_id);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- A forked thread copies the checkpoint its history needs, and the copy keeps the run that wrote
|
|
2
|
+
-- the summary. A run's checkpoint is therefore unique within a thread, not within the project.
|
|
3
|
+
-- The project-wide constraint was unnamed: drop the one unique on exactly those two columns, and
|
|
4
|
+
-- stop if it is not there rather than leave the old rule in place.
|
|
5
|
+
DO $$
|
|
6
|
+
DECLARE run_constraint TEXT;
|
|
7
|
+
BEGIN
|
|
8
|
+
SELECT conname INTO run_constraint
|
|
9
|
+
FROM pg_constraint
|
|
10
|
+
WHERE conrelid = 'agent_context_checkpoints'::regclass
|
|
11
|
+
AND contype = 'u'
|
|
12
|
+
AND pg_get_constraintdef(oid) = 'UNIQUE (project_id, created_by_run_id)';
|
|
13
|
+
|
|
14
|
+
IF run_constraint IS NULL THEN
|
|
15
|
+
RAISE EXCEPTION 'agent_context_checkpoints has no UNIQUE (project_id, created_by_run_id) to drop';
|
|
16
|
+
END IF;
|
|
17
|
+
|
|
18
|
+
EXECUTE format('ALTER TABLE agent_context_checkpoints DROP CONSTRAINT %I', run_constraint);
|
|
19
|
+
END $$;
|
|
20
|
+
|
|
21
|
+
ALTER TABLE agent_context_checkpoints
|
|
22
|
+
ADD CONSTRAINT uq_agent_context_checkpoints_thread_run
|
|
23
|
+
UNIQUE (project_id, thread_id, created_by_run_id);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- The values of unique properties, one row per value an object holds. Every object write keeps
|
|
2
|
+
-- them current, so a write that would give a value to a second object of the type is refused.
|
|
3
|
+
-- Values are keyed by their SHA-256: a long string stays under the btree key limit, and a lookup
|
|
4
|
+
-- still compares `value_key` itself.
|
|
5
|
+
CREATE TABLE object_unique_values (
|
|
6
|
+
project_id TEXT NOT NULL,
|
|
7
|
+
object_type_id TEXT NOT NULL,
|
|
8
|
+
property_id TEXT NOT NULL,
|
|
9
|
+
value_hash BYTEA NOT NULL,
|
|
10
|
+
value_key TEXT NOT NULL,
|
|
11
|
+
primary_id TEXT NOT NULL,
|
|
12
|
+
PRIMARY KEY (project_id, object_type_id, property_id, value_hash)
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
-- The unique properties whose values were built from the objects already stored, and how their
|
|
16
|
+
-- keys were computed. A claim made with other keys is refused until they are built again.
|
|
17
|
+
CREATE TABLE object_unique_properties (
|
|
18
|
+
project_id TEXT NOT NULL,
|
|
19
|
+
object_type_id TEXT NOT NULL,
|
|
20
|
+
property_id TEXT NOT NULL,
|
|
21
|
+
key_kind TEXT NOT NULL,
|
|
22
|
+
key_version INTEGER NOT NULL,
|
|
23
|
+
prepared_at TIMESTAMPTZ NOT NULL,
|
|
24
|
+
PRIMARY KEY (project_id, object_type_id, property_id)
|
|
25
|
+
);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
ALTER TABLE ai_model_call_usage
|
|
2
|
+
ADD COLUMN image_output_tokens BIGINT
|
|
3
|
+
CHECK (image_output_tokens IS NULL OR image_output_tokens >= 0),
|
|
4
|
+
ADD COLUMN audio_output_tokens BIGINT
|
|
5
|
+
CHECK (audio_output_tokens IS NULL OR audio_output_tokens >= 0),
|
|
6
|
+
ADD COLUMN output_images BIGINT
|
|
7
|
+
CHECK (output_images IS NULL OR output_images >= 0),
|
|
8
|
+
ADD COLUMN input_characters BIGINT
|
|
9
|
+
CHECK (input_characters IS NULL OR input_characters >= 0);
|
|
10
|
+
|
|
11
|
+
ALTER TABLE ai_model_call_usage
|
|
12
|
+
DROP CONSTRAINT ai_model_call_usage_model_kind_check;
|
|
13
|
+
|
|
14
|
+
ALTER TABLE ai_model_call_usage
|
|
15
|
+
ADD CONSTRAINT ai_model_call_usage_model_kind_check
|
|
16
|
+
CHECK (
|
|
17
|
+
model_kind IS NULL
|
|
18
|
+
OR model_kind IN (
|
|
19
|
+
'language', 'image', 'video', 'embedding', 'decision', 'transcription', 'speech', 'reranking'
|
|
20
|
+
)
|
|
21
|
+
);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
-- The files a model call produced. The execution that made the call decides who may read them.
|
|
2
|
+
CREATE TABLE ai_model_call_files (
|
|
3
|
+
project_id TEXT NOT NULL CHECK (length(trim(project_id)) > 0),
|
|
4
|
+
call_id TEXT NOT NULL CHECK (length(trim(call_id)) > 0),
|
|
5
|
+
execution_id TEXT NOT NULL CHECK (length(trim(execution_id)) > 0),
|
|
6
|
+
files JSONB NOT NULL CHECK (jsonb_typeof(files) = 'array' AND jsonb_array_length(files) > 0),
|
|
7
|
+
recorded_at TIMESTAMPTZ NOT NULL,
|
|
8
|
+
PRIMARY KEY (project_id, call_id),
|
|
9
|
+
FOREIGN KEY (project_id, execution_id)
|
|
10
|
+
REFERENCES executions(project_id, id)
|
|
11
|
+
);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
-- Agent memory: each repo, 'user:<userId>' or 'project', is a log of revisions. A save inserts one
|
|
2
|
+
-- revision and the file versions it wrote, and never updates a row. A file's current contents are
|
|
3
|
+
-- its highest-revision version; NULL contents mean that revision deleted it.
|
|
4
|
+
CREATE TABLE agent_memory_revisions (
|
|
5
|
+
project_id TEXT NOT NULL CHECK (length(trim(project_id)) > 0),
|
|
6
|
+
repo_id TEXT NOT NULL CHECK (length(trim(repo_id)) > 0),
|
|
7
|
+
revision INTEGER NOT NULL CHECK (revision > 0),
|
|
8
|
+
message TEXT NOT NULL CHECK (length(trim(message)) > 0),
|
|
9
|
+
author_principal_type TEXT NOT NULL
|
|
10
|
+
CHECK (author_principal_type IN ('user', 'serviceAccount', 'system')),
|
|
11
|
+
author_principal_id TEXT NOT NULL CHECK (length(author_principal_id) > 0),
|
|
12
|
+
source TEXT,
|
|
13
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
14
|
+
PRIMARY KEY (project_id, repo_id, revision)
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
-- A save also removes versions older than 90 days that are not their path's latest, so a
|
|
18
|
+
-- revision's rows here can be fewer than the files it wrote. Paths compare by code unit, as they
|
|
19
|
+
-- do in SQLite.
|
|
20
|
+
CREATE TABLE agent_memory_files (
|
|
21
|
+
project_id TEXT NOT NULL,
|
|
22
|
+
repo_id TEXT NOT NULL,
|
|
23
|
+
path TEXT COLLATE "C" NOT NULL CHECK (length(path) > 0),
|
|
24
|
+
revision INTEGER NOT NULL,
|
|
25
|
+
contents TEXT,
|
|
26
|
+
PRIMARY KEY (project_id, repo_id, path, revision),
|
|
27
|
+
FOREIGN KEY (project_id, repo_id, revision)
|
|
28
|
+
REFERENCES agent_memory_revisions (project_id, repo_id, revision) ON DELETE CASCADE
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
-- Deleting a repo cascades here, and Postgres does not index a foreign key by itself.
|
|
32
|
+
CREATE INDEX idx_agent_memory_files_revision
|
|
33
|
+
ON agent_memory_files (project_id, repo_id, revision);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- The threads an artifact is shared in: everyone who reads one of them reads the artifact. An
|
|
2
|
+
-- Agent turn publishing a version links its thread, for as long as a version from it remains; a
|
|
3
|
+
-- forked thread links its copies, `copied`, for as long as they last. Existing versions published
|
|
4
|
+
-- from a thread link it here.
|
|
5
|
+
CREATE TABLE artifact_threads (
|
|
6
|
+
project_id TEXT NOT NULL,
|
|
7
|
+
artifact_id TEXT NOT NULL,
|
|
8
|
+
thread_id TEXT NOT NULL CHECK (length(thread_id) > 0),
|
|
9
|
+
shared_at TIMESTAMPTZ NOT NULL,
|
|
10
|
+
copied BOOLEAN NOT NULL DEFAULT FALSE,
|
|
11
|
+
PRIMARY KEY (project_id, artifact_id, thread_id),
|
|
12
|
+
FOREIGN KEY (project_id, artifact_id) REFERENCES artifacts (project_id, id) ON DELETE CASCADE
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
CREATE INDEX idx_artifact_threads_thread ON artifact_threads (project_id, thread_id, artifact_id);
|
|
16
|
+
|
|
17
|
+
INSERT INTO artifact_threads (project_id, artifact_id, thread_id, shared_at)
|
|
18
|
+
SELECT project_id, artifact_id, origin_thread_id, MIN(created_at)
|
|
19
|
+
FROM artifact_versions
|
|
20
|
+
WHERE origin_thread_id IS NOT NULL
|
|
21
|
+
GROUP BY project_id, artifact_id, origin_thread_id;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
-- Calls of the Agent that wait for whoever asked for the turn. A call's run id is its key: a
|
|
2
|
+
-- thread holds at most one pending, approved or denied approval per key, and a cancelled,
|
|
3
|
+
-- superseded or expired one lets the same call ask again.
|
|
4
|
+
CREATE TABLE agent_approvals (
|
|
5
|
+
project_id TEXT NOT NULL,
|
|
6
|
+
id TEXT NOT NULL CHECK (length(trim(id)) > 0),
|
|
7
|
+
thread_id TEXT NOT NULL,
|
|
8
|
+
run_id TEXT NOT NULL CHECK (length(run_id) > 0),
|
|
9
|
+
caller_run_id TEXT NOT NULL CHECK (length(caller_run_id) > 0),
|
|
10
|
+
requested_by_type TEXT NOT NULL CHECK (requested_by_type IN ('user', 'serviceAccount', 'system')),
|
|
11
|
+
requested_by_id TEXT NOT NULL CHECK (length(requested_by_id) > 0),
|
|
12
|
+
mode TEXT NOT NULL CHECK (mode IN ('manual', 'accept-edits')),
|
|
13
|
+
call_key TEXT NOT NULL CHECK (length(call_key) > 0),
|
|
14
|
+
call JSONB NOT NULL,
|
|
15
|
+
status TEXT NOT NULL
|
|
16
|
+
CHECK (status IN ('pending', 'approved', 'denied', 'cancelled', 'superseded', 'expired')),
|
|
17
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
18
|
+
expires_at TIMESTAMPTZ NOT NULL,
|
|
19
|
+
decided_at TIMESTAMPTZ,
|
|
20
|
+
decided_by_type TEXT CHECK (decided_by_type IN ('user', 'serviceAccount', 'system')),
|
|
21
|
+
decided_by_id TEXT,
|
|
22
|
+
outcome JSONB,
|
|
23
|
+
reported_in_message_id TEXT,
|
|
24
|
+
PRIMARY KEY (project_id, id),
|
|
25
|
+
FOREIGN KEY (project_id, thread_id) REFERENCES agent_threads (project_id, id) ON DELETE CASCADE
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE UNIQUE INDEX idx_agent_approvals_live_call
|
|
29
|
+
ON agent_approvals (project_id, thread_id, call_key)
|
|
30
|
+
WHERE status IN ('pending', 'approved', 'denied');
|
|
31
|
+
|
|
32
|
+
CREATE INDEX idx_agent_approvals_thread ON agent_approvals (project_id, thread_id, created_at);
|
|
33
|
+
CREATE INDEX idx_agent_approvals_run ON agent_approvals (project_id, run_id, status);
|
|
34
|
+
CREATE INDEX idx_agent_approvals_caller_run ON agent_approvals (project_id, caller_run_id, status);
|
|
35
|
+
CREATE INDEX idx_agent_approvals_requester
|
|
36
|
+
ON agent_approvals (project_id, requested_by_type, requested_by_id, status);
|
|
37
|
+
|
|
38
|
+
-- What each user lets the Agent change without asking: a default, then their own choice in a
|
|
39
|
+
-- thread. Absent rows fall back, in that order.
|
|
40
|
+
CREATE TABLE agent_user_settings (
|
|
41
|
+
project_id TEXT NOT NULL,
|
|
42
|
+
principal_type TEXT NOT NULL CHECK (principal_type IN ('user', 'serviceAccount', 'system')),
|
|
43
|
+
principal_id TEXT NOT NULL CHECK (length(principal_id) > 0),
|
|
44
|
+
approval_mode TEXT NOT NULL CHECK (approval_mode IN ('manual', 'accept-edits', 'auto')),
|
|
45
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
46
|
+
PRIMARY KEY (project_id, principal_type, principal_id)
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
CREATE TABLE agent_thread_user_settings (
|
|
50
|
+
project_id TEXT NOT NULL,
|
|
51
|
+
thread_id TEXT NOT NULL,
|
|
52
|
+
principal_type TEXT NOT NULL CHECK (principal_type IN ('user', 'serviceAccount', 'system')),
|
|
53
|
+
principal_id TEXT NOT NULL CHECK (length(principal_id) > 0),
|
|
54
|
+
approval_mode TEXT NOT NULL CHECK (approval_mode IN ('manual', 'accept-edits', 'auto')),
|
|
55
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
56
|
+
PRIMARY KEY (project_id, thread_id, principal_type, principal_id),
|
|
57
|
+
FOREIGN KEY (project_id, thread_id) REFERENCES agent_threads (project_id, id) ON DELETE CASCADE
|
|
58
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- A paused Agent turn resumes once its requester decided on every call it held. What that needs:
|
|
2
|
+
-- the mode "Always allow" raised the turn to, whether the turn may still resume on a call, and the
|
|
3
|
+
-- claim a process takes before it runs an approved call left without its outcome.
|
|
4
|
+
ALTER TABLE agent_approvals
|
|
5
|
+
ADD COLUMN raised_mode TEXT CHECK (raised_mode IN ('manual', 'accept-edits', 'auto')),
|
|
6
|
+
ADD COLUMN turn_stopped_at TIMESTAMPTZ,
|
|
7
|
+
ADD COLUMN execution_claimed_at TIMESTAMPTZ;
|
|
8
|
+
|
|
9
|
+
-- The repair pass: calls past their time, approved calls without an outcome, and decided calls of
|
|
10
|
+
-- turns that may still resume. Each index covers only the rows that are still work.
|
|
11
|
+
CREATE INDEX idx_agent_approvals_due
|
|
12
|
+
ON agent_approvals (project_id, expires_at)
|
|
13
|
+
WHERE status = 'pending';
|
|
14
|
+
CREATE INDEX idx_agent_approvals_unfinished
|
|
15
|
+
ON agent_approvals (project_id, created_at, id)
|
|
16
|
+
WHERE status = 'approved' AND outcome IS NULL;
|
|
17
|
+
CREATE INDEX idx_agent_approvals_resumable
|
|
18
|
+
ON agent_approvals (project_id, created_at, id)
|
|
19
|
+
WHERE status IN ('approved', 'denied') AND reported_in_message_id IS NULL
|
|
20
|
+
AND turn_stopped_at IS NULL;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
-- A sub-agent works in a thread of its own: its task and its work are that thread's messages, and
|
|
2
|
+
-- each of its runs has the thread and a trigger message, as a turn does. The thread's parent is the
|
|
3
|
+
-- conversation, which every read of it goes through, and its spawning turn and key name it.
|
|
4
|
+
ALTER TABLE agent_threads
|
|
5
|
+
ADD COLUMN kind TEXT NOT NULL DEFAULT 'conversation'
|
|
6
|
+
CHECK (kind IN ('conversation', 'subagent')),
|
|
7
|
+
ADD COLUMN parent_thread_id TEXT,
|
|
8
|
+
ADD COLUMN parent_run_id TEXT,
|
|
9
|
+
ADD COLUMN spawn_key TEXT,
|
|
10
|
+
ADD CONSTRAINT agent_threads_kind_fields CHECK (
|
|
11
|
+
(
|
|
12
|
+
kind = 'conversation'
|
|
13
|
+
AND parent_thread_id IS NULL
|
|
14
|
+
AND parent_run_id IS NULL
|
|
15
|
+
AND spawn_key IS NULL
|
|
16
|
+
)
|
|
17
|
+
OR
|
|
18
|
+
(
|
|
19
|
+
kind = 'subagent'
|
|
20
|
+
AND parent_thread_id IS NOT NULL
|
|
21
|
+
AND parent_run_id IS NOT NULL
|
|
22
|
+
AND spawn_key IS NOT NULL
|
|
23
|
+
AND sandbox_params IS NULL
|
|
24
|
+
)
|
|
25
|
+
),
|
|
26
|
+
-- A conversation's sub-agent threads go with it.
|
|
27
|
+
ADD CONSTRAINT fk_agent_threads_parent
|
|
28
|
+
FOREIGN KEY (project_id, parent_thread_id)
|
|
29
|
+
REFERENCES agent_threads (project_id, id)
|
|
30
|
+
ON DELETE CASCADE,
|
|
31
|
+
ADD CONSTRAINT fk_agent_threads_parent_run
|
|
32
|
+
FOREIGN KEY (project_id, parent_run_id)
|
|
33
|
+
REFERENCES agent_runs (project_id, id)
|
|
34
|
+
ON DELETE RESTRICT,
|
|
35
|
+
ADD CONSTRAINT uq_agent_threads_spawn UNIQUE (project_id, parent_run_id, spawn_key);
|
|
36
|
+
|
|
37
|
+
ALTER TABLE agent_runs DROP CONSTRAINT agent_runs_kind_fields;
|
|
38
|
+
|
|
39
|
+
-- The children admitted before this have no thread. Each gets one named as a new child's is, with
|
|
40
|
+
-- its task as the only message: what it answered stays in its parent's `wait_agent` output.
|
|
41
|
+
CREATE TEMPORARY TABLE legacy_subagent_threads ON COMMIT DROP AS
|
|
42
|
+
SELECT
|
|
43
|
+
child.project_id,
|
|
44
|
+
child.id AS run_id,
|
|
45
|
+
'agt_thr_child_' || regexp_replace(child.id, '^agt_run_child_', '') AS thread_id,
|
|
46
|
+
'agt_msg_child_' || regexp_replace(child.id, '^agt_run_child_', '') AS message_id
|
|
47
|
+
FROM agent_runs AS child
|
|
48
|
+
WHERE child.kind = 'subagent' AND child.thread_id IS NULL;
|
|
49
|
+
|
|
50
|
+
INSERT INTO agent_threads (
|
|
51
|
+
project_id, id, kind, parent_thread_id, parent_run_id, spawn_key, owner_principal_type,
|
|
52
|
+
owner_principal_id, status, active_run_id, last_message_at, message_count, created_at,
|
|
53
|
+
updated_at
|
|
54
|
+
)
|
|
55
|
+
SELECT
|
|
56
|
+
child.project_id,
|
|
57
|
+
legacy.thread_id,
|
|
58
|
+
'subagent',
|
|
59
|
+
parent.thread_id,
|
|
60
|
+
child.parent_run_id,
|
|
61
|
+
child.spawn_key,
|
|
62
|
+
conversation.owner_principal_type,
|
|
63
|
+
conversation.owner_principal_id,
|
|
64
|
+
'active',
|
|
65
|
+
CASE WHEN child.status IN ('queued', 'running') THEN child.id END,
|
|
66
|
+
child.created_at,
|
|
67
|
+
1,
|
|
68
|
+
child.created_at,
|
|
69
|
+
COALESCE(child.completed_at, child.created_at)
|
|
70
|
+
FROM legacy_subagent_threads AS legacy
|
|
71
|
+
JOIN agent_runs AS child ON child.project_id = legacy.project_id AND child.id = legacy.run_id
|
|
72
|
+
JOIN agent_runs AS parent
|
|
73
|
+
ON parent.project_id = child.project_id AND parent.id = child.parent_run_id
|
|
74
|
+
JOIN agent_threads AS conversation
|
|
75
|
+
ON conversation.project_id = parent.project_id AND conversation.id = parent.thread_id;
|
|
76
|
+
|
|
77
|
+
INSERT INTO agent_messages (
|
|
78
|
+
project_id, id, thread_id, run_id, role, seq, parts, content_version, created_at
|
|
79
|
+
)
|
|
80
|
+
SELECT
|
|
81
|
+
child.project_id,
|
|
82
|
+
legacy.message_id,
|
|
83
|
+
legacy.thread_id,
|
|
84
|
+
child.id,
|
|
85
|
+
'user',
|
|
86
|
+
1,
|
|
87
|
+
jsonb_build_array(jsonb_build_object('type', 'text', 'text', child.spec->>'task')),
|
|
88
|
+
2,
|
|
89
|
+
child.created_at
|
|
90
|
+
FROM legacy_subagent_threads AS legacy
|
|
91
|
+
JOIN agent_runs AS child ON child.project_id = legacy.project_id AND child.id = legacy.run_id;
|
|
92
|
+
|
|
93
|
+
UPDATE agent_runs AS child
|
|
94
|
+
SET thread_id = legacy.thread_id, trigger_message_id = legacy.message_id
|
|
95
|
+
FROM legacy_subagent_threads AS legacy
|
|
96
|
+
WHERE child.project_id = legacy.project_id AND child.id = legacy.run_id;
|
|
97
|
+
|
|
98
|
+
DROP TABLE legacy_subagent_threads;
|
|
99
|
+
|
|
100
|
+
ALTER TABLE agent_runs ADD CONSTRAINT agent_runs_kind_fields CHECK (
|
|
101
|
+
(
|
|
102
|
+
kind = 'conversation'
|
|
103
|
+
AND thread_id IS NOT NULL
|
|
104
|
+
AND trigger_message_id IS NOT NULL
|
|
105
|
+
AND parent_run_id IS NULL
|
|
106
|
+
AND spawn_key IS NULL
|
|
107
|
+
AND result IS NULL
|
|
108
|
+
)
|
|
109
|
+
OR
|
|
110
|
+
(
|
|
111
|
+
kind = 'subagent'
|
|
112
|
+
AND thread_id IS NOT NULL
|
|
113
|
+
AND trigger_message_id IS NOT NULL
|
|
114
|
+
AND parent_run_id IS NOT NULL
|
|
115
|
+
AND spawn_key IS NOT NULL
|
|
116
|
+
AND spec IS NOT NULL
|
|
117
|
+
)
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
-- Lists read conversations only, so a conversation's children never weigh on them.
|
|
121
|
+
DROP INDEX idx_agent_threads_project_activity;
|
|
122
|
+
DROP INDEX idx_agent_threads_project_status_activity;
|
|
123
|
+
DROP INDEX idx_agent_threads_project_owner_activity;
|
|
124
|
+
|
|
125
|
+
CREATE INDEX idx_agent_threads_project_activity
|
|
126
|
+
ON agent_threads (project_id, (COALESCE(last_message_at, created_at)) DESC, id DESC)
|
|
127
|
+
WHERE kind = 'conversation';
|
|
128
|
+
CREATE INDEX idx_agent_threads_project_status_activity
|
|
129
|
+
ON agent_threads (project_id, status, (COALESCE(last_message_at, created_at)) DESC, id DESC)
|
|
130
|
+
WHERE kind = 'conversation';
|
|
131
|
+
CREATE INDEX idx_agent_threads_project_owner_activity
|
|
132
|
+
ON agent_threads (
|
|
133
|
+
project_id,
|
|
134
|
+
owner_principal_type,
|
|
135
|
+
owner_principal_id,
|
|
136
|
+
(COALESCE(last_message_at, created_at)) DESC,
|
|
137
|
+
id DESC
|
|
138
|
+
)
|
|
139
|
+
WHERE kind = 'conversation';
|
|
140
|
+
|
|
141
|
+
-- A conversation's children, in the order they were spawned. Only a child has a parent, so the
|
|
142
|
+
-- index holds children only, and a read by `parent_thread_id` can use it.
|
|
143
|
+
CREATE INDEX idx_agent_threads_children
|
|
144
|
+
ON agent_threads (project_id, parent_thread_id, created_at, id)
|
|
145
|
+
WHERE parent_thread_id IS NOT NULL;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
-- A child that pauses on its question waits for a later turn of its parent's conversation to
|
|
2
|
+
-- answer it. Each turn reads which of the conversation's children wait so: few do, so a partial
|
|
3
|
+
-- index keeps that read small however many children the conversation had.
|
|
4
|
+
CREATE INDEX idx_agent_runs_paused_on_question
|
|
5
|
+
ON agent_runs (project_id, parent_run_id)
|
|
6
|
+
WHERE kind = 'subagent' AND finish_reason = 'awaiting_answer';
|
package/src/migrations.ts
CHANGED
|
@@ -439,6 +439,19 @@ export const postgresStorageMigrations = defineMigrations<PostgresMigrationConte
|
|
|
439
439
|
pgSql("066-activity", activitySql),
|
|
440
440
|
pgSql("067-activity-timeline", activityTimelineSql),
|
|
441
441
|
pgSql("068-activity-interactions", activityInteractionsSql),
|
|
442
|
+
pgSql("069-artifact-kinds", artifactKindsSql),
|
|
443
|
+
pgSql("070-artifact-state", artifactStateSql),
|
|
444
|
+
pgSql("071-agent-thread-participants", agentThreadParticipantsSql),
|
|
445
|
+
pgSql("072-forked-context-checkpoints", forkedContextCheckpointsSql),
|
|
446
|
+
pgSql("073-unique-values", uniqueValuesSql),
|
|
447
|
+
pgSql("074-generated-media-usage", generatedMediaUsageSql),
|
|
448
|
+
pgSql("075-model-call-files", modelCallFilesSql),
|
|
449
|
+
pgSql("076-agent-memory", agentMemorySql),
|
|
450
|
+
pgSql("077-artifact-threads", artifactThreadsSql),
|
|
451
|
+
pgSql("078-agent-approvals", agentApprovalsSql),
|
|
452
|
+
pgSql("079-agent-approval-resume", agentApprovalResumeSql),
|
|
453
|
+
pgSql("080-subagent-threads", subagentThreadsSql),
|
|
454
|
+
pgSql("081-subagent-questions", subagentQuestionsSql),
|
|
442
455
|
],
|
|
443
456
|
})
|
|
444
457
|
|
|
@@ -481,3 +494,24 @@ import activityTimelineSql from "./migrations/067-activity-timeline.sql" with {
|
|
|
481
494
|
import activityInteractionsSql from "./migrations/068-activity-interactions.sql" with {
|
|
482
495
|
type: "text",
|
|
483
496
|
}
|
|
497
|
+
import artifactKindsSql from "./migrations/069-artifact-kinds.sql" with { type: "text" }
|
|
498
|
+
import artifactStateSql from "./migrations/070-artifact-state.sql" with { type: "text" }
|
|
499
|
+
import agentThreadParticipantsSql from "./migrations/071-agent-thread-participants.sql" with {
|
|
500
|
+
type: "text",
|
|
501
|
+
}
|
|
502
|
+
import forkedContextCheckpointsSql from "./migrations/072-forked-context-checkpoints.sql" with {
|
|
503
|
+
type: "text",
|
|
504
|
+
}
|
|
505
|
+
import uniqueValuesSql from "./migrations/073-unique-values.sql" with { type: "text" }
|
|
506
|
+
import generatedMediaUsageSql from "./migrations/074-generated-media-usage.sql" with {
|
|
507
|
+
type: "text",
|
|
508
|
+
}
|
|
509
|
+
import modelCallFilesSql from "./migrations/075-model-call-files.sql" with { type: "text" }
|
|
510
|
+
import agentMemorySql from "./migrations/076-agent-memory.sql" with { type: "text" }
|
|
511
|
+
import artifactThreadsSql from "./migrations/077-artifact-threads.sql" with { type: "text" }
|
|
512
|
+
import agentApprovalsSql from "./migrations/078-agent-approvals.sql" with { type: "text" }
|
|
513
|
+
import agentApprovalResumeSql from "./migrations/079-agent-approval-resume.sql" with {
|
|
514
|
+
type: "text",
|
|
515
|
+
}
|
|
516
|
+
import subagentThreadsSql from "./migrations/080-subagent-threads.sql" with { type: "text" }
|
|
517
|
+
import subagentQuestionsSql from "./migrations/081-subagent-questions.sql" with { type: "text" }
|