Skip to content

Commit 38a5d58

Browse files
committed
Add local storage layer
1 parent f896581 commit 38a5d58

1 file changed

Lines changed: 159 additions & 0 deletions

File tree

agentflow/storage.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
from __future__ import annotations
2+
3+
import json
4+
from datetime import datetime
5+
from pathlib import Path
6+
from typing import Any
7+
8+
from .errors import AgentFlowError
9+
from .models import RunRecord, Task, utc_now
10+
from .paths import AGENTFLOW_DIR, find_project_root
11+
12+
DEFAULT_CONFIG = {
13+
"project_name": "",
14+
"description": "",
15+
"default_agent": "manual",
16+
"commands": {
17+
"test": "",
18+
"lint": "",
19+
"format": "",
20+
},
21+
"context": {
22+
"max_file_chars": 12000,
23+
"include_git_status": True,
24+
"include_tree": True,
25+
},
26+
}
27+
28+
29+
class Store:
30+
def __init__(self, root: str | Path | None = None):
31+
self.root = find_project_root(root)
32+
self.base = self.root / AGENTFLOW_DIR
33+
self.tasks_file = self.base / "tasks.json"
34+
self.runs_file = self.base / "runs.json"
35+
self.config_file = self.base / "config.json"
36+
37+
def exists(self) -> bool:
38+
return self.base.is_dir()
39+
40+
def init(self, project_name: str | None = None, description: str = "") -> None:
41+
self.base.mkdir(parents=True, exist_ok=True)
42+
for name in ["context", "exports", "runs", "tmp"]:
43+
(self.base / name).mkdir(parents=True, exist_ok=True)
44+
if not self.tasks_file.exists():
45+
self._write_json(self.tasks_file, {"tasks": []})
46+
if not self.runs_file.exists():
47+
self._write_json(self.runs_file, {"runs": []})
48+
if not self.config_file.exists():
49+
cfg = dict(DEFAULT_CONFIG)
50+
cfg["project_name"] = project_name or self.root.name
51+
cfg["description"] = description
52+
self._write_json(self.config_file, cfg)
53+
54+
def require(self) -> None:
55+
if not self.exists():
56+
raise AgentFlowError("AgentFlowDesk is not initialized. Run: agentflow init")
57+
58+
def read_config(self) -> dict[str, Any]:
59+
self.require()
60+
cfg = self._read_json(self.config_file)
61+
merged = dict(DEFAULT_CONFIG)
62+
merged.update(cfg)
63+
merged["commands"] = {**DEFAULT_CONFIG["commands"], **cfg.get("commands", {})}
64+
merged["context"] = {**DEFAULT_CONFIG["context"], **cfg.get("context", {})}
65+
return merged
66+
67+
def write_config(self, config: dict[str, Any]) -> None:
68+
self.require()
69+
self._write_json(self.config_file, config)
70+
71+
def list_tasks(self, status: str | None = None) -> list[Task]:
72+
self.require()
73+
data = self._read_json(self.tasks_file)
74+
tasks = [Task.from_dict(item) for item in data.get("tasks", [])]
75+
if status:
76+
tasks = [t for t in tasks if t.status == status]
77+
return tasks
78+
79+
def save_tasks(self, tasks: list[Task]) -> None:
80+
self.require()
81+
self._write_json(self.tasks_file, {"tasks": [t.to_dict() for t in tasks]})
82+
83+
def next_task_id(self) -> str:
84+
today = datetime.now().strftime("%Y%m%d")
85+
prefix = f"AF-{today}-"
86+
max_n = 0
87+
for task in self.list_tasks():
88+
if task.id.startswith(prefix):
89+
try:
90+
max_n = max(max_n, int(task.id.rsplit("-", 1)[-1]))
91+
except ValueError:
92+
pass
93+
return f"{prefix}{max_n + 1:03d}"
94+
95+
def add_task(self, title: str, goal: str, files: list[str] | None = None,
96+
acceptance: list[str] | None = None, notes: str = "",
97+
preferred_agent: str = "") -> Task:
98+
task = Task(
99+
id=self.next_task_id(),
100+
title=title,
101+
goal=goal,
102+
files=files or [],
103+
acceptance=acceptance or [],
104+
notes=notes,
105+
preferred_agent=preferred_agent,
106+
)
107+
tasks = self.list_tasks()
108+
tasks.append(task)
109+
self.save_tasks(tasks)
110+
return task
111+
112+
def get_task(self, task_id: str) -> Task:
113+
for task in self.list_tasks():
114+
if task.id == task_id:
115+
return task
116+
raise AgentFlowError(f"Task not found: {task_id}")
117+
118+
def update_task(self, task: Task) -> None:
119+
tasks = self.list_tasks()
120+
for idx, existing in enumerate(tasks):
121+
if existing.id == task.id:
122+
task.updated_at = utc_now()
123+
tasks[idx] = task
124+
self.save_tasks(tasks)
125+
return
126+
raise AgentFlowError(f"Task not found: {task.id}")
127+
128+
def list_runs(self, task_id: str | None = None) -> list[RunRecord]:
129+
self.require()
130+
data = self._read_json(self.runs_file)
131+
runs = [RunRecord.from_dict(item) for item in data.get("runs", [])]
132+
if task_id:
133+
runs = [r for r in runs if r.task_id == task_id]
134+
return runs
135+
136+
def add_run(self, run: RunRecord) -> None:
137+
runs = self.list_runs()
138+
runs.append(run)
139+
self._write_json(self.runs_file, {"runs": [r.to_dict() for r in runs]})
140+
141+
def update_run(self, run: RunRecord) -> None:
142+
runs = self.list_runs()
143+
for idx, existing in enumerate(runs):
144+
if existing.id == run.id:
145+
runs[idx] = run
146+
self._write_json(self.runs_file, {"runs": [r.to_dict() for r in runs]})
147+
return
148+
raise AgentFlowError(f"Run not found: {run.id}")
149+
150+
@staticmethod
151+
def _read_json(path: Path) -> dict[str, Any]:
152+
if not path.exists():
153+
return {}
154+
return json.loads(path.read_text(encoding="utf-8") or "{}")
155+
156+
@staticmethod
157+
def _write_json(path: Path, data: dict[str, Any]) -> None:
158+
path.parent.mkdir(parents=True, exist_ok=True)
159+
path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")

0 commit comments

Comments
 (0)