repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
Mirrored from https://github.com/yingqi-z20/Agent-libOS
stars
latest
clone command
git clone gitlawb://did:key:z6MkqRzA...RfoM/yingqi-z20-Agen...git clone gitlawb://did:key:z6MkqRzA.../yingqi-z20-Agen...d98dd2c9IPC1d ago| #1 | from __future__ import annotations |
| #2 | |
| #3 | import sqlite3 |
| #4 | import threading |
| #5 | from pathlib import Path |
| #6 | from typing import Any, Iterable |
| #7 | |
| #8 | from agent_libos.utils.ids import utc_now |
| #9 | from agent_libos.models import ( |
| #10 | AgentObject, |
| #11 | AgentProcess, |
| #12 | AuditRecord, |
| #13 | Capability, |
| #14 | Checkpoint, |
| #15 | Event, |
| #16 | EventPriority, |
| #17 | EventType, |
| #18 | HumanRequest, |
| #19 | HumanRequestStatus, |
| #20 | LLMCallRecord, |
| #21 | MemoryView, |
| #22 | ObjectFilter, |
| #23 | ObjectHandle, |
| #24 | ObjectLink, |
| #25 | ObjectMetadata, |
| #26 | ObjectNamespace, |
| #27 | ObjectType, |
| #28 | ProcessStatus, |
| #29 | ProcessMessage, |
| #30 | ProcessMessageKind, |
| #31 | ProcessMessageStatus, |
| #32 | Provenance, |
| #33 | RelationType, |
| #34 | ResourceBudget, |
| #35 | ToolCandidate, |
| #36 | ToolCandidateStatus, |
| #37 | ToolHandle, |
| #38 | ToolSpec, |
| #39 | ViewMode, |
| #40 | ) |
| #41 | from agent_libos.utils.serde import dumps, loads |
| #42 | |
| #43 | |
| #44 | class SQLiteStore: |
| #45 | """Small SQLite repository used by the MVP runtime. |
| #46 | |
| #47 | The store is intentionally thin: policy, permissions, and process semantics |
| #48 | live in managers. This layer only owns durable shape and reconstruction. |
| #49 | """ |
| #50 | |
| #51 | SNAPSHOT_TABLES = [ |
| #52 | "object_namespaces", |
| #53 | "objects", |
| #54 | "object_links", |
| #55 | "processes", |
| #56 | "events", |
| #57 | "capabilities", |
| #58 | "human_requests", |
| #59 | "llm_calls", |
| #60 | "process_messages", |
| #61 | "tools", |
| #62 | "tool_candidates", |
| #63 | ] |
| #64 | SYSTEM_NAMESPACE = "system" |
| #65 | |
| #66 | def __init__(self, path: str | Path = ":memory:"): |
| #67 | self.path = str(path) |
| #68 | if self.path != ":memory:": |
| #69 | Path(self.path).parent.mkdir(parents=True, exist_ok=True) |
| #70 | self.conn = sqlite3.connect(self.path, check_same_thread=False) |
| #71 | self.conn.row_factory = sqlite3.Row |
| #72 | self._lock = threading.RLock() |
| #73 | # Object payloads are runtime memory, not durable database state. SQLite |
| #74 | # stores only metadata plus a marker saying whether a payload was present |
| #75 | # in this process. |
| #76 | self._object_payloads: dict[str, Any] = {} |
| #77 | self.initialize() |
| #78 | |
| #79 | def close(self) -> None: |
| #80 | self.conn.close() |
| #81 | |
| #82 | def initialize(self) -> None: |
| #83 | with self._lock: |
| #84 | self.conn.executescript( |
| #85 | """ |
| #86 | CREATE TABLE IF NOT EXISTS objects ( |
| #87 | oid TEXT PRIMARY KEY, |
| #88 | namespace TEXT NOT NULL DEFAULT 'system', |
| #89 | name TEXT NOT NULL, |
| #90 | type TEXT NOT NULL, |
| #91 | schema_version TEXT NOT NULL, |
| #92 | payload_json TEXT NOT NULL, |
| #93 | metadata_json TEXT NOT NULL, |
| #94 | provenance_json TEXT NOT NULL, |
| #95 | version INTEGER NOT NULL, |
| #96 | immutable INTEGER NOT NULL, |
| #97 | created_by TEXT NOT NULL, |
| #98 | created_at TEXT NOT NULL, |
| #99 | updated_at TEXT NOT NULL |
| #100 | ); |
| #101 | |
| #102 | CREATE TABLE IF NOT EXISTS object_namespaces ( |
| #103 | namespace TEXT PRIMARY KEY, |
| #104 | parent_namespace TEXT, |
| #105 | metadata_json TEXT NOT NULL, |
| #106 | created_by TEXT NOT NULL, |
| #107 | created_at TEXT NOT NULL, |
| #108 | updated_at TEXT NOT NULL |
| #109 | ); |
| #110 | |
| #111 | CREATE TABLE IF NOT EXISTS object_links ( |
| #112 | id TEXT PRIMARY KEY, |
| #113 | src_oid TEXT NOT NULL, |
| #114 | relation TEXT NOT NULL, |
| #115 | dst_oid TEXT NOT NULL, |
| #116 | metadata_json TEXT NOT NULL, |
| #117 | created_by TEXT NOT NULL, |
| #118 | created_at TEXT NOT NULL |
| #119 | ); |
| #120 | |
| #121 | CREATE TABLE IF NOT EXISTS processes ( |
| #122 | pid TEXT PRIMARY KEY, |
| #123 | parent_pid TEXT, |
| #124 | image_id TEXT NOT NULL, |
| #125 | status TEXT NOT NULL, |
| #126 | goal_oid TEXT, |
| #127 | memory_view_json TEXT, |
| #128 | capabilities_json TEXT NOT NULL, |
| #129 | loaded_skills_json TEXT NOT NULL, |
| #130 | tool_table_json TEXT NOT NULL, |
| #131 | event_cursor TEXT, |
| #132 | checkpoint_head TEXT, |
| #133 | status_message TEXT, |
| #134 | resource_budget_json TEXT NOT NULL, |
| #135 | working_directory TEXT NOT NULL DEFAULT '.', |
| #136 | created_at TEXT NOT NULL, |
| #137 | updated_at TEXT NOT NULL |
| #138 | ); |
| #139 | |
| #140 | CREATE TABLE IF NOT EXISTS events ( |
| #141 | event_id TEXT PRIMARY KEY, |
| #142 | type TEXT NOT NULL, |
| #143 | source TEXT NOT NULL, |
| #144 | target TEXT, |
| #145 | payload_json TEXT NOT NULL, |
| #146 | priority TEXT NOT NULL, |
| #147 | created_at TEXT NOT NULL, |
| #148 | correlation_id TEXT, |
| #149 | causality_json TEXT NOT NULL |
| #150 | ); |
| #151 | |
| #152 | CREATE TABLE IF NOT EXISTS capabilities ( |
| #153 | cap_id TEXT PRIMARY KEY, |
| #154 | subject TEXT NOT NULL, |
| #155 | resource TEXT NOT NULL, |
| #156 | rights_json TEXT NOT NULL, |
| #157 | constraints_json TEXT NOT NULL, |
| #158 | issued_by TEXT NOT NULL, |
| #159 | issued_at TEXT NOT NULL, |
| #160 | expires_at TEXT, |
| #161 | delegable INTEGER NOT NULL, |
| #162 | revocable INTEGER NOT NULL, |
| #163 | revoked INTEGER NOT NULL |
| #164 | ); |
| #165 | |
| #166 | CREATE TABLE IF NOT EXISTS audit_records ( |
| #167 | record_id TEXT PRIMARY KEY, |
| #168 | timestamp TEXT NOT NULL, |
| #169 | actor TEXT NOT NULL, |
| #170 | action TEXT NOT NULL, |
| #171 | target TEXT, |
| #172 | input_refs_json TEXT NOT NULL, |
| #173 | output_refs_json TEXT NOT NULL, |
| #174 | capability_refs_json TEXT NOT NULL, |
| #175 | decision_json TEXT, |
| #176 | correlation_id TEXT, |
| #177 | parent_record_id TEXT |
| #178 | ); |
| #179 | |
| #180 | CREATE TABLE IF NOT EXISTS checkpoints ( |
| #181 | checkpoint_id TEXT PRIMARY KEY, |
| #182 | pid TEXT NOT NULL, |
| #183 | reason TEXT NOT NULL, |
| #184 | snapshot_json TEXT NOT NULL, |
| #185 | created_at TEXT NOT NULL |
| #186 | ); |
| #187 | |
| #188 | CREATE TABLE IF NOT EXISTS human_requests ( |
| #189 | request_id TEXT PRIMARY KEY, |
| #190 | pid TEXT NOT NULL, |
| #191 | human TEXT NOT NULL, |
| #192 | payload_json TEXT NOT NULL, |
| #193 | status TEXT NOT NULL, |
| #194 | decision_json TEXT, |
| #195 | blocking INTEGER NOT NULL, |
| #196 | created_at TEXT NOT NULL, |
| #197 | updated_at TEXT NOT NULL |
| #198 | ); |
| #199 | |
| #200 | CREATE TABLE IF NOT EXISTS llm_calls ( |
| #201 | call_id TEXT PRIMARY KEY, |
| #202 | pid TEXT, |
| #203 | image_id TEXT, |
| #204 | purpose TEXT NOT NULL, |
| #205 | status TEXT NOT NULL, |
| #206 | api TEXT, |
| #207 | model TEXT, |
| #208 | request_id TEXT, |
| #209 | response_id TEXT, |
| #210 | messages_json TEXT NOT NULL, |
| #211 | tools_json TEXT NOT NULL, |
| #212 | request_options_json TEXT NOT NULL, |
| #213 | response_content TEXT NOT NULL, |
| #214 | tool_calls_json TEXT NOT NULL, |
| #215 | reasoning_json TEXT, |
| #216 | usage_json TEXT NOT NULL, |
| #217 | raw_response_json TEXT, |
| #218 | error TEXT, |
| #219 | created_at TEXT NOT NULL, |
| #220 | completed_at TEXT |
| #221 | ); |
| #222 | |
| #223 | CREATE INDEX IF NOT EXISTS idx_llm_calls_pid_created |
| #224 | ON llm_calls(pid, created_at); |
| #225 | |
| #226 | CREATE INDEX IF NOT EXISTS idx_llm_calls_request_id |
| #227 | ON llm_calls(request_id); |
| #228 | |
| #229 | CREATE INDEX IF NOT EXISTS idx_llm_calls_response_id |
| #230 | ON llm_calls(response_id); |
| #231 | |
| #232 | CREATE TABLE IF NOT EXISTS process_messages ( |
| #233 | message_id TEXT PRIMARY KEY, |
| #234 | sender TEXT NOT NULL, |
| #235 | recipient_pid TEXT NOT NULL, |
| #236 | kind TEXT NOT NULL, |
| #237 | channel TEXT NOT NULL DEFAULT 'default', |
| #238 | correlation_id TEXT, |
| #239 | reply_to TEXT, |
| #240 | subject TEXT NOT NULL, |
| #241 | body TEXT NOT NULL, |
| #242 | payload_json TEXT NOT NULL, |
| #243 | status TEXT NOT NULL, |
| #244 | created_at TEXT NOT NULL, |
| #245 | updated_at TEXT NOT NULL, |
| #246 | acked_at TEXT |
| #247 | ); |
| #248 | |
| #249 | CREATE INDEX IF NOT EXISTS idx_process_messages_recipient_status_kind |
| #250 | ON process_messages(recipient_pid, status, kind, channel, created_at); |
| #251 | |
| #252 | CREATE INDEX IF NOT EXISTS idx_process_messages_correlation |
| #253 | ON process_messages(recipient_pid, correlation_id, status, created_at); |
| #254 | |
| #255 | CREATE TABLE IF NOT EXISTS tools ( |
| #256 | tool_id TEXT PRIMARY KEY, |
| #257 | name TEXT NOT NULL, |
| #258 | spec_json TEXT NOT NULL, |
| #259 | scope TEXT NOT NULL, |
| #260 | registered_by TEXT NOT NULL, |
| #261 | created_at TEXT NOT NULL, |
| #262 | ephemeral INTEGER NOT NULL |
| #263 | ); |
| #264 | |
| #265 | CREATE TABLE IF NOT EXISTS tool_candidates ( |
| #266 | candidate_id TEXT PRIMARY KEY, |
| #267 | pid TEXT NOT NULL, |
| #268 | spec_json TEXT NOT NULL, |
| #269 | source_code TEXT NOT NULL, |
| #270 | tests_json TEXT NOT NULL, |
| #271 | requested_capabilities_json TEXT NOT NULL, |
| #272 | status TEXT NOT NULL, |
| #273 | validation_json TEXT, |
| #274 | created_at TEXT NOT NULL, |
| #275 | updated_at TEXT NOT NULL |
| #276 | ); |
| #277 | """ |
| #278 | ) |
| #279 | self._ensure_object_namespace_schema() |
| #280 | self._ensure_process_schema() |
| #281 | self._ensure_llm_call_schema() |
| #282 | self._ensure_process_message_schema() |
| #283 | self.conn.commit() |
| #284 | |
| #285 | def _execute(self, sql: str, params: Iterable[Any] = ()) -> sqlite3.Cursor: |
| #286 | with self._lock: |
| #287 | cur = self.conn.execute(sql, tuple(params)) |
| #288 | self.conn.commit() |
| #289 | return cur |
| #290 | |
| #291 | def _query(self, sql: str, params: Iterable[Any] = ()) -> list[sqlite3.Row]: |
| #292 | with self._lock: |
| #293 | return list(self.conn.execute(sql, tuple(params))) |
| #294 | |
| #295 | def insert_object(self, obj: AgentObject) -> None: |
| #296 | self._object_payloads[obj.oid] = obj.payload |
| #297 | self._execute( |
| #298 | """ |
| #299 | INSERT INTO objects ( |
| #300 | oid, namespace, name, type, schema_version, payload_json, metadata_json, |
| #301 | provenance_json, version, immutable, created_by, created_at, updated_at |
| #302 | ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
| #303 | """, |
| #304 | ( |
| #305 | obj.oid, |
| #306 | obj.namespace, |
| #307 | obj.name, |
| #308 | obj.type.value, |
| #309 | obj.schema_version, |
| #310 | dumps(self._memory_payload_marker(present=True)), |
| #311 | dumps(obj.metadata), |
| #312 | dumps(obj.provenance), |
| #313 | obj.version, |
| #314 | int(obj.immutable), |
| #315 | obj.created_by, |
| #316 | obj.created_at, |
| #317 | obj.updated_at, |
| #318 | ), |
| #319 | ) |
| #320 | |
| #321 | def update_object(self, obj: AgentObject) -> None: |
| #322 | self._object_payloads[obj.oid] = obj.payload |
| #323 | self._execute( |
| #324 | """ |
| #325 | UPDATE objects |
| #326 | SET namespace = ?, name = ?, type = ?, schema_version = ?, payload_json = ?, metadata_json = ?, |
| #327 | provenance_json = ?, version = ?, immutable = ?, created_by = ?, |
| #328 | created_at = ?, updated_at = ? |
| #329 | WHERE oid = ? |
| #330 | """, |
| #331 | ( |
| #332 | obj.namespace, |
| #333 | obj.name, |
| #334 | obj.type.value, |
| #335 | obj.schema_version, |
| #336 | dumps(self._memory_payload_marker(present=True)), |
| #337 | dumps(obj.metadata), |
| #338 | dumps(obj.provenance), |
| #339 | obj.version, |
| #340 | int(obj.immutable), |
| #341 | obj.created_by, |
| #342 | obj.created_at, |
| #343 | obj.updated_at, |
| #344 | obj.oid, |
| #345 | ), |
| #346 | ) |
| #347 | |
| #348 | def get_object(self, oid: str) -> AgentObject | None: |
| #349 | rows = self._query("SELECT * FROM objects WHERE oid = ?", (oid,)) |
| #350 | # A row without an in-memory payload is a directory remnant from a prior |
| #351 | # runtime instance or checkpoint restore, not a materializable Object. |
| #352 | if not rows or oid not in self._object_payloads: |
| #353 | return None |
| #354 | return self._row_to_object(rows[0]) |
| #355 | |
| #356 | def get_object_by_name(self, name: str, namespace: str) -> AgentObject | None: |
| #357 | rows = self._query("SELECT * FROM objects WHERE namespace = ? AND name = ?", (namespace, name)) |
| #358 | if not rows or rows[0]["oid"] not in self._object_payloads: |
| #359 | return None |
| #360 | return self._row_to_object(rows[0]) |
| #361 | |
| #362 | def object_name_exists(self, name: str, namespace: str, except_oid: str | None = None) -> bool: |
| #363 | rows = self._query("SELECT oid FROM objects WHERE namespace = ? AND name = ?", (namespace, name)) |
| #364 | return any(row["oid"] != except_oid for row in rows) |
| #365 | |
| #366 | def list_objects(self, namespace: str | None = None) -> list[AgentObject]: |
| #367 | if namespace is None: |
| #368 | rows = self._query("SELECT * FROM objects") |
| #369 | else: |
| #370 | rows = self._query("SELECT * FROM objects WHERE namespace = ?", (namespace,)) |
| #371 | return [ |
| #372 | self._row_to_object(row) |
| #373 | for row in rows |
| #374 | if row["oid"] in self._object_payloads |
| #375 | ] |
| #376 | |
| #377 | def list_object_oids_created_by(self, created_by: str) -> list[str]: |
| #378 | rows = self._query("SELECT oid FROM objects WHERE created_by = ? ORDER BY created_at", (created_by,)) |
| #379 | return [str(row["oid"]) for row in rows] |
| #380 | |
| #381 | def delete_object(self, oid: str) -> None: |
| #382 | self._object_payloads.pop(oid, None) |
| #383 | self._execute("DELETE FROM object_links WHERE src_oid = ? OR dst_oid = ?", (oid, oid)) |
| #384 | self._execute("DELETE FROM objects WHERE oid = ?", (oid,)) |
| #385 | |
| #386 | def insert_namespace(self, namespace: ObjectNamespace) -> None: |
| #387 | self._execute( |
| #388 | """ |
| #389 | INSERT INTO object_namespaces ( |
| #390 | namespace, parent_namespace, metadata_json, created_by, created_at, updated_at |
| #391 | ) VALUES (?, ?, ?, ?, ?, ?) |
| #392 | """, |
| #393 | ( |
| #394 | namespace.namespace, |
| #395 | namespace.parent_namespace, |
| #396 | dumps(namespace.metadata), |
| #397 | namespace.created_by, |
| #398 | namespace.created_at, |
| #399 | namespace.updated_at, |
| #400 | ), |
| #401 | ) |
| #402 | |
| #403 | def get_namespace(self, namespace: str) -> ObjectNamespace | None: |
| #404 | rows = self._query("SELECT * FROM object_namespaces WHERE namespace = ?", (namespace,)) |
| #405 | return self._row_to_namespace(rows[0]) if rows else None |
| #406 | |
| #407 | def namespace_exists(self, namespace: str) -> bool: |
| #408 | rows = self._query("SELECT 1 FROM object_namespaces WHERE namespace = ?", (namespace,)) |
| #409 | return bool(rows) |
| #410 | |
| #411 | def list_namespaces(self, parent_namespace: str | None = None) -> list[ObjectNamespace]: |
| #412 | if parent_namespace is None: |
| #413 | rows = self._query("SELECT * FROM object_namespaces ORDER BY namespace") |
| #414 | else: |
| #415 | rows = self._query( |
| #416 | "SELECT * FROM object_namespaces WHERE parent_namespace = ? ORDER BY namespace", |
| #417 | (parent_namespace,), |
| #418 | ) |
| #419 | return [self._row_to_namespace(row) for row in rows] |
| #420 | |
| #421 | def insert_link(self, link: ObjectLink) -> None: |
| #422 | self._execute( |
| #423 | "INSERT INTO object_links VALUES (?, ?, ?, ?, ?, ?, ?)", |
| #424 | ( |
| #425 | link.link_id, |
| #426 | link.src, |
| #427 | link.relation.value, |
| #428 | link.dst, |
| #429 | dumps(link.metadata), |
| #430 | link.created_by, |
| #431 | link.created_at, |
| #432 | ), |
| #433 | ) |
| #434 | |
| #435 | def list_links(self, src: str | None = None, dst: str | None = None) -> list[ObjectLink]: |
| #436 | if src is not None: |
| #437 | rows = self._query("SELECT * FROM object_links WHERE src_oid = ?", (src,)) |
| #438 | elif dst is not None: |
| #439 | rows = self._query("SELECT * FROM object_links WHERE dst_oid = ?", (dst,)) |
| #440 | else: |
| #441 | rows = self._query("SELECT * FROM object_links") |
| #442 | return [self._row_to_link(row) for row in rows] |
| #443 | |
| #444 | def insert_process(self, process: AgentProcess) -> None: |
| #445 | self._execute( |
| #446 | """ |
| #447 | INSERT INTO processes VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
| #448 | """, |
| #449 | self._process_params(process), |
| #450 | ) |
| #451 | |
| #452 | def update_process(self, process: AgentProcess) -> None: |
| #453 | self._execute( |
| #454 | """ |
| #455 | UPDATE processes |
| #456 | SET parent_pid = ?, image_id = ?, status = ?, goal_oid = ?, |
| #457 | memory_view_json = ?, capabilities_json = ?, loaded_skills_json = ?, |
| #458 | tool_table_json = ?, event_cursor = ?, checkpoint_head = ?, |
| #459 | status_message = ?, resource_budget_json = ?, working_directory = ?, created_at = ?, |
| #460 | updated_at = ? |
| #461 | WHERE pid = ? |
| #462 | """, |
| #463 | ( |
| #464 | process.parent_pid, |
| #465 | process.image_id, |
| #466 | process.status.value, |
| #467 | process.goal_oid, |
| #468 | dumps(process.memory_view) if process.memory_view else None, |
| #469 | dumps(process.capabilities), |
| #470 | dumps(process.loaded_skills), |
| #471 | dumps(process.tool_table), |
| #472 | process.event_cursor, |
| #473 | process.checkpoint_head, |
| #474 | process.status_message, |
| #475 | dumps(process.resource_budget), |
| #476 | process.working_directory, |
| #477 | process.created_at, |
| #478 | process.updated_at, |
| #479 | process.pid, |
| #480 | ), |
| #481 | ) |
| #482 | |
| #483 | def get_process(self, pid: str) -> AgentProcess | None: |
| #484 | rows = self._query("SELECT * FROM processes WHERE pid = ?", (pid,)) |
| #485 | return self._row_to_process(rows[0]) if rows else None |
| #486 | |
| #487 | def list_processes(self) -> list[AgentProcess]: |
| #488 | return [self._row_to_process(row) for row in self._query("SELECT * FROM processes")] |
| #489 | |
| #490 | def insert_event(self, event: Event) -> None: |
| #491 | self._execute( |
| #492 | "INSERT INTO events VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", |
| #493 | ( |
| #494 | event.event_id, |
| #495 | event.type.value, |
| #496 | event.source, |
| #497 | event.target, |
| #498 | dumps(event.payload), |
| #499 | event.priority.value, |
| #500 | event.created_at, |
| #501 | event.correlation_id, |
| #502 | dumps(event.causality), |
| #503 | ), |
| #504 | ) |
| #505 | |
| #506 | def list_events(self, target: str | None = None) -> list[Event]: |
| #507 | if target is None: |
| #508 | rows = self._query("SELECT * FROM events ORDER BY created_at") |
| #509 | else: |
| #510 | rows = self._query( |
| #511 | "SELECT * FROM events WHERE target IS NULL OR target = ? ORDER BY created_at", |
| #512 | (target,), |
| #513 | ) |
| #514 | return [self._row_to_event(row) for row in rows] |
| #515 | |
| #516 | def insert_capability(self, cap: Capability) -> None: |
| #517 | self._execute( |
| #518 | "INSERT INTO capabilities VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", |
| #519 | ( |
| #520 | cap.cap_id, |
| #521 | cap.subject, |
| #522 | cap.resource, |
| #523 | dumps(cap.rights), |
| #524 | dumps(cap.constraints), |
| #525 | cap.issued_by, |
| #526 | cap.issued_at, |
| #527 | cap.expires_at, |
| #528 | int(cap.delegable), |
| #529 | int(cap.revocable), |
| #530 | int(cap.revoked), |
| #531 | ), |
| #532 | ) |
| #533 | |
| #534 | def update_capability(self, cap: Capability) -> None: |
| #535 | self._execute( |
| #536 | """ |
| #537 | UPDATE capabilities |
| #538 | SET subject = ?, resource = ?, rights_json = ?, constraints_json = ?, |
| #539 | issued_by = ?, issued_at = ?, expires_at = ?, delegable = ?, |
| #540 | revocable = ?, revoked = ? |
| #541 | WHERE cap_id = ? |
| #542 | """, |
| #543 | ( |
| #544 | cap.subject, |
| #545 | cap.resource, |
| #546 | dumps(cap.rights), |
| #547 | dumps(cap.constraints), |
| #548 | cap.issued_by, |
| #549 | cap.issued_at, |
| #550 | cap.expires_at, |
| #551 | int(cap.delegable), |
| #552 | int(cap.revocable), |
| #553 | int(cap.revoked), |
| #554 | cap.cap_id, |
| #555 | ), |
| #556 | ) |
| #557 | |
| #558 | def get_capability(self, cap_id: str) -> Capability | None: |
| #559 | rows = self._query("SELECT * FROM capabilities WHERE cap_id = ?", (cap_id,)) |
| #560 | return self._row_to_capability(rows[0]) if rows else None |
| #561 | |
| #562 | def list_capabilities(self, subject: str | None = None) -> list[Capability]: |
| #563 | if subject is None: |
| #564 | rows = self._query("SELECT * FROM capabilities") |
| #565 | else: |
| #566 | rows = self._query("SELECT * FROM capabilities WHERE subject = ?", (subject,)) |
| #567 | return [self._row_to_capability(row) for row in rows] |
| #568 | |
| #569 | def insert_audit(self, record: AuditRecord) -> None: |
| #570 | self._execute( |
| #571 | "INSERT INTO audit_records VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", |
| #572 | ( |
| #573 | record.record_id, |
| #574 | record.timestamp, |
| #575 | record.actor, |
| #576 | record.action, |
| #577 | record.target, |
| #578 | dumps(record.input_refs), |
| #579 | dumps(record.output_refs), |
| #580 | dumps(record.capability_refs), |
| #581 | dumps(record.decision) if record.decision is not None else None, |
| #582 | record.correlation_id, |
| #583 | record.parent_record_id, |
| #584 | ), |
| #585 | ) |
| #586 | |
| #587 | def list_audit(self, limit: int | None = None) -> list[AuditRecord]: |
| #588 | sql = "SELECT * FROM audit_records ORDER BY timestamp" |
| #589 | params: tuple[Any, ...] = () |
| #590 | if limit is not None: |
| #591 | sql += " LIMIT ?" |
| #592 | params = (limit,) |
| #593 | return [self._row_to_audit(row) for row in self._query(sql, params)] |
| #594 | |
| #595 | def insert_human_request(self, request: HumanRequest) -> None: |
| #596 | self._execute( |
| #597 | "INSERT INTO human_requests VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", |
| #598 | ( |
| #599 | request.request_id, |
| #600 | request.pid, |
| #601 | request.human, |
| #602 | dumps(request.payload), |
| #603 | request.status.value, |
| #604 | dumps(request.decision) if request.decision is not None else None, |
| #605 | int(request.blocking), |
| #606 | request.created_at, |
| #607 | request.updated_at, |
| #608 | ), |
| #609 | ) |
| #610 | |
| #611 | def update_human_request(self, request: HumanRequest) -> None: |
| #612 | self._execute( |
| #613 | """ |
| #614 | UPDATE human_requests |
| #615 | SET pid = ?, human = ?, payload_json = ?, status = ?, decision_json = ?, |
| #616 | blocking = ?, created_at = ?, updated_at = ? |
| #617 | WHERE request_id = ? |
| #618 | """, |
| #619 | ( |
| #620 | request.pid, |
| #621 | request.human, |
| #622 | dumps(request.payload), |
| #623 | request.status.value, |
| #624 | dumps(request.decision) if request.decision is not None else None, |
| #625 | int(request.blocking), |
| #626 | request.created_at, |
| #627 | request.updated_at, |
| #628 | request.request_id, |
| #629 | ), |
| #630 | ) |
| #631 | |
| #632 | def get_human_request(self, request_id: str) -> HumanRequest | None: |
| #633 | rows = self._query("SELECT * FROM human_requests WHERE request_id = ?", (request_id,)) |
| #634 | return self._row_to_human_request(rows[0]) if rows else None |
| #635 | |
| #636 | def list_human_requests(self, pid: str | None = None) -> list[HumanRequest]: |
| #637 | if pid is None: |
| #638 | rows = self._query("SELECT * FROM human_requests ORDER BY created_at") |
| #639 | else: |
| #640 | rows = self._query( |
| #641 | "SELECT * FROM human_requests WHERE pid = ? ORDER BY created_at", |
| #642 | (pid,), |
| #643 | ) |
| #644 | return [self._row_to_human_request(row) for row in rows] |
| #645 | |
| #646 | def insert_llm_call(self, record: LLMCallRecord) -> None: |
| #647 | self._execute( |
| #648 | """ |
| #649 | INSERT INTO llm_calls ( |
| #650 | call_id, pid, image_id, purpose, status, api, model, request_id, response_id, |
| #651 | messages_json, tools_json, request_options_json, response_content, tool_calls_json, |
| #652 | reasoning_json, usage_json, raw_response_json, error, created_at, completed_at |
| #653 | ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
| #654 | """, |
| #655 | ( |
| #656 | record.call_id, |
| #657 | record.pid, |
| #658 | record.image_id, |
| #659 | record.purpose, |
| #660 | record.status, |
| #661 | record.api, |
| #662 | record.model, |
| #663 | record.request_id, |
| #664 | record.response_id, |
| #665 | dumps(record.messages), |
| #666 | dumps(record.tools), |
| #667 | dumps(record.request_options), |
| #668 | record.response_content, |
| #669 | dumps(record.tool_calls), |
| #670 | dumps(record.reasoning) if record.reasoning is not None else None, |
| #671 | dumps(record.usage), |
| #672 | dumps(record.raw_response) if record.raw_response is not None else None, |
| #673 | record.error, |
| #674 | record.created_at, |
| #675 | record.completed_at, |
| #676 | ), |
| #677 | ) |
| #678 | |
| #679 | def list_llm_calls(self, pid: str | None = None, limit: int | None = None) -> list[LLMCallRecord]: |
| #680 | params: list[Any] = [] |
| #681 | sql = "SELECT * FROM llm_calls" |
| #682 | if pid is not None: |
| #683 | sql += " WHERE pid = ?" |
| #684 | params.append(pid) |
| #685 | sql += " ORDER BY created_at, call_id" |
| #686 | if limit is not None: |
| #687 | sql += " LIMIT ?" |
| #688 | params.append(limit) |
| #689 | return [self._row_to_llm_call(row) for row in self._query(sql, params)] |
| #690 | |
| #691 | def insert_process_message(self, message: ProcessMessage) -> None: |
| #692 | self._execute( |
| #693 | """ |
| #694 | INSERT INTO process_messages ( |
| #695 | message_id, sender, recipient_pid, kind, channel, correlation_id, reply_to, |
| #696 | subject, body, payload_json, status, created_at, updated_at, acked_at |
| #697 | ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
| #698 | """, |
| #699 | ( |
| #700 | message.message_id, |
| #701 | message.sender, |
| #702 | message.recipient_pid, |
| #703 | message.kind.value, |
| #704 | message.channel, |
| #705 | message.correlation_id, |
| #706 | message.reply_to, |
| #707 | message.subject, |
| #708 | message.body, |
| #709 | dumps(message.payload), |
| #710 | message.status.value, |
| #711 | message.created_at, |
| #712 | message.updated_at, |
| #713 | message.acked_at, |
| #714 | ), |
| #715 | ) |
| #716 | |
| #717 | def update_process_message(self, message: ProcessMessage) -> None: |
| #718 | self._execute( |
| #719 | """ |
| #720 | UPDATE process_messages |
| #721 | SET sender = ?, recipient_pid = ?, kind = ?, subject = ?, body = ?, |
| #722 | channel = ?, correlation_id = ?, reply_to = ?, payload_json = ?, |
| #723 | status = ?, created_at = ?, updated_at = ?, acked_at = ? |
| #724 | WHERE message_id = ? |
| #725 | """, |
| #726 | ( |
| #727 | message.sender, |
| #728 | message.recipient_pid, |
| #729 | message.kind.value, |
| #730 | message.subject, |
| #731 | message.body, |
| #732 | message.channel, |
| #733 | message.correlation_id, |
| #734 | message.reply_to, |
| #735 | dumps(message.payload), |
| #736 | message.status.value, |
| #737 | message.created_at, |
| #738 | message.updated_at, |
| #739 | message.acked_at, |
| #740 | message.message_id, |
| #741 | ), |
| #742 | ) |
| #743 | |
| #744 | def get_process_message(self, message_id: str) -> ProcessMessage | None: |
| #745 | rows = self._query("SELECT * FROM process_messages WHERE message_id = ?", (message_id,)) |
| #746 | return self._row_to_process_message(rows[0]) if rows else None |
| #747 | |
| #748 | def list_process_messages( |
| #749 | self, |
| #750 | recipient_pid: str | None = None, |
| #751 | *, |
| #752 | status: ProcessMessageStatus | str | None = None, |
| #753 | kind: ProcessMessageKind | str | None = None, |
| #754 | sender: str | None = None, |
| #755 | channel: str | None = None, |
| #756 | correlation_id: str | None = None, |
| #757 | reply_to: str | None = None, |
| #758 | message_ids: list[str] | None = None, |
| #759 | ) -> list[ProcessMessage]: |
| #760 | clauses: list[str] = [] |
| #761 | params: list[Any] = [] |
| #762 | if message_ids is not None and not message_ids: |
| #763 | return [] |
| #764 | if recipient_pid is not None: |
| #765 | clauses.append("recipient_pid = ?") |
| #766 | params.append(recipient_pid) |
| #767 | if status is not None: |
| #768 | selected_status = ProcessMessageStatus(status) |
| #769 | clauses.append("status = ?") |
| #770 | params.append(selected_status.value) |
| #771 | if kind is not None: |
| #772 | selected_kind = ProcessMessageKind(kind) |
| #773 | clauses.append("kind = ?") |
| #774 | params.append(selected_kind.value) |
| #775 | if sender is not None: |
| #776 | clauses.append("sender = ?") |
| #777 | params.append(sender) |
| #778 | if channel is not None: |
| #779 | clauses.append("channel = ?") |
| #780 | params.append(channel) |
| #781 | if correlation_id is not None: |
| #782 | clauses.append("correlation_id = ?") |
| #783 | params.append(correlation_id) |
| #784 | if reply_to is not None: |
| #785 | clauses.append("reply_to = ?") |
| #786 | params.append(reply_to) |
| #787 | if message_ids is not None: |
| #788 | placeholders = ", ".join("?" for _ in message_ids) |
| #789 | clauses.append(f"message_id IN ({placeholders})") |
| #790 | params.extend(message_ids) |
| #791 | where = f" WHERE {' AND '.join(clauses)}" if clauses else "" |
| #792 | rows = self._query(f"SELECT * FROM process_messages{where} ORDER BY created_at, message_id", params) |
| #793 | return [self._row_to_process_message(row) for row in rows] |
| #794 | |
| #795 | def insert_tool(self, handle: ToolHandle, spec: ToolSpec, registered_by: str, created_at: str, ephemeral: bool) -> None: |
| #796 | self._execute( |
| #797 | "INSERT INTO tools VALUES (?, ?, ?, ?, ?, ?, ?)", |
| #798 | ( |
| #799 | handle.tool_id, |
| #800 | handle.name, |
| #801 | dumps(spec), |
| #802 | handle.scope, |
| #803 | registered_by, |
| #804 | created_at, |
| #805 | int(ephemeral), |
| #806 | ), |
| #807 | ) |
| #808 | |
| #809 | def get_tool_spec(self, tool_id: str) -> ToolSpec | None: |
| #810 | rows = self._query("SELECT * FROM tools WHERE tool_id = ?", (tool_id,)) |
| #811 | if not rows: |
| #812 | return None |
| #813 | return self._dict_to_tool_spec(loads(rows[0]["spec_json"])) |
| #814 | |
| #815 | def list_tools(self) -> list[dict[str, Any]]: |
| #816 | return [dict(row) for row in self._query("SELECT * FROM tools ORDER BY created_at")] |
| #817 | |
| #818 | def insert_tool_candidate(self, candidate: ToolCandidate) -> None: |
| #819 | self._execute( |
| #820 | "INSERT INTO tool_candidates VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", |
| #821 | ( |
| #822 | candidate.candidate_id, |
| #823 | candidate.pid, |
| #824 | dumps(candidate.spec), |
| #825 | candidate.source_code, |
| #826 | dumps(candidate.tests), |
| #827 | dumps(candidate.requested_capabilities), |
| #828 | candidate.status.value, |
| #829 | dumps(candidate.validation) if candidate.validation is not None else None, |
| #830 | candidate.created_at, |
| #831 | candidate.updated_at, |
| #832 | ), |
| #833 | ) |
| #834 | |
| #835 | def update_tool_candidate(self, candidate: ToolCandidate) -> None: |
| #836 | self._execute( |
| #837 | """ |
| #838 | UPDATE tool_candidates |
| #839 | SET pid = ?, spec_json = ?, source_code = ?, tests_json = ?, |
| #840 | requested_capabilities_json = ?, status = ?, validation_json = ?, |
| #841 | created_at = ?, updated_at = ? |
| #842 | WHERE candidate_id = ? |
| #843 | """, |
| #844 | ( |
| #845 | candidate.pid, |
| #846 | dumps(candidate.spec), |
| #847 | candidate.source_code, |
| #848 | dumps(candidate.tests), |
| #849 | dumps(candidate.requested_capabilities), |
| #850 | candidate.status.value, |
| #851 | dumps(candidate.validation) if candidate.validation is not None else None, |
| #852 | candidate.created_at, |
| #853 | candidate.updated_at, |
| #854 | candidate.candidate_id, |
| #855 | ), |
| #856 | ) |
| #857 | |
| #858 | def get_tool_candidate(self, candidate_id: str) -> ToolCandidate | None: |
| #859 | rows = self._query("SELECT * FROM tool_candidates WHERE candidate_id = ?", (candidate_id,)) |
| #860 | return self._row_to_tool_candidate(rows[0]) if rows else None |
| #861 | |
| #862 | def insert_checkpoint(self, checkpoint: Checkpoint, snapshot: dict[str, Any]) -> None: |
| #863 | self._execute( |
| #864 | "INSERT INTO checkpoints VALUES (?, ?, ?, ?, ?)", |
| #865 | ( |
| #866 | checkpoint.checkpoint_id, |
| #867 | checkpoint.pid, |
| #868 | checkpoint.reason, |
| #869 | dumps(snapshot), |
| #870 | checkpoint.created_at, |
| #871 | ), |
| #872 | ) |
| #873 | |
| #874 | def get_checkpoint_snapshot(self, checkpoint_id: str) -> tuple[Checkpoint, dict[str, Any]] | None: |
| #875 | rows = self._query("SELECT * FROM checkpoints WHERE checkpoint_id = ?", (checkpoint_id,)) |
| #876 | if not rows: |
| #877 | return None |
| #878 | row = rows[0] |
| #879 | checkpoint = Checkpoint( |
| #880 | checkpoint_id=row["checkpoint_id"], |
| #881 | pid=row["pid"], |
| #882 | reason=row["reason"], |
| #883 | created_at=row["created_at"], |
| #884 | ) |
| #885 | return checkpoint, loads(row["snapshot_json"], {}) |
| #886 | |
| #887 | def snapshot_tables(self) -> dict[str, list[dict[str, Any]]]: |
| #888 | snapshot: dict[str, list[dict[str, Any]]] = {} |
| #889 | with self._lock: |
| #890 | for table in self.SNAPSHOT_TABLES: |
| #891 | snapshot[table] = [self._row_to_dict(row) for row in self.conn.execute(f"SELECT * FROM {table}")] |
| #892 | return snapshot |
| #893 | |
| #894 | def restore_tables(self, snapshot: dict[str, list[dict[str, Any]]]) -> None: |
| #895 | with self._lock: |
| #896 | cur = self.conn.cursor() |
| #897 | for table in self.SNAPSHOT_TABLES: |
| #898 | cur.execute(f"DELETE FROM {table}") |
| #899 | rows = self._normalize_restore_rows(table, snapshot.get(table, [])) |
| #900 | if not rows: |
| #901 | continue |
| #902 | columns = list(rows[0].keys()) |
| #903 | placeholders = ", ".join("?" for _ in columns) |
| #904 | col_sql = ", ".join(columns) |
| #905 | for row in rows: |
| #906 | cur.execute( |
| #907 | f"INSERT INTO {table} ({col_sql}) VALUES ({placeholders})", |
| #908 | tuple(row[column] for column in columns), |
| #909 | ) |
| #910 | self._ensure_object_namespace_schema() |
| #911 | self._ensure_process_schema() |
| #912 | self._ensure_llm_call_schema() |
| #913 | self._ensure_process_message_schema() |
| #914 | self.conn.commit() |
| #915 | |
| #916 | def _ensure_process_schema(self) -> None: |
| #917 | columns = {row["name"] for row in self.conn.execute("PRAGMA table_info(processes)")} |
| #918 | if "working_directory" not in columns: |
| #919 | self.conn.execute("ALTER TABLE processes ADD COLUMN working_directory TEXT NOT NULL DEFAULT '.'") |
| #920 | |
| #921 | def _ensure_llm_call_schema(self) -> None: |
| #922 | self.conn.execute( |
| #923 | """ |
| #924 | CREATE TABLE IF NOT EXISTS llm_calls ( |
| #925 | call_id TEXT PRIMARY KEY, |
| #926 | pid TEXT, |
| #927 | image_id TEXT, |
| #928 | purpose TEXT NOT NULL, |
| #929 | status TEXT NOT NULL, |
| #930 | api TEXT, |
| #931 | model TEXT, |
| #932 | request_id TEXT, |
| #933 | response_id TEXT, |
| #934 | messages_json TEXT NOT NULL, |
| #935 | tools_json TEXT NOT NULL, |
| #936 | request_options_json TEXT NOT NULL, |
| #937 | response_content TEXT NOT NULL, |
| #938 | tool_calls_json TEXT NOT NULL, |
| #939 | reasoning_json TEXT, |
| #940 | usage_json TEXT NOT NULL, |
| #941 | raw_response_json TEXT, |
| #942 | error TEXT, |
| #943 | created_at TEXT NOT NULL, |
| #944 | completed_at TEXT |
| #945 | ) |
| #946 | """ |
| #947 | ) |
| #948 | self.conn.execute("CREATE INDEX IF NOT EXISTS idx_llm_calls_pid_created ON llm_calls(pid, created_at)") |
| #949 | self.conn.execute("CREATE INDEX IF NOT EXISTS idx_llm_calls_request_id ON llm_calls(request_id)") |
| #950 | self.conn.execute("CREATE INDEX IF NOT EXISTS idx_llm_calls_response_id ON llm_calls(response_id)") |
| #951 | |
| #952 | def _ensure_process_message_schema(self) -> None: |
| #953 | columns = {row["name"] for row in self.conn.execute("PRAGMA table_info(process_messages)")} |
| #954 | if "channel" not in columns: |
| #955 | self.conn.execute("ALTER TABLE process_messages ADD COLUMN channel TEXT NOT NULL DEFAULT 'default'") |
| #956 | if "correlation_id" not in columns: |
| #957 | self.conn.execute("ALTER TABLE process_messages ADD COLUMN correlation_id TEXT") |
| #958 | if "reply_to" not in columns: |
| #959 | self.conn.execute("ALTER TABLE process_messages ADD COLUMN reply_to TEXT") |
| #960 | self.conn.execute( |
| #961 | """ |
| #962 | CREATE INDEX IF NOT EXISTS idx_process_messages_recipient_status_kind |
| #963 | ON process_messages(recipient_pid, status, kind, channel, created_at) |
| #964 | """ |
| #965 | ) |
| #966 | self.conn.execute( |
| #967 | """ |
| #968 | CREATE INDEX IF NOT EXISTS idx_process_messages_correlation |
| #969 | ON process_messages(recipient_pid, correlation_id, status, created_at) |
| #970 | """ |
| #971 | ) |
| #972 | |
| #973 | def _ensure_object_namespace_schema(self) -> None: |
| #974 | self.conn.execute( |
| #975 | """ |
| #976 | CREATE TABLE IF NOT EXISTS object_namespaces ( |
| #977 | namespace TEXT PRIMARY KEY, |
| #978 | parent_namespace TEXT, |
| #979 | metadata_json TEXT NOT NULL, |
| #980 | created_by TEXT NOT NULL, |
| #981 | created_at TEXT NOT NULL, |
| #982 | updated_at TEXT NOT NULL |
| #983 | ) |
| #984 | """ |
| #985 | ) |
| #986 | columns = {row["name"] for row in self.conn.execute("PRAGMA table_info(objects)")} |
| #987 | if "namespace" not in columns or self._has_name_only_unique_index(): |
| #988 | self._rebuild_objects_table_with_namespace(columns) |
| #989 | elif "name" not in columns: |
| #990 | self.conn.execute("ALTER TABLE objects ADD COLUMN name TEXT") |
| #991 | self.conn.execute("UPDATE objects SET name = oid WHERE name IS NULL OR name = ''") |
| #992 | self.conn.execute("DROP INDEX IF EXISTS idx_objects_name") |
| #993 | self.conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_objects_namespace_name ON objects(namespace, name)") |
| #994 | now = utc_now() |
| #995 | self.conn.execute( |
| #996 | """ |
| #997 | INSERT OR IGNORE INTO object_namespaces ( |
| #998 | namespace, parent_namespace, metadata_json, created_by, created_at, updated_at |
| #999 | ) VALUES (?, ?, ?, ?, ?, ?) |
| #1000 | """, |
| #1001 | (self.SYSTEM_NAMESPACE, None, dumps({"kind": "root"}), "runtime", now, now), |
| #1002 | ) |
| #1003 | namespaces = { |
| #1004 | str(row["namespace"] or self.SYSTEM_NAMESPACE) |
| #1005 | for row in self.conn.execute("SELECT DISTINCT namespace FROM objects") |
| #1006 | } |
| #1007 | for namespace in sorted(namespaces): |
| #1008 | for current in self._namespace_chain(namespace): |
| #1009 | if current == self.SYSTEM_NAMESPACE: |
| #1010 | continue |
| #1011 | parent = current.rsplit("/", 1)[0] if "/" in current else None |
| #1012 | self.conn.execute( |
| #1013 | """ |
| #1014 | INSERT OR IGNORE INTO object_namespaces ( |
| #1015 | namespace, parent_namespace, metadata_json, created_by, created_at, updated_at |
| #1016 | ) VALUES (?, ?, ?, ?, ?, ?) |
| #1017 | """, |
| #1018 | (current, parent, dumps({"kind": "migration"}), "storage.migration", now, now), |
| #1019 | ) |
| #1020 | |
| #1021 | def _rebuild_objects_table_with_namespace(self, columns: set[str]) -> None: |
| #1022 | self.conn.execute("DROP INDEX IF EXISTS idx_objects_name") |
| #1023 | self.conn.execute("ALTER TABLE objects RENAME TO objects_old") |
| #1024 | self.conn.execute( |
| #1025 | """ |
| #1026 | CREATE TABLE objects ( |
| #1027 | oid TEXT PRIMARY KEY, |
| #1028 | namespace TEXT NOT NULL DEFAULT 'system', |
| #1029 | name TEXT NOT NULL, |
| #1030 | type TEXT NOT NULL, |
| #1031 | schema_version TEXT NOT NULL, |
| #1032 | payload_json TEXT NOT NULL, |
| #1033 | metadata_json TEXT NOT NULL, |
| #1034 | provenance_json TEXT NOT NULL, |
| #1035 | version INTEGER NOT NULL, |
| #1036 | immutable INTEGER NOT NULL, |
| #1037 | created_by TEXT NOT NULL, |
| #1038 | created_at TEXT NOT NULL, |
| #1039 | updated_at TEXT NOT NULL |
| #1040 | ) |
| #1041 | """ |
| #1042 | ) |
| #1043 | namespace_expr = "namespace" if "namespace" in columns else f"'{self.SYSTEM_NAMESPACE}'" |
| #1044 | name_expr = "name" if "name" in columns else "oid" |
| #1045 | self.conn.execute( |
| #1046 | f""" |
| #1047 | INSERT INTO objects ( |
| #1048 | oid, namespace, name, type, schema_version, payload_json, metadata_json, |
| #1049 | provenance_json, version, immutable, created_by, created_at, updated_at |
| #1050 | ) |
| #1051 | SELECT |
| #1052 | oid, COALESCE({namespace_expr}, '{self.SYSTEM_NAMESPACE}'), COALESCE({name_expr}, oid), type, |
| #1053 | schema_version, payload_json, metadata_json, provenance_json, version, |
| #1054 | immutable, created_by, created_at, updated_at |
| #1055 | FROM objects_old |
| #1056 | """ |
| #1057 | ) |
| #1058 | self.conn.execute("DROP TABLE objects_old") |
| #1059 | |
| #1060 | def _has_name_only_unique_index(self) -> bool: |
| #1061 | for index in self.conn.execute("PRAGMA index_list(objects)"): |
| #1062 | if not bool(index["unique"]): |
| #1063 | continue |
| #1064 | columns = [row["name"] for row in self.conn.execute(f"PRAGMA index_info({index['name']})")] |
| #1065 | if columns == ["name"]: |
| #1066 | return True |
| #1067 | return False |
| #1068 | |
| #1069 | def _namespace_chain(self, namespace: str) -> list[str]: |
| #1070 | parts = namespace.split("/") |
| #1071 | return ["/".join(parts[:index]) for index in range(1, len(parts) + 1)] |
| #1072 | |
| #1073 | def _normalize_restore_rows(self, table: str, rows: list[dict[str, Any]]) -> list[dict[str, Any]]: |
| #1074 | if table != "objects": |
| #1075 | if table == "processes": |
| #1076 | return [dict(row, working_directory=row.get("working_directory") or ".") for row in rows] |
| #1077 | if table == "process_messages": |
| #1078 | normalized_messages: list[dict[str, Any]] = [] |
| #1079 | for row in rows: |
| #1080 | item = dict(row) |
| #1081 | item.setdefault("channel", "default") |
| #1082 | item.setdefault("correlation_id", None) |
| #1083 | item.setdefault("reply_to", None) |
| #1084 | normalized_messages.append(item) |
| #1085 | return normalized_messages |
| #1086 | return rows |
| #1087 | normalized: list[dict[str, Any]] = [] |
| #1088 | for row in rows: |
| #1089 | item = dict(row) |
| #1090 | item.setdefault("name", item.get("oid")) |
| #1091 | item.setdefault("namespace", self.SYSTEM_NAMESPACE) |
| #1092 | item["payload_json"] = dumps(self._memory_payload_marker(present=False)) |
| #1093 | normalized.append(item) |
| #1094 | return normalized |
| #1095 | |
| #1096 | def _memory_payload_marker(self, present: bool) -> dict[str, Any]: |
| #1097 | return {"storage": "runtime_memory", "present": present} |
| #1098 | |
| #1099 | def _process_params(self, process: AgentProcess) -> tuple[Any, ...]: |
| #1100 | return ( |
| #1101 | process.pid, |
| #1102 | process.parent_pid, |
| #1103 | process.image_id, |
| #1104 | process.status.value, |
| #1105 | process.goal_oid, |
| #1106 | dumps(process.memory_view) if process.memory_view else None, |
| #1107 | dumps(process.capabilities), |
| #1108 | dumps(process.loaded_skills), |
| #1109 | dumps(process.tool_table), |
| #1110 | process.event_cursor, |
| #1111 | process.checkpoint_head, |
| #1112 | process.status_message, |
| #1113 | dumps(process.resource_budget), |
| #1114 | process.working_directory, |
| #1115 | process.created_at, |
| #1116 | process.updated_at, |
| #1117 | ) |
| #1118 | |
| #1119 | def _row_to_dict(self, row: sqlite3.Row) -> dict[str, Any]: |
| #1120 | return {key: row[key] for key in row.keys()} |
| #1121 | |
| #1122 | def _row_to_object(self, row: sqlite3.Row) -> AgentObject: |
| #1123 | metadata = ObjectMetadata(**loads(row["metadata_json"], {})) |
| #1124 | provenance = Provenance(**loads(row["provenance_json"], {})) |
| #1125 | return AgentObject( |
| #1126 | oid=row["oid"], |
| #1127 | namespace=row["namespace"], |
| #1128 | name=row["name"], |
| #1129 | type=ObjectType(row["type"]), |
| #1130 | schema_version=row["schema_version"], |
| #1131 | payload=self._object_payloads[row["oid"]], |
| #1132 | metadata=metadata, |
| #1133 | provenance=provenance, |
| #1134 | version=row["version"], |
| #1135 | immutable=bool(row["immutable"]), |
| #1136 | created_by=row["created_by"], |
| #1137 | created_at=row["created_at"], |
| #1138 | updated_at=row["updated_at"], |
| #1139 | ) |
| #1140 | |
| #1141 | def _row_to_namespace(self, row: sqlite3.Row) -> ObjectNamespace: |
| #1142 | return ObjectNamespace( |
| #1143 | namespace=row["namespace"], |
| #1144 | parent_namespace=row["parent_namespace"], |
| #1145 | metadata=loads(row["metadata_json"], {}), |
| #1146 | created_by=row["created_by"], |
| #1147 | created_at=row["created_at"], |
| #1148 | updated_at=row["updated_at"], |
| #1149 | ) |
| #1150 | |
| #1151 | def _row_to_link(self, row: sqlite3.Row) -> ObjectLink: |
| #1152 | return ObjectLink( |
| #1153 | link_id=row["id"], |
| #1154 | src=row["src_oid"], |
| #1155 | relation=RelationType(row["relation"]), |
| #1156 | dst=row["dst_oid"], |
| #1157 | metadata=loads(row["metadata_json"], {}), |
| #1158 | created_by=row["created_by"], |
| #1159 | created_at=row["created_at"], |
| #1160 | ) |
| #1161 | |
| #1162 | def _row_to_process(self, row: sqlite3.Row) -> AgentProcess: |
| #1163 | return AgentProcess( |
| #1164 | pid=row["pid"], |
| #1165 | parent_pid=row["parent_pid"], |
| #1166 | image_id=row["image_id"], |
| #1167 | status=ProcessStatus(row["status"]), |
| #1168 | goal_oid=row["goal_oid"], |
| #1169 | memory_view=self._dict_to_view(loads(row["memory_view_json"])) if row["memory_view_json"] else None, |
| #1170 | capabilities=loads(row["capabilities_json"], []), |
| #1171 | loaded_skills=loads(row["loaded_skills_json"], {}), |
| #1172 | tool_table=loads(row["tool_table_json"], {}), |
| #1173 | event_cursor=row["event_cursor"], |
| #1174 | checkpoint_head=row["checkpoint_head"], |
| #1175 | resource_budget=ResourceBudget(**loads(row["resource_budget_json"], {})), |
| #1176 | created_at=row["created_at"], |
| #1177 | updated_at=row["updated_at"], |
| #1178 | working_directory=row["working_directory"] if "working_directory" in row.keys() else ".", |
| #1179 | status_message=row["status_message"], |
| #1180 | ) |
| #1181 | |
| #1182 | def _row_to_event(self, row: sqlite3.Row) -> Event: |
| #1183 | return Event( |
| #1184 | event_id=row["event_id"], |
| #1185 | type=EventType(row["type"]), |
| #1186 | source=row["source"], |
| #1187 | target=row["target"], |
| #1188 | payload=loads(row["payload_json"], {}), |
| #1189 | priority=EventPriority(row["priority"]), |
| #1190 | created_at=row["created_at"], |
| #1191 | correlation_id=row["correlation_id"], |
| #1192 | causality=loads(row["causality_json"], {}), |
| #1193 | ) |
| #1194 | |
| #1195 | def _row_to_capability(self, row: sqlite3.Row) -> Capability: |
| #1196 | return Capability( |
| #1197 | cap_id=row["cap_id"], |
| #1198 | subject=row["subject"], |
| #1199 | resource=row["resource"], |
| #1200 | rights=set(loads(row["rights_json"], [])), |
| #1201 | constraints=loads(row["constraints_json"], {}), |
| #1202 | issued_by=row["issued_by"], |
| #1203 | issued_at=row["issued_at"], |
| #1204 | expires_at=row["expires_at"], |
| #1205 | delegable=bool(row["delegable"]), |
| #1206 | revocable=bool(row["revocable"]), |
| #1207 | revoked=bool(row["revoked"]), |
| #1208 | ) |
| #1209 | |
| #1210 | def _row_to_audit(self, row: sqlite3.Row) -> AuditRecord: |
| #1211 | return AuditRecord( |
| #1212 | record_id=row["record_id"], |
| #1213 | timestamp=row["timestamp"], |
| #1214 | actor=row["actor"], |
| #1215 | action=row["action"], |
| #1216 | target=row["target"], |
| #1217 | input_refs=loads(row["input_refs_json"], []), |
| #1218 | output_refs=loads(row["output_refs_json"], []), |
| #1219 | capability_refs=loads(row["capability_refs_json"], []), |
| #1220 | decision=loads(row["decision_json"]) if row["decision_json"] else None, |
| #1221 | correlation_id=row["correlation_id"], |
| #1222 | parent_record_id=row["parent_record_id"], |
| #1223 | ) |
| #1224 | |
| #1225 | def _row_to_human_request(self, row: sqlite3.Row) -> HumanRequest: |
| #1226 | return HumanRequest( |
| #1227 | request_id=row["request_id"], |
| #1228 | pid=row["pid"], |
| #1229 | human=row["human"], |
| #1230 | payload=loads(row["payload_json"], {}), |
| #1231 | status=HumanRequestStatus(row["status"]), |
| #1232 | decision=loads(row["decision_json"]) if row["decision_json"] else None, |
| #1233 | blocking=bool(row["blocking"]), |
| #1234 | created_at=row["created_at"], |
| #1235 | updated_at=row["updated_at"], |
| #1236 | ) |
| #1237 | |
| #1238 | def _row_to_llm_call(self, row: sqlite3.Row) -> LLMCallRecord: |
| #1239 | return LLMCallRecord( |
| #1240 | call_id=row["call_id"], |
| #1241 | pid=row["pid"], |
| #1242 | image_id=row["image_id"], |
| #1243 | purpose=row["purpose"], |
| #1244 | status=row["status"], |
| #1245 | api=row["api"], |
| #1246 | model=row["model"], |
| #1247 | request_id=row["request_id"], |
| #1248 | response_id=row["response_id"], |
| #1249 | messages=loads(row["messages_json"], []), |
| #1250 | tools=loads(row["tools_json"], []), |
| #1251 | request_options=loads(row["request_options_json"], {}), |
| #1252 | response_content=row["response_content"], |
| #1253 | tool_calls=loads(row["tool_calls_json"], []), |
| #1254 | reasoning=loads(row["reasoning_json"]) if row["reasoning_json"] else None, |
| #1255 | usage=loads(row["usage_json"], {}), |
| #1256 | raw_response=loads(row["raw_response_json"]) if row["raw_response_json"] else None, |
| #1257 | error=row["error"], |
| #1258 | created_at=row["created_at"], |
| #1259 | completed_at=row["completed_at"], |
| #1260 | ) |
| #1261 | |
| #1262 | def _row_to_process_message(self, row: sqlite3.Row) -> ProcessMessage: |
| #1263 | return ProcessMessage( |
| #1264 | message_id=row["message_id"], |
| #1265 | sender=row["sender"], |
| #1266 | recipient_pid=row["recipient_pid"], |
| #1267 | kind=ProcessMessageKind(row["kind"]), |
| #1268 | channel=row["channel"] if "channel" in row.keys() else "default", |
| #1269 | correlation_id=row["correlation_id"] if "correlation_id" in row.keys() else None, |
| #1270 | reply_to=row["reply_to"] if "reply_to" in row.keys() else None, |
| #1271 | subject=row["subject"], |
| #1272 | body=row["body"], |
| #1273 | payload=loads(row["payload_json"], {}), |
| #1274 | status=ProcessMessageStatus(row["status"]), |
| #1275 | created_at=row["created_at"], |
| #1276 | updated_at=row["updated_at"], |
| #1277 | acked_at=row["acked_at"], |
| #1278 | ) |
| #1279 | |
| #1280 | def _row_to_tool_candidate(self, row: sqlite3.Row) -> ToolCandidate: |
| #1281 | return ToolCandidate( |
| #1282 | candidate_id=row["candidate_id"], |
| #1283 | pid=row["pid"], |
| #1284 | spec=self._dict_to_tool_spec(loads(row["spec_json"], {})), |
| #1285 | source_code=row["source_code"], |
| #1286 | tests=loads(row["tests_json"], []), |
| #1287 | requested_capabilities=loads(row["requested_capabilities_json"], []), |
| #1288 | status=ToolCandidateStatus(row["status"]), |
| #1289 | validation=loads(row["validation_json"]) if row["validation_json"] else None, |
| #1290 | created_at=row["created_at"], |
| #1291 | updated_at=row["updated_at"], |
| #1292 | ) |
| #1293 | |
| #1294 | def _dict_to_tool_spec(self, data: dict[str, Any]) -> ToolSpec: |
| #1295 | return ToolSpec( |
| #1296 | name=data["name"], |
| #1297 | description=data.get("description", ""), |
| #1298 | version=data.get("version", "1.0.0"), |
| #1299 | input_schema=data.get("input_schema", {}), |
| #1300 | output_schema=data.get("output_schema", {}), |
| #1301 | policy=data.get("policy", {}), |
| #1302 | tags=data.get("tags", []), |
| #1303 | metadata=data.get("metadata", {}), |
| #1304 | required_capabilities=data.get("required_capabilities", []), |
| #1305 | side_effects=data.get("side_effects", []), |
| #1306 | ) |
| #1307 | |
| #1308 | def _dict_to_view(self, data: dict[str, Any]) -> MemoryView: |
| #1309 | return MemoryView( |
| #1310 | view_id=data["view_id"], |
| #1311 | owner_pid=data["owner_pid"], |
| #1312 | roots=[self._dict_to_handle(item) for item in data.get("roots", [])], |
| #1313 | filters=[self._dict_to_filter(item) for item in data.get("filters", [])], |
| #1314 | rights_policy=data.get("rights_policy", "inherit"), |
| #1315 | created_from=data.get("created_from"), |
| #1316 | mode=ViewMode(data.get("mode", ViewMode.READ_ONLY.value)), |
| #1317 | ) |
| #1318 | |
| #1319 | def _dict_to_filter(self, data: dict[str, Any]) -> ObjectFilter: |
| #1320 | return ObjectFilter( |
| #1321 | type=ObjectType(data["type"]) if data.get("type") else None, |
| #1322 | tags=data.get("tags", []), |
| #1323 | text=data.get("text"), |
| #1324 | ) |
| #1325 | |
| #1326 | def _dict_to_handle(self, data: dict[str, Any]) -> ObjectHandle: |
| #1327 | return ObjectHandle( |
| #1328 | oid=data["oid"], |
| #1329 | rights=set(data.get("rights", [])), |
| #1330 | capability_id=data["capability_id"], |
| #1331 | expires_at=data.get("expires_at"), |
| #1332 | ) |
| #1333 |