Coverage for agentos/checkpoint/sqlite.py: 36%
56 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 12:20 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 12:20 +0800
1"""
2SQLite Checkpointer — 零依赖本地持久化。
4适用场景: 单机部署、开发调试、POC。
5生产多机部署请使用 PostgresCheckpointer。
6"""
8from __future__ import annotations
10import json
11import os
12import sqlite3
14from agentos.checkpoint.base import (
15 Checkpoint,
16 CheckpointBackend,
17 CheckpointMetadata,
18)
20__all__ = ["SQLiteCheckpointer"]
22_SCHEMA = """
23CREATE TABLE IF NOT EXISTS checkpoints (
24 id INTEGER PRIMARY KEY AUTOINCREMENT,
25 thread_id TEXT NOT NULL,
26 checkpoint_id TEXT NOT NULL UNIQUE,
27 parent_id TEXT,
28 step INTEGER NOT NULL,
29 created_at TEXT NOT NULL,
30 tags TEXT NOT NULL DEFAULT '[]',
31 summary TEXT NOT NULL DEFAULT '',
32 messages_blob TEXT NOT NULL DEFAULT '[]',
33 state_blob TEXT NOT NULL DEFAULT '{}',
34 tools_blob TEXT NOT NULL DEFAULT '{}',
35 next_node TEXT NOT NULL DEFAULT ''
36);
38CREATE INDEX IF NOT EXISTS idx_thread_step ON checkpoints(thread_id, step DESC);
39CREATE INDEX IF NOT EXISTS idx_checkpoint_id ON checkpoints(checkpoint_id);
40CREATE INDEX IF NOT EXISTS idx_parent ON checkpoints(parent_id);
41"""
44class SQLiteCheckpointer(CheckpointBackend):
45 """SQLite 后端 Checkpointer。
47 用法:
48 cp = SQLiteCheckpointer(db_path="data/checkpoints.db")
49 await cp.put(checkpoint)
50 latest = await cp.get_latest("thread_abc")
51 """
53 def __init__(self, db_path: str = "checkpoints.db"):
54 self._db_path = db_path
55 os.makedirs(os.path.dirname(db_path) or ".", exist_ok=True)
56 self._init_db()
58 def _init_db(self) -> None:
59 with sqlite3.connect(self._db_path) as conn:
60 conn.executescript(_SCHEMA)
61 conn.commit()
63 def _get_conn(self) -> sqlite3.Connection:
64 conn = sqlite3.connect(self._db_path)
65 conn.row_factory = sqlite3.Row
66 return conn
68 def _row_to_metadata(self, row: sqlite3.Row) -> CheckpointMetadata:
69 return CheckpointMetadata(
70 thread_id=row["thread_id"],
71 checkpoint_id=row["checkpoint_id"],
72 parent_checkpoint_id=row["parent_id"],
73 step=row["step"],
74 created_at=row["created_at"],
75 tags=json.loads(row["tags"]),
76 summary=row["summary"],
77 )
79 def _row_to_checkpoint(self, row: sqlite3.Row) -> Checkpoint:
80 return Checkpoint(
81 metadata=self._row_to_metadata(row),
82 messages=json.loads(row["messages_blob"]),
83 state=json.loads(row["state_blob"]),
84 tools_result=json.loads(row["tools_blob"]),
85 next_node=row["next_node"],
86 )
88 async def put(self, checkpoint: Checkpoint) -> str:
89 meta = checkpoint.metadata
90 with self._get_conn() as conn:
91 conn.execute(
92 """INSERT OR REPLACE INTO checkpoints
93 (thread_id, checkpoint_id, parent_id, step, created_at, tags, summary,
94 messages_blob, state_blob, tools_blob, next_node)
95 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
96 (
97 meta.thread_id,
98 meta.checkpoint_id,
99 meta.parent_checkpoint_id,
100 meta.step,
101 meta.created_at,
102 json.dumps(meta.tags),
103 meta.summary,
104 json.dumps(checkpoint.messages, ensure_ascii=False),
105 json.dumps(checkpoint.state, ensure_ascii=False),
106 json.dumps(checkpoint.tools_result, ensure_ascii=False),
107 checkpoint.next_node,
108 ),
109 )
110 conn.commit()
111 return meta.checkpoint_id
113 async def get(self, checkpoint_id: str) -> Checkpoint | None:
114 with self._get_conn() as conn:
115 row = conn.execute(
116 "SELECT * FROM checkpoints WHERE checkpoint_id = ?", (checkpoint_id,)
117 ).fetchone()
118 return self._row_to_checkpoint(row) if row else None
120 async def get_latest(self, thread_id: str) -> Checkpoint | None:
121 with self._get_conn() as conn:
122 row = conn.execute(
123 "SELECT * FROM checkpoints WHERE thread_id = ? ORDER BY step DESC LIMIT 1",
124 (thread_id,),
125 ).fetchone()
126 return self._row_to_checkpoint(row) if row else None
128 async def list_threads(self, limit: int = 50, offset: int = 0) -> list[CheckpointMetadata]:
129 with self._get_conn() as conn:
130 rows = conn.execute(
131 """SELECT * FROM checkpoints
132 WHERE checkpoint_id IN (
133 SELECT checkpoint_id FROM checkpoints
134 GROUP BY thread_id HAVING step = MAX(step)
135 )
136 ORDER BY created_at DESC LIMIT ? OFFSET ?""",
137 (limit, offset),
138 ).fetchall()
139 return [self._row_to_metadata(r) for r in rows]
141 async def list_checkpoints(
142 self, thread_id: str, limit: int = 100, offset: int = 0
143 ) -> list[CheckpointMetadata]:
144 with self._get_conn() as conn:
145 rows = conn.execute(
146 "SELECT * FROM checkpoints WHERE thread_id = ? ORDER BY step DESC LIMIT ? OFFSET ?",
147 (thread_id, limit, offset),
148 ).fetchall()
149 return [self._row_to_metadata(r) for r in rows]
151 async def delete_thread(self, thread_id: str) -> int:
152 with self._get_conn() as conn:
153 cur = conn.execute("DELETE FROM checkpoints WHERE thread_id = ?", (thread_id,))
154 conn.commit()
155 return cur.rowcount
157 async def delete_before(self, thread_id: str, before_step: int) -> int:
158 with self._get_conn() as conn:
159 cur = conn.execute(
160 "DELETE FROM checkpoints WHERE thread_id = ? AND step < ?",
161 (thread_id, before_step),
162 )
163 conn.commit()
164 return cur.rowcount