agentcheckpoint 1.0.0__py3-none-any.whl
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.
- agentcheckpoint/__init__.py +3 -0
- agentcheckpoint/__main__.py +6 -0
- agentcheckpoint/server.py +551 -0
- agentcheckpoint-1.0.0.dist-info/METADATA +320 -0
- agentcheckpoint-1.0.0.dist-info/RECORD +9 -0
- agentcheckpoint-1.0.0.dist-info/WHEEL +5 -0
- agentcheckpoint-1.0.0.dist-info/entry_points.txt +2 -0
- agentcheckpoint-1.0.0.dist-info/licenses/LICENSE +21 -0
- agentcheckpoint-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
"""AgentCheckpoint — Atomic key-value state store for AI agent coordination.
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server backed by SQLite for checkpoint coordination
|
|
4
|
+
between AI agents, cron workers, or any multi-agent workflow. Each key holds a
|
|
5
|
+
JSON value. Reads always return the latest written value — no semantic ambiguity,
|
|
6
|
+
no stale entries, no race conditions.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
agentcheckpoint # stdio MCP server
|
|
10
|
+
CHECKPOINT_DB_PATH=/tmp/state.db agentcheckpoint # custom DB path
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import sqlite3
|
|
18
|
+
|
|
19
|
+
from mcp.server import Server
|
|
20
|
+
from mcp.server.stdio import stdio_server
|
|
21
|
+
from mcp.types import (
|
|
22
|
+
Tool,
|
|
23
|
+
TextContent,
|
|
24
|
+
Resource,
|
|
25
|
+
ResourceTemplate,
|
|
26
|
+
TextResourceContents,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
DB_PATH = os.environ.get(
|
|
30
|
+
"CHECKPOINT_DB_PATH",
|
|
31
|
+
os.path.expanduser("~/.hermes/checkpoints.db"),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
server = Server("agentcheckpoint")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_db() -> sqlite3.Connection:
|
|
38
|
+
"""Get a WAL-mode SQLite connection. Creates the table if missing."""
|
|
39
|
+
conn = sqlite3.connect(DB_PATH, timeout=5)
|
|
40
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
41
|
+
conn.execute("PRAGMA synchronous=NORMAL")
|
|
42
|
+
conn.row_factory = sqlite3.Row
|
|
43
|
+
conn.execute(
|
|
44
|
+
"""CREATE TABLE IF NOT EXISTS checkpoints (
|
|
45
|
+
key TEXT PRIMARY KEY,
|
|
46
|
+
value TEXT NOT NULL,
|
|
47
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
48
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
49
|
+
)"""
|
|
50
|
+
)
|
|
51
|
+
return conn
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _validate_json(value: str) -> str | None:
|
|
55
|
+
"""Return error message if value is not valid JSON, else None."""
|
|
56
|
+
try:
|
|
57
|
+
json.loads(value)
|
|
58
|
+
except json.JSONDecodeError as e:
|
|
59
|
+
return f"Invalid JSON: {e}"
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _upsert(conn: sqlite3.Connection, key: str, value: str) -> int:
|
|
64
|
+
"""Insert or replace a key's value atomically, incrementing version."""
|
|
65
|
+
conn.execute(
|
|
66
|
+
"""INSERT INTO checkpoints (key, value, version, updated_at)
|
|
67
|
+
VALUES (?, ?, 1, datetime('now'))
|
|
68
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
69
|
+
value = excluded.value,
|
|
70
|
+
version = checkpoints.version + 1,
|
|
71
|
+
updated_at = datetime('now')""",
|
|
72
|
+
(key, value),
|
|
73
|
+
)
|
|
74
|
+
conn.commit()
|
|
75
|
+
row = conn.execute(
|
|
76
|
+
"SELECT version FROM checkpoints WHERE key = ?", (key,)
|
|
77
|
+
).fetchone()
|
|
78
|
+
return row["version"]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ── Embedded documentation resources ──────────────────────────────────────
|
|
82
|
+
# Agents can read these like files — no separate download needed.
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
DOCS = {
|
|
86
|
+
"usage": """# AgentCheckpoint — Usage Patterns
|
|
87
|
+
|
|
88
|
+
## Single-Writer Pattern (cron jobs, solo agents)
|
|
89
|
+
|
|
90
|
+
Use `force_set_state` — it always succeeds and always replaces.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
mcp_checkpoint_force_set_state(
|
|
94
|
+
key="workflow:daily-plan",
|
|
95
|
+
value='{"phase": "research", "progress": 0.3}'
|
|
96
|
+
)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Multi-Agent Coordination Pattern
|
|
100
|
+
|
|
101
|
+
Use `get_state` + `set_state` with the version guard (OCC).
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
# 1. READ current state
|
|
105
|
+
state = mcp_checkpoint_get_state(key="workflow:plan-today")
|
|
106
|
+
plan = json.loads(state["value"])
|
|
107
|
+
# plan.current_index = 5
|
|
108
|
+
|
|
109
|
+
# 2. MODIFY — claim the next task
|
|
110
|
+
plan.current_index += 1
|
|
111
|
+
plan.current_task = "analisis"
|
|
112
|
+
|
|
113
|
+
# 3. WRITE with version guard
|
|
114
|
+
result = mcp_checkpoint_set_state(
|
|
115
|
+
key="workflow:plan-today",
|
|
116
|
+
value=json.dumps(plan),
|
|
117
|
+
expected_version=state["version"]
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if result["status"] == "conflict":
|
|
121
|
+
# Another agent changed the state — retry from step 1
|
|
122
|
+
pass
|
|
123
|
+
elif result["status"] == "ok":
|
|
124
|
+
# We own this version now
|
|
125
|
+
pass
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Always pass `expected_version` when multiple agents write to the same key.
|
|
129
|
+
If you get a conflict, re-read and retry — that's the OCC pattern.
|
|
130
|
+
|
|
131
|
+
## Key Naming Convention
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
workflow:<id> — Multi-step workflow state
|
|
135
|
+
project:<name>:<attr> — Project-level attributes
|
|
136
|
+
lock:<resource> — Distributed locks
|
|
137
|
+
plan:<date> — Daily/periodic plans
|
|
138
|
+
checkpoint:<task> — Task checkpoints
|
|
139
|
+
cron:<job-name> — Cron job coordination
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Use colons as separators. Keep keys semantic but short.
|
|
143
|
+
Values must be valid JSON strings.
|
|
144
|
+
|
|
145
|
+
## What NOT to Store
|
|
146
|
+
|
|
147
|
+
| ❌ Don't | ✅ Do |
|
|
148
|
+
|----------|-------|
|
|
149
|
+
| Facts, observations, learnings | agentmemory / vector store |
|
|
150
|
+
| Long text, documents | File system, RAG |
|
|
151
|
+
| Large JSON blobs (>100KB) | Split into multiple keys |
|
|
152
|
+
| Ephemeral task-local state | Keep in memory, write at boundaries |
|
|
153
|
+
|
|
154
|
+
AgentCheckpoint is for STATE COORDINATION, not memory.
|
|
155
|
+
Use both tools together: checkpoint for state, agentmemory for knowledge.
|
|
156
|
+
|
|
157
|
+
## Integration with Cron / Workers
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
# Worker startup: check if work was already done
|
|
161
|
+
state = mcp_checkpoint_get_state(key="checkpoint:nocturno-2026-06-12")
|
|
162
|
+
if state["status"] != "not_found":
|
|
163
|
+
print("Work already completed, skipping")
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
# Worker claims and executes
|
|
167
|
+
mcp_checkpoint_force_set_state(
|
|
168
|
+
key="checkpoint:nocturno-2026-06-12",
|
|
169
|
+
value='{"status": "in-progress", "started_at": "..."}'
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# ... do work ...
|
|
173
|
+
|
|
174
|
+
mcp_checkpoint_force_set_state(
|
|
175
|
+
key="checkpoint:nocturno-2026-06-12",
|
|
176
|
+
value='{"status": "completed", "finished_at": "..."}'
|
|
177
|
+
)
|
|
178
|
+
```
|
|
179
|
+
""",
|
|
180
|
+
"coordination": """# Multi-Agent Coordination
|
|
181
|
+
|
|
182
|
+
AgentCheckpoint solves the "stale state" problem in multi-agent workflows.
|
|
183
|
+
|
|
184
|
+
## Problem
|
|
185
|
+
|
|
186
|
+
Agent A reads state, Agent B writes new state, Agent A writes based on its
|
|
187
|
+
stale read — overwriting B's work. This is the classic read-modify-write race.
|
|
188
|
+
|
|
189
|
+
## Solution: Optimistic Concurrency Control (OCC)
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
# Every agent follows this pattern:
|
|
193
|
+
|
|
194
|
+
def claim_and_work(key, agent_id):
|
|
195
|
+
while True:
|
|
196
|
+
# READ with version
|
|
197
|
+
current = mcp_checkpoint_get_state(key=key)
|
|
198
|
+
if current["status"] == "not_found":
|
|
199
|
+
# First claim
|
|
200
|
+
result = mcp_checkpoint_set_state(
|
|
201
|
+
key=key,
|
|
202
|
+
value=json.dumps({"owner": agent_id, "step": 0}),
|
|
203
|
+
expected_version=0 # create-only
|
|
204
|
+
)
|
|
205
|
+
else:
|
|
206
|
+
plan = json.loads(current["value"])
|
|
207
|
+
plan["owner"] = agent_id
|
|
208
|
+
plan["step"] += 1
|
|
209
|
+
# WRITE with version guard from the read
|
|
210
|
+
result = mcp_checkpoint_set_state(
|
|
211
|
+
key=key,
|
|
212
|
+
value=json.dumps(plan),
|
|
213
|
+
expected_version=current["version"]
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if result["status"] == "ok":
|
|
217
|
+
return result["version"]
|
|
218
|
+
# conflict → re-read and retry
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Each write carries the version observed at read time. If another agent
|
|
222
|
+
changed the key in between, the write fails with "conflict" and you retry.
|
|
223
|
+
|
|
224
|
+
## When to Skip Version Guard
|
|
225
|
+
|
|
226
|
+
Single-writer scenarios (cron jobs, solo agents, sequential workflows):
|
|
227
|
+
use `force_set_state` — no version check, always succeeds.
|
|
228
|
+
|
|
229
|
+
Multi-writer scenarios (multiple agents, parallel workers, distributed
|
|
230
|
+
systems): use `set_state` with `expected_version` — this is OCC.
|
|
231
|
+
""",
|
|
232
|
+
"keys": """# Key Naming Convention
|
|
233
|
+
|
|
234
|
+
## Structure
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
<domain>:<identifier>[:<attribute>]
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
- **domain**: what kind of state (workflow, project, lock, plan, checkpoint, cron)
|
|
241
|
+
- **identifier**: unique name within that domain
|
|
242
|
+
- **attribute** (optional): sub-key for structured states
|
|
243
|
+
|
|
244
|
+
## Examples
|
|
245
|
+
|
|
246
|
+
| Key | Purpose |
|
|
247
|
+
|-----|---------|
|
|
248
|
+
| `workflow:daily-digest` | Multi-step workflow state |
|
|
249
|
+
| `project:agentcheckpoint:build-status` | Build state for a project |
|
|
250
|
+
| `lock:database-migration` | Mutex for a critical operation |
|
|
251
|
+
| `plan:2026-06-12` | Daily execution plan |
|
|
252
|
+
| `checkpoint:nocturno-pilar-1` | Nightly worker checkpoint |
|
|
253
|
+
| `cron:noticias-mañana` | Cron job coordination |
|
|
254
|
+
|
|
255
|
+
## Best Practices
|
|
256
|
+
|
|
257
|
+
- Use colons (`:`) as separators — they're readable and work with LIKE queries
|
|
258
|
+
- Keep keys under 200 chars
|
|
259
|
+
- Values must be valid JSON strings
|
|
260
|
+
- Use `list_state(pattern="project:%")` to find all project keys
|
|
261
|
+
- Group related keys with a common prefix for easy listing
|
|
262
|
+
""",
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@server.list_resources()
|
|
267
|
+
async def handle_list_resources() -> list[Resource]:
|
|
268
|
+
return [
|
|
269
|
+
Resource(
|
|
270
|
+
uri="checkpoint://docs/usage",
|
|
271
|
+
name="Usage Patterns",
|
|
272
|
+
description="How to use agentcheckpoint: single-writer, multi-agent OCC, key naming, anti-patterns",
|
|
273
|
+
mimeType="text/markdown",
|
|
274
|
+
),
|
|
275
|
+
Resource(
|
|
276
|
+
uri="checkpoint://docs/coordination",
|
|
277
|
+
name="Multi-Agent Coordination",
|
|
278
|
+
description="OCC pattern for multi-agent workflows with conflict detection",
|
|
279
|
+
mimeType="text/markdown",
|
|
280
|
+
),
|
|
281
|
+
Resource(
|
|
282
|
+
uri="checkpoint://docs/keys",
|
|
283
|
+
name="Key Naming Convention",
|
|
284
|
+
description="Standard key structure and naming best practices",
|
|
285
|
+
mimeType="text/markdown",
|
|
286
|
+
),
|
|
287
|
+
]
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@server.read_resource()
|
|
291
|
+
async def handle_read_resource(uri: str) -> TextResourceContents:
|
|
292
|
+
parts = uri.split("://", 1)
|
|
293
|
+
if len(parts) != 2 or parts[0] != "checkpoint":
|
|
294
|
+
raise ValueError(f"Unknown resource: {uri}")
|
|
295
|
+
|
|
296
|
+
doc_id = parts[1].removeprefix("docs/")
|
|
297
|
+
content = DOCS.get(doc_id)
|
|
298
|
+
if content is None:
|
|
299
|
+
raise ValueError(f"Unknown documentation section: {doc_id}")
|
|
300
|
+
|
|
301
|
+
return TextResourceContents(
|
|
302
|
+
uri=uri,
|
|
303
|
+
mimeType="text/markdown",
|
|
304
|
+
text=content,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# ── Tool definitions ──────────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@server.list_tools()
|
|
312
|
+
async def handle_list_tools() -> list[Tool]:
|
|
313
|
+
return [
|
|
314
|
+
Tool(
|
|
315
|
+
name="get_state",
|
|
316
|
+
description=(
|
|
317
|
+
"Read the current value of a checkpoint by key. "
|
|
318
|
+
"Returns the latest stored JSON value, its version, and update timestamp. "
|
|
319
|
+
'Returns {"status": "not_found"} if key doesn\'t exist.'
|
|
320
|
+
),
|
|
321
|
+
inputSchema={
|
|
322
|
+
"type": "object",
|
|
323
|
+
"properties": {
|
|
324
|
+
"key": {
|
|
325
|
+
"type": "string",
|
|
326
|
+
"description": "Checkpoint key, e.g. 'workflow:plan-2026-06-12'",
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
"required": ["key"],
|
|
330
|
+
},
|
|
331
|
+
),
|
|
332
|
+
Tool(
|
|
333
|
+
name="set_state",
|
|
334
|
+
description=(
|
|
335
|
+
"Atomically write a checkpoint with optional version guard. "
|
|
336
|
+
"Pass expected_version from a prior get_state call. "
|
|
337
|
+
"expected_version=0 → create-only (fails if key exists). "
|
|
338
|
+
"expected_version=N → update only if stored version matches (conflict-safe). "
|
|
339
|
+
"Omit expected_version or pass -1 → unconditional write. "
|
|
340
|
+
"Use force_set_state for simpler unconditional writes."
|
|
341
|
+
),
|
|
342
|
+
inputSchema={
|
|
343
|
+
"type": "object",
|
|
344
|
+
"properties": {
|
|
345
|
+
"key": {"type": "string", "description": "Checkpoint key"},
|
|
346
|
+
"value": {
|
|
347
|
+
"type": "string",
|
|
348
|
+
"description": "Value to store (must be JSON-encoded string)",
|
|
349
|
+
},
|
|
350
|
+
"expected_version": {
|
|
351
|
+
"type": "integer",
|
|
352
|
+
"description": "Version guard: -1=unconditional, 0=create-only, N=versioned update",
|
|
353
|
+
"default": -1,
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
"required": ["key", "value"],
|
|
357
|
+
},
|
|
358
|
+
),
|
|
359
|
+
Tool(
|
|
360
|
+
name="force_set_state",
|
|
361
|
+
description=(
|
|
362
|
+
"Unconditionally write a checkpoint value. "
|
|
363
|
+
"Always succeeds. Prefer for single-writer workflows. "
|
|
364
|
+
"For concurrent writers, use set_state with expected_version."
|
|
365
|
+
),
|
|
366
|
+
inputSchema={
|
|
367
|
+
"type": "object",
|
|
368
|
+
"properties": {
|
|
369
|
+
"key": {"type": "string", "description": "Checkpoint key"},
|
|
370
|
+
"value": {
|
|
371
|
+
"type": "string",
|
|
372
|
+
"description": "Value to store (must be JSON-encoded string)",
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
"required": ["key", "value"],
|
|
376
|
+
},
|
|
377
|
+
),
|
|
378
|
+
Tool(
|
|
379
|
+
name="list_state",
|
|
380
|
+
description=(
|
|
381
|
+
"List checkpoint keys matching a pattern (SQL LIKE syntax). "
|
|
382
|
+
"Pass '%' or omit for all keys. Returns key, version, and updated_at for each match."
|
|
383
|
+
),
|
|
384
|
+
inputSchema={
|
|
385
|
+
"type": "object",
|
|
386
|
+
"properties": {
|
|
387
|
+
"pattern": {
|
|
388
|
+
"type": "string",
|
|
389
|
+
"description": "SQL LIKE pattern (default '%' = all keys)",
|
|
390
|
+
"default": "%",
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
"required": [],
|
|
394
|
+
},
|
|
395
|
+
),
|
|
396
|
+
Tool(
|
|
397
|
+
name="delete_state",
|
|
398
|
+
description="Remove a checkpoint key and its value permanently.",
|
|
399
|
+
inputSchema={
|
|
400
|
+
"type": "object",
|
|
401
|
+
"properties": {
|
|
402
|
+
"key": {
|
|
403
|
+
"type": "string",
|
|
404
|
+
"description": "Checkpoint key to delete",
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
"required": ["key"],
|
|
408
|
+
},
|
|
409
|
+
),
|
|
410
|
+
]
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
# ── Tool call dispatcher ──────────────────────────────────────────────────
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
@server.call_tool()
|
|
417
|
+
async def handle_call_tool(name: str, arguments: dict) -> list[TextContent]:
|
|
418
|
+
def ok(data: dict) -> list[TextContent]:
|
|
419
|
+
return [TextContent(type="text", text=json.dumps(data))]
|
|
420
|
+
|
|
421
|
+
conn = get_db()
|
|
422
|
+
try:
|
|
423
|
+
if name == "get_state":
|
|
424
|
+
key = arguments["key"]
|
|
425
|
+
row = conn.execute(
|
|
426
|
+
"SELECT value, version, updated_at FROM checkpoints WHERE key = ?",
|
|
427
|
+
(key,),
|
|
428
|
+
).fetchone()
|
|
429
|
+
if row:
|
|
430
|
+
return ok({
|
|
431
|
+
"status": "ok",
|
|
432
|
+
"key": key,
|
|
433
|
+
"value": row["value"],
|
|
434
|
+
"version": row["version"],
|
|
435
|
+
"updated_at": row["updated_at"],
|
|
436
|
+
})
|
|
437
|
+
return ok({"status": "not_found", "key": key})
|
|
438
|
+
|
|
439
|
+
elif name == "set_state":
|
|
440
|
+
key = arguments["key"]
|
|
441
|
+
value = arguments["value"]
|
|
442
|
+
expected_version = arguments.get("expected_version", -1)
|
|
443
|
+
|
|
444
|
+
err = _validate_json(value)
|
|
445
|
+
if err:
|
|
446
|
+
return ok({"status": "error", "message": err})
|
|
447
|
+
|
|
448
|
+
if expected_version == -1:
|
|
449
|
+
version = _upsert(conn, key, value)
|
|
450
|
+
return ok({"status": "ok", "key": key, "version": version})
|
|
451
|
+
|
|
452
|
+
row = conn.execute(
|
|
453
|
+
"SELECT version FROM checkpoints WHERE key = ?", (key,)
|
|
454
|
+
).fetchone()
|
|
455
|
+
|
|
456
|
+
if expected_version == 0:
|
|
457
|
+
if row is not None:
|
|
458
|
+
return ok({
|
|
459
|
+
"status": "conflict",
|
|
460
|
+
"key": key,
|
|
461
|
+
"message": f"Key already exists (version={row['version']}). Use expected_version=N to update, or force_set_state.",
|
|
462
|
+
"actual_version": row["version"],
|
|
463
|
+
})
|
|
464
|
+
conn.execute(
|
|
465
|
+
"INSERT INTO checkpoints (key, value, version, updated_at) VALUES (?, ?, 1, datetime('now'))",
|
|
466
|
+
(key, value),
|
|
467
|
+
)
|
|
468
|
+
conn.commit()
|
|
469
|
+
return ok({"status": "ok", "key": key, "version": 1})
|
|
470
|
+
|
|
471
|
+
if row is None:
|
|
472
|
+
return ok({
|
|
473
|
+
"status": "not_found",
|
|
474
|
+
"key": key,
|
|
475
|
+
"message": "Key does not exist. Use expected_version=0 to create, or force_set_state.",
|
|
476
|
+
})
|
|
477
|
+
if row["version"] != expected_version:
|
|
478
|
+
return ok({
|
|
479
|
+
"status": "conflict",
|
|
480
|
+
"key": key,
|
|
481
|
+
"expected_version": expected_version,
|
|
482
|
+
"actual_version": row["version"],
|
|
483
|
+
"message": f"Version mismatch: expected {expected_version}, actual {row['version']}. Call get_state and retry.",
|
|
484
|
+
})
|
|
485
|
+
conn.execute(
|
|
486
|
+
"UPDATE checkpoints SET value = ?, version = version + 1, updated_at = datetime('now') WHERE key = ? AND version = ?",
|
|
487
|
+
(value, key, expected_version),
|
|
488
|
+
)
|
|
489
|
+
conn.commit()
|
|
490
|
+
new_row = conn.execute(
|
|
491
|
+
"SELECT version FROM checkpoints WHERE key = ?", (key,)
|
|
492
|
+
).fetchone()
|
|
493
|
+
return ok({"status": "ok", "key": key, "version": new_row["version"]})
|
|
494
|
+
|
|
495
|
+
elif name == "force_set_state":
|
|
496
|
+
key = arguments["key"]
|
|
497
|
+
value = arguments["value"]
|
|
498
|
+
|
|
499
|
+
err = _validate_json(value)
|
|
500
|
+
if err:
|
|
501
|
+
return ok({"status": "error", "message": err})
|
|
502
|
+
|
|
503
|
+
version = _upsert(conn, key, value)
|
|
504
|
+
return ok({"status": "ok", "key": key, "version": version})
|
|
505
|
+
|
|
506
|
+
elif name == "list_state":
|
|
507
|
+
pattern = arguments.get("pattern", "%")
|
|
508
|
+
rows = conn.execute(
|
|
509
|
+
"SELECT key, version, updated_at FROM checkpoints WHERE key LIKE ? ORDER BY key",
|
|
510
|
+
(pattern,),
|
|
511
|
+
).fetchall()
|
|
512
|
+
return ok({
|
|
513
|
+
"status": "ok",
|
|
514
|
+
"keys": [
|
|
515
|
+
{"key": r["key"], "version": r["version"], "updated_at": r["updated_at"]}
|
|
516
|
+
for r in rows
|
|
517
|
+
],
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
elif name == "delete_state":
|
|
521
|
+
key = arguments["key"]
|
|
522
|
+
conn.execute("DELETE FROM checkpoints WHERE key = ?", (key,))
|
|
523
|
+
conn.commit()
|
|
524
|
+
return ok({"status": "ok", "key": key, "deleted": True})
|
|
525
|
+
|
|
526
|
+
else:
|
|
527
|
+
return ok({"status": "error", "message": f"Unknown tool: {name}"})
|
|
528
|
+
|
|
529
|
+
finally:
|
|
530
|
+
conn.close()
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
# ── Entry point ───────────────────────────────────────────────────────────
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
async def main_async() -> None:
|
|
537
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
538
|
+
await server.run(read_stream, write_stream, server.create_initialization_options())
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def main() -> None:
|
|
542
|
+
"""Run the agentcheckpoint MCP server over stdio."""
|
|
543
|
+
import asyncio
|
|
544
|
+
try:
|
|
545
|
+
asyncio.run(main_async())
|
|
546
|
+
except KeyboardInterrupt:
|
|
547
|
+
pass
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
if __name__ == "__main__":
|
|
551
|
+
main()
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentcheckpoint
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: MCP checkpoint server: atomic state coordination for AI agents, cron workers, and multi-agent systems
|
|
5
|
+
Author-email: Ernesto Maldonado <erniomaldo@users.noreply.github.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/erniomaldo/agentcheckpoint
|
|
8
|
+
Project-URL: Source, https://github.com/erniomaldo/agentcheckpoint
|
|
9
|
+
Project-URL: Documentation, https://github.com/erniomaldo/agentcheckpoint#readme
|
|
10
|
+
Keywords: mcp,checkpoint,state,agent,coordination,hermes,sqlite
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: mcp>=1.0.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
25
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# AgentCheckpoint
|
|
29
|
+
|
|
30
|
+
**Servidor MCP de almacenamiento atómico clave-valor para coordinación de agentes de IA.**
|
|
31
|
+
|
|
32
|
+
Evita que tus agentes de IA trabajen con estado obsoleto. AgentCheckpoint es un servidor
|
|
33
|
+
MCP (Model Context Protocol) minimalista respaldado por SQLite que brinda a tus agentes
|
|
34
|
+
un almacén de estado compartido y atómico — **siempre devolviendo el último valor escrito**.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install agentcheckpoint
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Luego agrégalo a tu cliente MCP:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"mcpServers": {
|
|
45
|
+
"checkpoint": {
|
|
46
|
+
"command": "agentcheckpoint",
|
|
47
|
+
"timeout": 10
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## El Problema
|
|
54
|
+
|
|
55
|
+
Los almacenes de memoria semántica (bases vectoriales, agentmemory, etc.) están diseñados
|
|
56
|
+
para hechos, no para coordinación de estado. Cuando múltiples agentes leen/escriben
|
|
57
|
+
estado compartido:
|
|
58
|
+
|
|
59
|
+
- `memory.save()` crea **nuevas entradas** en lugar de actualizar — se acumulan decenas
|
|
60
|
+
de versiones obsoletas
|
|
61
|
+
- `memory.recall()` devuelve resultados por **similitud semántica**, no por la última
|
|
62
|
+
marca de tiempo
|
|
63
|
+
- Los agentes leen estado desactualizado y **vuelven a ejecutar trabajo ya completado**
|
|
64
|
+
|
|
65
|
+
## La Solución — 100 líneas de Python
|
|
66
|
+
|
|
67
|
+
AgentCheckpoint es un almacén clave-valor con escrituras atómicas, **diseñado para
|
|
68
|
+
coordinación de estado, no para memoria**.
|
|
69
|
+
|
|
70
|
+
- **Siempre lo último** — `get_state(key)` devuelve el único valor actual
|
|
71
|
+
- **Escrituras atómicas** — `force_set_state(key, value)` siempre actualiza, nunca añade
|
|
72
|
+
- **Seguro contra conflictos** — `set_state(key, value, expected_version)` detecta
|
|
73
|
+
conflictos de lectura-modificación-escritura
|
|
74
|
+
- **Respaldado por SQLite** — cero infraestructura, un solo archivo, modo WAL
|
|
75
|
+
- **Una tabla, cinco herramientas** — lo suficientemente simple para entenderlo en 5 minutos
|
|
76
|
+
|
|
77
|
+
## Herramientas
|
|
78
|
+
|
|
79
|
+
| Herramienta | Descripción |
|
|
80
|
+
|-------------|-------------|
|
|
81
|
+
| `get_state` | Lee el valor actual, versión y timestamp de una clave |
|
|
82
|
+
| `set_state` | Escribe con guardia de versión opcional (detección OCC de conflictos) |
|
|
83
|
+
| `force_set_state` | Escritura atómica incondicional (para flujos de un solo escritor) |
|
|
84
|
+
| `list_state` | Lista claves que coinciden con un patrón SQL LIKE |
|
|
85
|
+
| `delete_state` | Elimina una clave permanentemente |
|
|
86
|
+
|
|
87
|
+
## Inicio Rápido
|
|
88
|
+
|
|
89
|
+
### 1. Instalación
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pip install agentcheckpoint
|
|
93
|
+
# o
|
|
94
|
+
uv pip install agentcheckpoint
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 2. Agregar a tu cliente MCP
|
|
98
|
+
|
|
99
|
+
Copia y pega la configuración para tu plataforma. Después de agregarla,
|
|
100
|
+
**reinicia tu cliente**.
|
|
101
|
+
|
|
102
|
+
#### 🟣 Claude Desktop
|
|
103
|
+
|
|
104
|
+
Edita `claude_desktop_config.json`:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"mcpServers": {
|
|
109
|
+
"checkpoint": {
|
|
110
|
+
"command": "agentcheckpoint",
|
|
111
|
+
"timeout": 10
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
#### 🔵 Claude Code
|
|
118
|
+
|
|
119
|
+
Agrega a `~/.claude/settings.json` bajo `mcpServers`:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"mcpServers": {
|
|
124
|
+
"checkpoint": {
|
|
125
|
+
"command": "agentcheckpoint",
|
|
126
|
+
"timeout": 10
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
O usa la CLI:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
claude mcp add checkpoint -- python -m agentcheckpoint
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
#### 🟢 Cursor
|
|
139
|
+
|
|
140
|
+
Agrega a `~/.cursor/mcp.json`:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"mcpServers": {
|
|
145
|
+
"checkpoint": {
|
|
146
|
+
"command": "agentcheckpoint",
|
|
147
|
+
"timeout": 10
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
#### 🟠 Windsurf
|
|
154
|
+
|
|
155
|
+
Agrega a `~/.codeium/windsurf/mcp_config.json`:
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"mcpServers": {
|
|
160
|
+
"checkpoint": {
|
|
161
|
+
"command": "agentcheckpoint",
|
|
162
|
+
"timeout": 10
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
#### ⚪ Continue.dev
|
|
169
|
+
|
|
170
|
+
Agrega a `~/.continue/config.json` bajo `experimental.mcpServers` (o `mcpServers`
|
|
171
|
+
según la versión):
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"experimental": {
|
|
176
|
+
"mcpServers": {
|
|
177
|
+
"checkpoint": {
|
|
178
|
+
"command": "agentcheckpoint",
|
|
179
|
+
"timeout": 10
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### 🔶 Hermes Agent
|
|
187
|
+
|
|
188
|
+
Agrega a `~/.hermes/config.yaml` bajo `mcp_servers`:
|
|
189
|
+
|
|
190
|
+
```yaml
|
|
191
|
+
mcp_servers:
|
|
192
|
+
checkpoint:
|
|
193
|
+
command: "agentcheckpoint"
|
|
194
|
+
timeout: 10
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Luego ejecuta `/reload-mcp` en sesión, o reinicia el gateway.
|
|
198
|
+
|
|
199
|
+
#### 🐍 Cualquier cliente con soporte uvx
|
|
200
|
+
|
|
201
|
+
Si tu cliente soporta `uvx` (la mayoría modernos lo hacen):
|
|
202
|
+
|
|
203
|
+
```json
|
|
204
|
+
{
|
|
205
|
+
"mcpServers": {
|
|
206
|
+
"checkpoint": {
|
|
207
|
+
"command": "uvx",
|
|
208
|
+
"args": ["agentcheckpoint"],
|
|
209
|
+
"timeout": 10
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### 3. Verificar que funciona
|
|
216
|
+
|
|
217
|
+
Una vez configurado, pregúntale a tu agente:
|
|
218
|
+
|
|
219
|
+
> "¿Qué herramientas tengo del servidor MCP checkpoint?"
|
|
220
|
+
|
|
221
|
+
Deberías ver cinco herramientas: `get_state`, `set_state`, `force_set_state`,
|
|
222
|
+
`list_state` y `delete_state` (prefijadas con `mcp_checkpoint_` en algunos clientes).
|
|
223
|
+
|
|
224
|
+
### 4. Uso básico
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
# Ejemplo: guardar y leer un checkpoint
|
|
228
|
+
mcp_checkpoint_force_set_state(
|
|
229
|
+
key="proyecto:build-status",
|
|
230
|
+
value='{"phase": "testing", "passed": 13, "failed": 2}'
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Más tarde...
|
|
234
|
+
status = mcp_checkpoint_get_state(key="proyecto:build-status")
|
|
235
|
+
# → {value: {...}, version: 1, updated_at: "2026-06-12"}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Ejemplo: Coordinación Multi-Agente
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
# Agente A lee el plan actual
|
|
242
|
+
state = client.call_tool("get_state", {"key": "workflow:plan-hoy"})
|
|
243
|
+
plan = json.loads(state.value)
|
|
244
|
+
# plan.current_index = 5, plan.current_status = "completada"
|
|
245
|
+
|
|
246
|
+
# Agente A toma la siguiente tarea
|
|
247
|
+
plan.current_index += 1 # ahora 6
|
|
248
|
+
plan.current_status = "en-progreso"
|
|
249
|
+
client.call_tool("force_set_state", {
|
|
250
|
+
"key": "workflow:plan-hoy",
|
|
251
|
+
"value": json.dumps(plan)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
# ... El Agente A trabaja en la tarea ...
|
|
255
|
+
|
|
256
|
+
# Agente A la marca como completada
|
|
257
|
+
plan.tasks[6].status = "completada"
|
|
258
|
+
plan.current_status = "completada"
|
|
259
|
+
client.call_tool("force_set_state", {
|
|
260
|
+
"key": "workflow:plan-hoy",
|
|
261
|
+
"value": json.dumps(plan)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
# Agente B (siguiente tick) lee — siempre obtiene lo último
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Configuración
|
|
268
|
+
|
|
269
|
+
| Variable de entorno | Por defecto | Descripción |
|
|
270
|
+
|---------------------|-------------|-------------|
|
|
271
|
+
| `CHECKPOINT_DB_PATH` | `~/.hermes/checkpoints.db` | Ruta de la base de datos SQLite |
|
|
272
|
+
|
|
273
|
+
## Arquitectura
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
┌──────────────┐ MCP stdio ┌──────────────────┐ SQLite WAL ┌──────────┐
|
|
277
|
+
│ Agente / │ ────────────────→ │ agentcheckpoint │ ───────────────→ │ state.db │
|
|
278
|
+
│ Cron Worker │ ←──────────────── │ Servidor MCP │ ←─────────────── │ (1 file) │
|
|
279
|
+
└──────────────┘ └──────────────────┘ └──────────┘
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
El servidor se ejecuta como un subproceso stdio. Las herramientas se autodescubren
|
|
283
|
+
a través del cliente MCP. Sin puertos de red, sin contenedor, sin configuración más
|
|
284
|
+
allá de agregarlo a tu `mcpServers`.
|
|
285
|
+
|
|
286
|
+
## ¿Por qué no usar agentmemory / base vectorial?
|
|
287
|
+
|
|
288
|
+
AgentCheckpoint no reemplaza la memoria — es una herramienta diferente para un
|
|
289
|
+
trabajo diferente:
|
|
290
|
+
|
|
291
|
+
| | AgentCheckpoint | Memoria Vectorial/Semántica |
|
|
292
|
+
|---|---|---|
|
|
293
|
+
| **Propósito** | Coordinación de estado | Hechos, aprendizaje, recuperación |
|
|
294
|
+
| **Escritura** | Siempre reemplaza (UPDATE) | Siempre añade (INSERT) |
|
|
295
|
+
| **Lectura** | Coincidencia exacta de clave (`SELECT WHERE key=?`) | Similitud semántica (`ORDER BY distance`) |
|
|
296
|
+
| **Concurrencia** | Guardia de versión (OCC) | Ninguna |
|
|
297
|
+
| **Persistencia** | SQLite WAL (transaccional) | Varía según el backend |
|
|
298
|
+
|
|
299
|
+
**Úsalos juntos**: AgentCheckpoint para estado compartido (planes, checkpoints,
|
|
300
|
+
bloqueos), memoria vectorial para descubrimientos, observaciones y hechos.
|
|
301
|
+
|
|
302
|
+
## Desarrollo
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
git clone https://github.com/erniomaldo/agentcheckpoint
|
|
306
|
+
cd agentcheckpoint
|
|
307
|
+
pip install -e ".[dev]"
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Licencia
|
|
311
|
+
|
|
312
|
+
MIT
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Idiomas
|
|
317
|
+
|
|
318
|
+
- [English](README.en.md)
|
|
319
|
+
- [Português](README.pt.md)
|
|
320
|
+
- [Français](README.fr.md)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
agentcheckpoint/__init__.py,sha256=RCMN1roPF-EuEh3mBRggASK0ewTJS1TQ5FW-z5ulC2c,105
|
|
2
|
+
agentcheckpoint/__main__.py,sha256=H55AJ9EBkEVXAKifHQDLGrWb-YOnx0SBRImj59E8H_Q,164
|
|
3
|
+
agentcheckpoint/server.py,sha256=keGnxccTC9PMUAdu4A7_nIJGHZS2MqZynSKsa2M1N1g,18609
|
|
4
|
+
agentcheckpoint-1.0.0.dist-info/licenses/LICENSE,sha256=cz33kz5I0OzyStQkeeP2yjpNjET6B0P0XP4JM-fGdYk,1074
|
|
5
|
+
agentcheckpoint-1.0.0.dist-info/METADATA,sha256=xgRzeh3CStU0qDDRfrW2z_lcCSA6SUVyR_e_0iaSqi4,8870
|
|
6
|
+
agentcheckpoint-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
7
|
+
agentcheckpoint-1.0.0.dist-info/entry_points.txt,sha256=bHlJZCDuGvG0CMpbJ2o1lfu73S8bKuC6osS7n0T_7xg,66
|
|
8
|
+
agentcheckpoint-1.0.0.dist-info/top_level.txt,sha256=wDaTvLBsjdUQ-dJVXEtP6Q29YnmLZqVx8Ivt1MOp1HQ,16
|
|
9
|
+
agentcheckpoint-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ernesto Maldonado
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agentcheckpoint
|