agentic-development-framework 0.1.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.
@@ -0,0 +1,231 @@
1
+ """Transactional SQLite state store with snapshots and checkpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import sqlite3
9
+ from collections.abc import Iterator
10
+ from contextlib import contextmanager
11
+ from datetime import UTC, datetime
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from agentic_framework.exceptions import StateError
16
+
17
+ _SCHEMA = """
18
+ CREATE TABLE IF NOT EXISTS project (
19
+ id INTEGER PRIMARY KEY CHECK (id = 1),
20
+ root TEXT NOT NULL,
21
+ initialized_at TEXT NOT NULL
22
+ );
23
+ CREATE TABLE IF NOT EXISTS workflow (
24
+ id INTEGER PRIMARY KEY CHECK (id = 1),
25
+ status TEXT NOT NULL,
26
+ current_stage TEXT NOT NULL,
27
+ updated_at TEXT NOT NULL
28
+ );
29
+ CREATE TABLE IF NOT EXISTS tasks (
30
+ task_id TEXT PRIMARY KEY,
31
+ agent_id TEXT NOT NULL,
32
+ title TEXT NOT NULL,
33
+ description TEXT NOT NULL,
34
+ dependencies_json TEXT NOT NULL DEFAULT '[]',
35
+ expected_outputs_json TEXT NOT NULL DEFAULT '[]',
36
+ acceptance_criteria_json TEXT NOT NULL DEFAULT '[]',
37
+ status TEXT NOT NULL
38
+ );
39
+ CREATE TABLE IF NOT EXISTS approvals (
40
+ stage_id TEXT PRIMARY KEY,
41
+ status TEXT NOT NULL,
42
+ feedback TEXT,
43
+ updated_at TEXT NOT NULL
44
+ );
45
+ CREATE TABLE IF NOT EXISTS checkpoints (
46
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
47
+ reason TEXT NOT NULL,
48
+ state_json TEXT NOT NULL,
49
+ sha256 TEXT NOT NULL,
50
+ created_at TEXT NOT NULL
51
+ );
52
+ CREATE TABLE IF NOT EXISTS events (
53
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
54
+ event_type TEXT NOT NULL,
55
+ payload_json TEXT NOT NULL,
56
+ created_at TEXT NOT NULL
57
+ );
58
+ PRAGMA user_version = 1;
59
+ """
60
+
61
+
62
+ def _now() -> str:
63
+ return datetime.now(UTC).isoformat()
64
+
65
+
66
+ class RuntimeStore:
67
+ """Own the runtime database, inspection snapshots, and JSONL event history."""
68
+
69
+ def __init__(self, project_root: Path) -> None:
70
+ self.project_root = project_root.expanduser().resolve()
71
+ self.runtime = self.project_root / ".agentic"
72
+ self.database_path = self.runtime / "database" / "agentic.db"
73
+ self.state_dir = self.runtime / "state"
74
+ self.events_path = self.runtime / "logs" / "events.jsonl"
75
+
76
+ @contextmanager
77
+ def connect(self) -> Iterator[sqlite3.Connection]:
78
+ connection = sqlite3.connect(self.database_path)
79
+ connection.row_factory = sqlite3.Row
80
+ connection.execute("PRAGMA foreign_keys = ON")
81
+ try:
82
+ yield connection
83
+ connection.commit()
84
+ except Exception:
85
+ connection.rollback()
86
+ raise
87
+ finally:
88
+ connection.close()
89
+
90
+ def initialize(self) -> None:
91
+ self.database_path.parent.mkdir(parents=True, exist_ok=True)
92
+ self.state_dir.mkdir(parents=True, exist_ok=True)
93
+ self.events_path.parent.mkdir(parents=True, exist_ok=True)
94
+ timestamp = _now()
95
+ with self.connect() as connection:
96
+ connection.executescript(_SCHEMA)
97
+ connection.execute(
98
+ "INSERT OR IGNORE INTO project(id, root, initialized_at) VALUES(1, ?, ?)",
99
+ (str(self.project_root), timestamp),
100
+ )
101
+ connection.execute(
102
+ "INSERT OR IGNORE INTO workflow(id, status, current_stage, updated_at) "
103
+ "VALUES(1, 'not_started', 'project_initialization', ?)",
104
+ (timestamp,),
105
+ )
106
+ self.write_snapshots()
107
+ self.events_path.touch(exist_ok=True)
108
+
109
+ def workflow(self) -> dict[str, Any]:
110
+ with self.connect() as connection:
111
+ row = connection.execute(
112
+ "SELECT status, current_stage, updated_at FROM workflow WHERE id = 1"
113
+ ).fetchone()
114
+ if row is None:
115
+ raise StateError("runtime workflow state is missing; run adt init")
116
+ return dict(row)
117
+
118
+ def tasks(self) -> list[dict[str, Any]]:
119
+ with self.connect() as connection:
120
+ rows = connection.execute(
121
+ "SELECT task_id, agent_id, title, status FROM tasks ORDER BY task_id"
122
+ ).fetchall()
123
+ return [dict(row) for row in rows]
124
+
125
+ def approvals(self, status: str | None = None) -> list[dict[str, Any]]:
126
+ query = "SELECT stage_id, status, feedback, updated_at FROM approvals"
127
+ parameters: tuple[str, ...] = ()
128
+ if status:
129
+ query += " WHERE status = ?"
130
+ parameters = (status,)
131
+ query += " ORDER BY stage_id"
132
+ with self.connect() as connection:
133
+ rows = connection.execute(query, parameters).fetchall()
134
+ return [dict(row) for row in rows]
135
+
136
+ def approval_status(self, stage_id: str) -> str | None:
137
+ with self.connect() as connection:
138
+ row = connection.execute(
139
+ "SELECT status FROM approvals WHERE stage_id = ?", (stage_id,)
140
+ ).fetchone()
141
+ return row["status"] if row else None
142
+
143
+ def set_approval(self, stage_id: str, status: str, feedback: str | None = None) -> None:
144
+ with self.connect() as connection:
145
+ connection.execute(
146
+ "INSERT INTO approvals(stage_id, status, feedback, updated_at) VALUES(?, ?, ?, ?) "
147
+ "ON CONFLICT(stage_id) DO UPDATE SET status=excluded.status, "
148
+ "feedback=excluded.feedback, updated_at=excluded.updated_at",
149
+ (stage_id, status, feedback, _now()),
150
+ )
151
+ self.record_event("approval_updated", {"stage_id": stage_id, "status": status})
152
+ self.write_snapshots()
153
+
154
+ def update_workflow(self, status: str, current_stage: str) -> None:
155
+ with self.connect() as connection:
156
+ connection.execute(
157
+ "UPDATE workflow SET status = ?, current_stage = ?, updated_at = ? WHERE id = 1",
158
+ (status, current_stage, _now()),
159
+ )
160
+ self.record_event("workflow_updated", {"status": status, "stage": current_stage})
161
+ self.write_snapshots()
162
+
163
+ def record_event(self, event_type: str, payload: dict[str, Any]) -> None:
164
+ timestamp = _now()
165
+ serialized = json.dumps(payload, sort_keys=True, separators=(",", ":"))
166
+ with self.connect() as connection:
167
+ connection.execute(
168
+ "INSERT INTO events(event_type, payload_json, created_at) VALUES(?, ?, ?)",
169
+ (event_type, serialized, timestamp),
170
+ )
171
+ entry = json.dumps(
172
+ {"timestamp": timestamp, "type": event_type, "payload": payload},
173
+ sort_keys=True,
174
+ separators=(",", ":"),
175
+ )
176
+ with self.events_path.open("a", encoding="utf-8") as event_log:
177
+ event_log.write(entry + "\n")
178
+
179
+ def create_checkpoint(self, reason: str) -> int:
180
+ state = {"workflow": self.workflow(), "tasks": self.tasks()}
181
+ serialized = json.dumps(state, sort_keys=True, separators=(",", ":"))
182
+ checksum = hashlib.sha256(serialized.encode()).hexdigest()
183
+ with self.connect() as connection:
184
+ cursor = connection.execute(
185
+ "INSERT INTO checkpoints(reason, state_json, sha256, created_at) "
186
+ "VALUES(?, ?, ?, ?)",
187
+ (reason, serialized, checksum, _now()),
188
+ )
189
+ checkpoint_id = int(cursor.lastrowid)
190
+ self.record_event("checkpoint_created", {"id": checkpoint_id, "reason": reason})
191
+ return checkpoint_id
192
+
193
+ def validate_latest_checkpoint(self) -> dict[str, Any] | None:
194
+ with self.connect() as connection:
195
+ row = connection.execute(
196
+ "SELECT state_json, sha256 FROM checkpoints ORDER BY id DESC LIMIT 1"
197
+ ).fetchone()
198
+ if row is None:
199
+ return None
200
+ checksum = hashlib.sha256(row["state_json"].encode()).hexdigest()
201
+ if checksum != row["sha256"]:
202
+ raise StateError("latest checkpoint failed integrity validation")
203
+ try:
204
+ value = json.loads(row["state_json"])
205
+ except json.JSONDecodeError as exc:
206
+ raise StateError("latest checkpoint contains invalid state") from exc
207
+ workflow = value.get("workflow") if isinstance(value, dict) else None
208
+ if (
209
+ not isinstance(workflow, dict)
210
+ or not isinstance(workflow.get("status"), str)
211
+ or not isinstance(workflow.get("current_stage"), str)
212
+ ):
213
+ raise StateError("latest checkpoint has an invalid structure")
214
+ return value
215
+
216
+ @staticmethod
217
+ def _atomic_json(path: Path, value: object) -> None:
218
+ temporary = path.with_suffix(path.suffix + ".tmp")
219
+ temporary.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
220
+ os.replace(temporary, path)
221
+
222
+ def write_snapshots(self) -> None:
223
+ self.state_dir.mkdir(parents=True, exist_ok=True)
224
+ workflow = self.workflow()
225
+ tasks = self.tasks()
226
+ self._atomic_json(self.state_dir / "workflow.json", workflow)
227
+ self._atomic_json(self.state_dir / "tasks.json", tasks)
228
+ self._atomic_json(
229
+ self.state_dir / "execution.json",
230
+ {"status": workflow["status"], "updated_at": workflow["updated_at"]},
231
+ )
@@ -0,0 +1 @@
1
+ """Packaged canonical template assets."""
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: agentic-commands
3
+ description: Interpret ADF workflow commands such as /start, /status, /pause, /resume, /continue, /stop, /review, /approve, /revise, /requirements, and /tasks in an initialized project.
4
+ ---
5
+
6
+ # ADF command interpretation
7
+
8
+ When the user sends an ADF slash command, run the matching `adt` command from the project root so
9
+ the operation is persisted outside the conversation. Show the command result without claiming
10
+ work occurred that the CLI did not record.
11
+
12
+ Use these mappings:
13
+
14
+ - `/start`, `/status`, `/pause`, `/resume`, `/continue`, `/stop`, `/review`, `/requirements`, and
15
+ `/tasks` map directly to the same `adt` subcommand.
16
+ - `/approve <stage>` maps to `adt approve <stage>`.
17
+ - `/revise <stage> <feedback>` maps to `adt revise <stage> <feedback>`.
18
+ - `/help` maps to `adt help`.
19
+
20
+ Do not advance through an approval gate without an explicit recorded approval. If the CLI reports
21
+ invalid state or checkpoint corruption, stop and present that error rather than reconstructing or
22
+ overwriting state from conversation memory.