blob: efc3252f139ffc4fc63fbd10c0d1163e823e9213 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import type { DatabaseSync } from 'node:sqlite';
export const SQLITE_CORE_EXECUTION_SCHEMA_VERSION = 9;
export const MODEL_PROJECTION_TARGET_SQL =
"CASE WHEN json_valid(record_json) THEN CASE WHEN json_type(record_json, '$.data.transition.target.runtimeEventId') = 'text' THEN nullif(json_extract(record_json, '$.data.transition.target.runtimeEventId'), '') WHEN json_type(record_json, '$.data.runtimeEventId') = 'text' THEN nullif(json_extract(record_json, '$.data.runtimeEventId'), '') END END";
export function migrateSqliteCoreExecutionDatabase(db: DatabaseSync): void {
db.exec(`
CREATE TABLE IF NOT EXISTS core_agent_runs (
session_id TEXT NOT NULL,
run_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
latest_model_call_sequence INTEGER CHECK (latest_model_call_sequence >= 0),
PRIMARY KEY (session_id, run_id)
);
CREATE INDEX IF NOT EXISTS core_agent_runs_session_order
ON core_agent_runs(session_id, created_at, run_id);
CREATE TABLE IF NOT EXISTS core_agent_run_events (
session_id TEXT NOT NULL,
run_id TEXT NOT NULL,
sequence INTEGER NOT NULL CHECK (sequence >= 0),
event_id TEXT NOT NULL,
event_type TEXT NOT NULL,
event_ts INTEGER NOT NULL,
record_json TEXT NOT NULL,
PRIMARY KEY (session_id, run_id, sequence),
FOREIGN KEY (session_id, run_id)
REFERENCES core_agent_runs(session_id, run_id)
ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS core_agent_run_events_identity
ON core_agent_run_events(session_id, run_id, event_id);
CREATE INDEX IF NOT EXISTS core_agent_run_events_type_sequence
ON core_agent_run_events(event_type, session_id, run_id, sequence);
CREATE INDEX IF NOT EXISTS core_model_projection_target
ON core_agent_run_events(session_id,
${MODEL_PROJECTION_TARGET_SQL}
) WHERE event_type = 'model_projection_transition_recorded';
CREATE TABLE IF NOT EXISTS core_agent_run_projections (
session_id TEXT NOT NULL,
event_type TEXT NOT NULL,
event_json TEXT,
PRIMARY KEY (session_id, event_type)
);
CREATE TABLE IF NOT EXISTS core_root_turn_admissions (
session_id TEXT NOT NULL,
turn_id TEXT NOT NULL,
admitted_at INTEGER NOT NULL,
record_json TEXT NOT NULL,
PRIMARY KEY (session_id, turn_id)
);
CREATE INDEX IF NOT EXISTS core_root_turn_admissions_order
ON core_root_turn_admissions(session_id, admitted_at, turn_id);
CREATE TABLE IF NOT EXISTS core_root_turn_start_rejections (
session_id TEXT NOT NULL,
turn_id TEXT NOT NULL,
rejected_at INTEGER NOT NULL,
record_json TEXT NOT NULL,
PRIMARY KEY (session_id, turn_id)
);
CREATE TABLE IF NOT EXISTS core_root_source_message_proofs (
session_id TEXT NOT NULL,
message_id TEXT NOT NULL,
turn_id TEXT NOT NULL,
PRIMARY KEY (session_id, message_id),
FOREIGN KEY (session_id, turn_id)
REFERENCES core_root_turn_admissions(session_id, turn_id)
ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS core_interaction_requests (
request_id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
turn_id TEXT NOT NULL,
run_id TEXT NOT NULL,
request_kind TEXT NOT NULL,
created_at INTEGER NOT NULL,
record_json TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS core_interaction_pending
ON core_interaction_requests(session_id, created_at, request_id);
CREATE TABLE IF NOT EXISTS core_interaction_outcomes (
request_id TEXT PRIMARY KEY,
record_json TEXT NOT NULL,
FOREIGN KEY (request_id)
REFERENCES core_interaction_requests(request_id)
ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS core_client_capability_session_grants (
session_id TEXT NOT NULL,
provider_id TEXT NOT NULL,
contract_id TEXT NOT NULL,
server_id TEXT NOT NULL,
tool_name TEXT NOT NULL,
capability TEXT NOT NULL,
scope_kind TEXT NOT NULL,
scope_value TEXT NOT NULL,
granted_at INTEGER NOT NULL,
record_json TEXT NOT NULL,
PRIMARY KEY (
session_id, provider_id, contract_id, capability, scope_kind, scope_value
)
);
CREATE INDEX IF NOT EXISTS core_client_capability_session_grants_session
ON core_client_capability_session_grants(session_id, granted_at);
CREATE TABLE IF NOT EXISTS core_shell_runs (
session_id TEXT NOT NULL,
shell_run_id TEXT NOT NULL,
started_at INTEGER NOT NULL,
record_json TEXT NOT NULL,
PRIMARY KEY (session_id, shell_run_id)
);
CREATE INDEX IF NOT EXISTS core_shell_runs_session_order
ON core_shell_runs(session_id, started_at, shell_run_id);
`);
ensureColumn(
db,
'core_agent_runs',
'latest_model_call_sequence',
'INTEGER CHECK (latest_model_call_sequence >= 0)',
);
// The runtime migration runs first and has already turned every stored Run header into an
// invocation opening fact, so the row keeps only what the ledger needs to hang its events on.
dropColumn(db, 'core_agent_runs', 'record_json');
db.exec(`
UPDATE core_agent_runs
SET latest_model_call_sequence = (
SELECT MAX(sequence)
FROM core_agent_run_events
WHERE core_agent_run_events.session_id = core_agent_runs.session_id
AND core_agent_run_events.run_id = core_agent_runs.run_id
AND event_type = 'model_call_attempt_recorded'
)
WHERE latest_model_call_sequence IS NULL
AND EXISTS (
SELECT 1
FROM core_agent_run_events
WHERE core_agent_run_events.session_id = core_agent_runs.session_id
AND core_agent_run_events.run_id = core_agent_runs.run_id
AND event_type = 'model_call_attempt_recorded'
);
CREATE INDEX IF NOT EXISTS core_agent_runs_model_call_high_water
ON core_agent_runs(session_id, latest_model_call_sequence, run_id)
WHERE latest_model_call_sequence IS NOT NULL;
DROP INDEX IF EXISTS core_root_turn_continuation_source;
CREATE INDEX IF NOT EXISTS core_root_turn_continuation_source_v2
ON core_root_turn_admissions(
session_id,
json_extract(record_json, '$.execution.sourceTurnId'),
json_extract(record_json, '$.execution.sourceRunId')
)
WHERE json_extract(record_json, '$.execution.kind') = 'safe_boundary_continuation';
DROP INDEX IF EXISTS core_agent_runs_identity;
DROP TABLE IF EXISTS core_message_receipts;
DROP TABLE IF EXISTS core_message_host_epochs;
`);
}
function ensureColumn(db: DatabaseSync, table: string, column: string, definition: string): void {
const columns = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name?: unknown }>;
if (columns.some((candidate) => candidate.name === column)) return;
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
}
function dropColumn(db: DatabaseSync, table: string, column: string): void {
const columns = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name?: unknown }>;
if (!columns.some((candidate) => candidate.name === column)) return;
db.exec(`ALTER TABLE ${table} DROP COLUMN ${column}`);
}