agent-runtime-core 0.1.5__py3-none-any.whl → 0.3.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.
- {agent_runtime → agent_runtime_core}/__init__.py +59 -7
- {agent_runtime → agent_runtime_core}/config.py +1 -1
- {agent_runtime → agent_runtime_core}/events/__init__.py +5 -5
- {agent_runtime → agent_runtime_core}/events/memory.py +1 -1
- {agent_runtime → agent_runtime_core}/events/redis.py +1 -1
- {agent_runtime → agent_runtime_core}/events/sqlite.py +1 -1
- {agent_runtime → agent_runtime_core}/llm/__init__.py +6 -6
- {agent_runtime → agent_runtime_core}/llm/anthropic.py +4 -4
- {agent_runtime → agent_runtime_core}/llm/litellm_client.py +2 -2
- {agent_runtime → agent_runtime_core}/llm/openai.py +4 -4
- agent_runtime_core/persistence/__init__.py +88 -0
- agent_runtime_core/persistence/base.py +332 -0
- agent_runtime_core/persistence/file.py +507 -0
- agent_runtime_core/persistence/manager.py +266 -0
- {agent_runtime → agent_runtime_core}/queue/__init__.py +5 -5
- {agent_runtime → agent_runtime_core}/queue/memory.py +1 -1
- {agent_runtime → agent_runtime_core}/queue/redis.py +1 -1
- {agent_runtime → agent_runtime_core}/queue/sqlite.py +1 -1
- {agent_runtime → agent_runtime_core}/registry.py +1 -1
- {agent_runtime → agent_runtime_core}/runner.py +6 -6
- {agent_runtime → agent_runtime_core}/state/__init__.py +5 -5
- {agent_runtime → agent_runtime_core}/state/memory.py +1 -1
- {agent_runtime → agent_runtime_core}/state/redis.py +1 -1
- {agent_runtime → agent_runtime_core}/state/sqlite.py +1 -1
- {agent_runtime → agent_runtime_core}/testing.py +1 -1
- {agent_runtime → agent_runtime_core}/tracing/__init__.py +4 -4
- {agent_runtime → agent_runtime_core}/tracing/langfuse.py +1 -1
- {agent_runtime → agent_runtime_core}/tracing/noop.py +1 -1
- {agent_runtime_core-0.1.5.dist-info → agent_runtime_core-0.3.0.dist-info}/METADATA +1 -1
- agent_runtime_core-0.3.0.dist-info/RECORD +36 -0
- agent_runtime_core-0.1.5.dist-info/RECORD +0 -32
- {agent_runtime → agent_runtime_core}/events/base.py +0 -0
- {agent_runtime → agent_runtime_core}/interfaces.py +0 -0
- {agent_runtime → agent_runtime_core}/queue/base.py +0 -0
- {agent_runtime → agent_runtime_core}/state/base.py +0 -0
- {agent_runtime_core-0.1.5.dist-info → agent_runtime_core-0.3.0.dist-info}/WHEEL +0 -0
- {agent_runtime_core-0.1.5.dist-info → agent_runtime_core-0.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File-based implementations of persistence stores.
|
|
3
|
+
|
|
4
|
+
These implementations store data in hidden directories:
|
|
5
|
+
- Global: ~/.agent_runtime/
|
|
6
|
+
- Project: ./.agent_runtime/
|
|
7
|
+
|
|
8
|
+
Data is stored as JSON files for easy inspection and debugging.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Optional
|
|
16
|
+
from uuid import UUID
|
|
17
|
+
|
|
18
|
+
from agent_runtime_core.persistence.base import (
|
|
19
|
+
MemoryStore,
|
|
20
|
+
ConversationStore,
|
|
21
|
+
TaskStore,
|
|
22
|
+
PreferencesStore,
|
|
23
|
+
Scope,
|
|
24
|
+
Conversation,
|
|
25
|
+
ConversationMessage,
|
|
26
|
+
ToolCall,
|
|
27
|
+
ToolResult,
|
|
28
|
+
TaskList,
|
|
29
|
+
Task,
|
|
30
|
+
TaskState,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _get_base_path(scope: Scope, project_dir: Optional[Path] = None) -> Path:
|
|
35
|
+
"""Get the base path for a given scope."""
|
|
36
|
+
if scope == Scope.GLOBAL:
|
|
37
|
+
return Path.home() / ".agent_runtime"
|
|
38
|
+
elif scope == Scope.PROJECT:
|
|
39
|
+
base = project_dir or Path.cwd()
|
|
40
|
+
return base / ".agent_runtime"
|
|
41
|
+
else:
|
|
42
|
+
raise ValueError(f"Cannot get path for scope: {scope}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _ensure_dir(path: Path) -> None:
|
|
46
|
+
"""Ensure a directory exists."""
|
|
47
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _JSONEncoder(json.JSONEncoder):
|
|
51
|
+
"""Custom JSON encoder for our data types."""
|
|
52
|
+
|
|
53
|
+
def default(self, obj):
|
|
54
|
+
if isinstance(obj, UUID):
|
|
55
|
+
return str(obj)
|
|
56
|
+
if isinstance(obj, datetime):
|
|
57
|
+
return obj.isoformat()
|
|
58
|
+
if isinstance(obj, TaskState):
|
|
59
|
+
return obj.value
|
|
60
|
+
if hasattr(obj, '__dataclass_fields__'):
|
|
61
|
+
return {k: getattr(obj, k) for k in obj.__dataclass_fields__}
|
|
62
|
+
return super().default(obj)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _json_dumps(obj: Any) -> str:
|
|
66
|
+
"""Serialize object to JSON string."""
|
|
67
|
+
return json.dumps(obj, cls=_JSONEncoder, indent=2)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _parse_datetime(value: Any) -> datetime:
|
|
71
|
+
"""Parse a datetime from string or return as-is."""
|
|
72
|
+
if isinstance(value, datetime):
|
|
73
|
+
return value
|
|
74
|
+
if isinstance(value, str):
|
|
75
|
+
return datetime.fromisoformat(value)
|
|
76
|
+
return value
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _parse_uuid(value: Any) -> UUID:
|
|
80
|
+
"""Parse a UUID from string or return as-is."""
|
|
81
|
+
if isinstance(value, UUID):
|
|
82
|
+
return value
|
|
83
|
+
if isinstance(value, str):
|
|
84
|
+
return UUID(value)
|
|
85
|
+
return value
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class FileMemoryStore(MemoryStore):
|
|
89
|
+
"""
|
|
90
|
+
File-based memory store.
|
|
91
|
+
|
|
92
|
+
Stores key-value pairs in JSON files:
|
|
93
|
+
- {base_path}/memory/{key}.json
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(self, project_dir: Optional[Path] = None):
|
|
97
|
+
self._project_dir = project_dir
|
|
98
|
+
|
|
99
|
+
def _get_memory_path(self, scope: Scope) -> Path:
|
|
100
|
+
return _get_base_path(scope, self._project_dir) / "memory"
|
|
101
|
+
|
|
102
|
+
def _get_key_path(self, key: str, scope: Scope) -> Path:
|
|
103
|
+
# Sanitize key for filesystem
|
|
104
|
+
safe_key = key.replace("/", "_").replace("\\", "_")
|
|
105
|
+
return self._get_memory_path(scope) / f"{safe_key}.json"
|
|
106
|
+
|
|
107
|
+
async def get(self, key: str, scope: Scope = Scope.PROJECT) -> Optional[Any]:
|
|
108
|
+
path = self._get_key_path(key, scope)
|
|
109
|
+
if not path.exists():
|
|
110
|
+
return None
|
|
111
|
+
try:
|
|
112
|
+
with open(path, "r") as f:
|
|
113
|
+
data = json.load(f)
|
|
114
|
+
return data.get("value")
|
|
115
|
+
except (json.JSONDecodeError, IOError):
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
async def set(self, key: str, value: Any, scope: Scope = Scope.PROJECT) -> None:
|
|
119
|
+
path = self._get_key_path(key, scope)
|
|
120
|
+
_ensure_dir(path.parent)
|
|
121
|
+
with open(path, "w") as f:
|
|
122
|
+
f.write(_json_dumps({
|
|
123
|
+
"key": key,
|
|
124
|
+
"value": value,
|
|
125
|
+
"updated_at": datetime.utcnow(),
|
|
126
|
+
}))
|
|
127
|
+
|
|
128
|
+
async def delete(self, key: str, scope: Scope = Scope.PROJECT) -> bool:
|
|
129
|
+
path = self._get_key_path(key, scope)
|
|
130
|
+
if path.exists():
|
|
131
|
+
path.unlink()
|
|
132
|
+
return True
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
async def list_keys(self, scope: Scope = Scope.PROJECT, prefix: Optional[str] = None) -> list[str]:
|
|
136
|
+
memory_path = self._get_memory_path(scope)
|
|
137
|
+
if not memory_path.exists():
|
|
138
|
+
return []
|
|
139
|
+
keys = []
|
|
140
|
+
for file in memory_path.glob("*.json"):
|
|
141
|
+
key = file.stem
|
|
142
|
+
if prefix is None or key.startswith(prefix):
|
|
143
|
+
keys.append(key)
|
|
144
|
+
return sorted(keys)
|
|
145
|
+
|
|
146
|
+
async def clear(self, scope: Scope = Scope.PROJECT) -> None:
|
|
147
|
+
memory_path = self._get_memory_path(scope)
|
|
148
|
+
if memory_path.exists():
|
|
149
|
+
for file in memory_path.glob("*.json"):
|
|
150
|
+
file.unlink()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class FileConversationStore(ConversationStore):
|
|
154
|
+
"""
|
|
155
|
+
File-based conversation store.
|
|
156
|
+
|
|
157
|
+
Stores conversations in JSON files:
|
|
158
|
+
- {base_path}/conversations/{conversation_id}.json
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
def __init__(self, project_dir: Optional[Path] = None):
|
|
162
|
+
self._project_dir = project_dir
|
|
163
|
+
|
|
164
|
+
def _get_conversations_path(self, scope: Scope) -> Path:
|
|
165
|
+
return _get_base_path(scope, self._project_dir) / "conversations"
|
|
166
|
+
|
|
167
|
+
def _get_conversation_path(self, conversation_id: UUID, scope: Scope) -> Path:
|
|
168
|
+
return self._get_conversations_path(scope) / f"{conversation_id}.json"
|
|
169
|
+
|
|
170
|
+
def _serialize_conversation(self, conversation: Conversation) -> dict:
|
|
171
|
+
"""Serialize a conversation to a dict."""
|
|
172
|
+
return {
|
|
173
|
+
"id": str(conversation.id),
|
|
174
|
+
"title": conversation.title,
|
|
175
|
+
"messages": [self._serialize_message(m) for m in conversation.messages],
|
|
176
|
+
"created_at": conversation.created_at.isoformat(),
|
|
177
|
+
"updated_at": conversation.updated_at.isoformat(),
|
|
178
|
+
"metadata": conversation.metadata,
|
|
179
|
+
"agent_key": conversation.agent_key,
|
|
180
|
+
"summary": conversation.summary,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
def _serialize_message(self, message: ConversationMessage) -> dict:
|
|
184
|
+
"""Serialize a message to a dict."""
|
|
185
|
+
return {
|
|
186
|
+
"id": str(message.id),
|
|
187
|
+
"role": message.role,
|
|
188
|
+
"content": message.content,
|
|
189
|
+
"timestamp": message.timestamp.isoformat(),
|
|
190
|
+
"tool_calls": [
|
|
191
|
+
{
|
|
192
|
+
"id": tc.id,
|
|
193
|
+
"name": tc.name,
|
|
194
|
+
"arguments": tc.arguments,
|
|
195
|
+
"timestamp": tc.timestamp.isoformat(),
|
|
196
|
+
}
|
|
197
|
+
for tc in message.tool_calls
|
|
198
|
+
],
|
|
199
|
+
"tool_call_id": message.tool_call_id,
|
|
200
|
+
"model": message.model,
|
|
201
|
+
"usage": message.usage,
|
|
202
|
+
"metadata": message.metadata,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
def _deserialize_conversation(self, data: dict) -> Conversation:
|
|
206
|
+
"""Deserialize a conversation from a dict."""
|
|
207
|
+
return Conversation(
|
|
208
|
+
id=_parse_uuid(data["id"]),
|
|
209
|
+
title=data.get("title"),
|
|
210
|
+
messages=[self._deserialize_message(m) for m in data.get("messages", [])],
|
|
211
|
+
created_at=_parse_datetime(data["created_at"]),
|
|
212
|
+
updated_at=_parse_datetime(data["updated_at"]),
|
|
213
|
+
metadata=data.get("metadata", {}),
|
|
214
|
+
agent_key=data.get("agent_key"),
|
|
215
|
+
summary=data.get("summary"),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def _deserialize_message(self, data: dict) -> ConversationMessage:
|
|
219
|
+
"""Deserialize a message from a dict."""
|
|
220
|
+
return ConversationMessage(
|
|
221
|
+
id=_parse_uuid(data["id"]),
|
|
222
|
+
role=data["role"],
|
|
223
|
+
content=data["content"],
|
|
224
|
+
timestamp=_parse_datetime(data["timestamp"]),
|
|
225
|
+
tool_calls=[
|
|
226
|
+
ToolCall(
|
|
227
|
+
id=tc["id"],
|
|
228
|
+
name=tc["name"],
|
|
229
|
+
arguments=tc["arguments"],
|
|
230
|
+
timestamp=_parse_datetime(tc["timestamp"]),
|
|
231
|
+
)
|
|
232
|
+
for tc in data.get("tool_calls", [])
|
|
233
|
+
],
|
|
234
|
+
tool_call_id=data.get("tool_call_id"),
|
|
235
|
+
model=data.get("model"),
|
|
236
|
+
usage=data.get("usage", {}),
|
|
237
|
+
metadata=data.get("metadata", {}),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
async def save(self, conversation: Conversation, scope: Scope = Scope.PROJECT) -> None:
|
|
241
|
+
path = self._get_conversation_path(conversation.id, scope)
|
|
242
|
+
_ensure_dir(path.parent)
|
|
243
|
+
conversation.updated_at = datetime.utcnow()
|
|
244
|
+
with open(path, "w") as f:
|
|
245
|
+
f.write(_json_dumps(self._serialize_conversation(conversation)))
|
|
246
|
+
|
|
247
|
+
async def get(self, conversation_id: UUID, scope: Scope = Scope.PROJECT) -> Optional[Conversation]:
|
|
248
|
+
path = self._get_conversation_path(conversation_id, scope)
|
|
249
|
+
if not path.exists():
|
|
250
|
+
return None
|
|
251
|
+
try:
|
|
252
|
+
with open(path, "r") as f:
|
|
253
|
+
data = json.load(f)
|
|
254
|
+
return self._deserialize_conversation(data)
|
|
255
|
+
except (json.JSONDecodeError, IOError):
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
async def delete(self, conversation_id: UUID, scope: Scope = Scope.PROJECT) -> bool:
|
|
259
|
+
path = self._get_conversation_path(conversation_id, scope)
|
|
260
|
+
if path.exists():
|
|
261
|
+
path.unlink()
|
|
262
|
+
return True
|
|
263
|
+
return False
|
|
264
|
+
|
|
265
|
+
async def list_conversations(
|
|
266
|
+
self,
|
|
267
|
+
scope: Scope = Scope.PROJECT,
|
|
268
|
+
limit: int = 100,
|
|
269
|
+
offset: int = 0,
|
|
270
|
+
agent_key: Optional[str] = None,
|
|
271
|
+
) -> list[Conversation]:
|
|
272
|
+
conversations_path = self._get_conversations_path(scope)
|
|
273
|
+
if not conversations_path.exists():
|
|
274
|
+
return []
|
|
275
|
+
|
|
276
|
+
conversations = []
|
|
277
|
+
for file in conversations_path.glob("*.json"):
|
|
278
|
+
try:
|
|
279
|
+
with open(file, "r") as f:
|
|
280
|
+
data = json.load(f)
|
|
281
|
+
conv = self._deserialize_conversation(data)
|
|
282
|
+
if agent_key is None or conv.agent_key == agent_key:
|
|
283
|
+
conversations.append(conv)
|
|
284
|
+
except (json.JSONDecodeError, IOError):
|
|
285
|
+
continue
|
|
286
|
+
|
|
287
|
+
# Sort by updated_at descending
|
|
288
|
+
conversations.sort(key=lambda c: c.updated_at, reverse=True)
|
|
289
|
+
return conversations[offset:offset + limit]
|
|
290
|
+
|
|
291
|
+
async def add_message(
|
|
292
|
+
self,
|
|
293
|
+
conversation_id: UUID,
|
|
294
|
+
message: ConversationMessage,
|
|
295
|
+
scope: Scope = Scope.PROJECT,
|
|
296
|
+
) -> None:
|
|
297
|
+
conversation = await self.get(conversation_id, scope)
|
|
298
|
+
if conversation is None:
|
|
299
|
+
raise ValueError(f"Conversation not found: {conversation_id}")
|
|
300
|
+
conversation.messages.append(message)
|
|
301
|
+
await self.save(conversation, scope)
|
|
302
|
+
|
|
303
|
+
async def get_messages(
|
|
304
|
+
self,
|
|
305
|
+
conversation_id: UUID,
|
|
306
|
+
scope: Scope = Scope.PROJECT,
|
|
307
|
+
limit: Optional[int] = None,
|
|
308
|
+
before: Optional[datetime] = None,
|
|
309
|
+
) -> list[ConversationMessage]:
|
|
310
|
+
conversation = await self.get(conversation_id, scope)
|
|
311
|
+
if conversation is None:
|
|
312
|
+
return []
|
|
313
|
+
|
|
314
|
+
messages = conversation.messages
|
|
315
|
+
if before:
|
|
316
|
+
messages = [m for m in messages if m.timestamp < before]
|
|
317
|
+
if limit:
|
|
318
|
+
messages = messages[-limit:]
|
|
319
|
+
return messages
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class FileTaskStore(TaskStore):
|
|
323
|
+
"""
|
|
324
|
+
File-based task store.
|
|
325
|
+
|
|
326
|
+
Stores task lists in JSON files:
|
|
327
|
+
- {base_path}/tasks/{task_list_id}.json
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
def __init__(self, project_dir: Optional[Path] = None):
|
|
331
|
+
self._project_dir = project_dir
|
|
332
|
+
|
|
333
|
+
def _get_tasks_path(self, scope: Scope) -> Path:
|
|
334
|
+
return _get_base_path(scope, self._project_dir) / "tasks"
|
|
335
|
+
|
|
336
|
+
def _get_task_list_path(self, task_list_id: UUID, scope: Scope) -> Path:
|
|
337
|
+
return self._get_tasks_path(scope) / f"{task_list_id}.json"
|
|
338
|
+
|
|
339
|
+
def _serialize_task_list(self, task_list: TaskList) -> dict:
|
|
340
|
+
return {
|
|
341
|
+
"id": str(task_list.id),
|
|
342
|
+
"name": task_list.name,
|
|
343
|
+
"tasks": [
|
|
344
|
+
{
|
|
345
|
+
"id": str(t.id),
|
|
346
|
+
"name": t.name,
|
|
347
|
+
"description": t.description,
|
|
348
|
+
"state": t.state.value,
|
|
349
|
+
"parent_id": str(t.parent_id) if t.parent_id else None,
|
|
350
|
+
"created_at": t.created_at.isoformat(),
|
|
351
|
+
"updated_at": t.updated_at.isoformat(),
|
|
352
|
+
"metadata": t.metadata,
|
|
353
|
+
}
|
|
354
|
+
for t in task_list.tasks
|
|
355
|
+
],
|
|
356
|
+
"created_at": task_list.created_at.isoformat(),
|
|
357
|
+
"updated_at": task_list.updated_at.isoformat(),
|
|
358
|
+
"conversation_id": str(task_list.conversation_id) if task_list.conversation_id else None,
|
|
359
|
+
"run_id": str(task_list.run_id) if task_list.run_id else None,
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
def _deserialize_task_list(self, data: dict) -> TaskList:
|
|
363
|
+
return TaskList(
|
|
364
|
+
id=_parse_uuid(data["id"]),
|
|
365
|
+
name=data["name"],
|
|
366
|
+
tasks=[
|
|
367
|
+
Task(
|
|
368
|
+
id=_parse_uuid(t["id"]),
|
|
369
|
+
name=t["name"],
|
|
370
|
+
description=t.get("description", ""),
|
|
371
|
+
state=TaskState(t["state"]),
|
|
372
|
+
parent_id=_parse_uuid(t["parent_id"]) if t.get("parent_id") else None,
|
|
373
|
+
created_at=_parse_datetime(t["created_at"]),
|
|
374
|
+
updated_at=_parse_datetime(t["updated_at"]),
|
|
375
|
+
metadata=t.get("metadata", {}),
|
|
376
|
+
)
|
|
377
|
+
for t in data.get("tasks", [])
|
|
378
|
+
],
|
|
379
|
+
created_at=_parse_datetime(data["created_at"]),
|
|
380
|
+
updated_at=_parse_datetime(data["updated_at"]),
|
|
381
|
+
conversation_id=_parse_uuid(data["conversation_id"]) if data.get("conversation_id") else None,
|
|
382
|
+
run_id=_parse_uuid(data["run_id"]) if data.get("run_id") else None,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
async def save(self, task_list: TaskList, scope: Scope = Scope.PROJECT) -> None:
|
|
386
|
+
path = self._get_task_list_path(task_list.id, scope)
|
|
387
|
+
_ensure_dir(path.parent)
|
|
388
|
+
task_list.updated_at = datetime.utcnow()
|
|
389
|
+
with open(path, "w") as f:
|
|
390
|
+
f.write(_json_dumps(self._serialize_task_list(task_list)))
|
|
391
|
+
|
|
392
|
+
async def get(self, task_list_id: UUID, scope: Scope = Scope.PROJECT) -> Optional[TaskList]:
|
|
393
|
+
path = self._get_task_list_path(task_list_id, scope)
|
|
394
|
+
if not path.exists():
|
|
395
|
+
return None
|
|
396
|
+
try:
|
|
397
|
+
with open(path, "r") as f:
|
|
398
|
+
data = json.load(f)
|
|
399
|
+
return self._deserialize_task_list(data)
|
|
400
|
+
except (json.JSONDecodeError, IOError):
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
async def delete(self, task_list_id: UUID, scope: Scope = Scope.PROJECT) -> bool:
|
|
404
|
+
path = self._get_task_list_path(task_list_id, scope)
|
|
405
|
+
if path.exists():
|
|
406
|
+
path.unlink()
|
|
407
|
+
return True
|
|
408
|
+
return False
|
|
409
|
+
|
|
410
|
+
async def get_by_conversation(
|
|
411
|
+
self,
|
|
412
|
+
conversation_id: UUID,
|
|
413
|
+
scope: Scope = Scope.PROJECT,
|
|
414
|
+
) -> Optional[TaskList]:
|
|
415
|
+
tasks_path = self._get_tasks_path(scope)
|
|
416
|
+
if not tasks_path.exists():
|
|
417
|
+
return None
|
|
418
|
+
|
|
419
|
+
for file in tasks_path.glob("*.json"):
|
|
420
|
+
try:
|
|
421
|
+
with open(file, "r") as f:
|
|
422
|
+
data = json.load(f)
|
|
423
|
+
if data.get("conversation_id") == str(conversation_id):
|
|
424
|
+
return self._deserialize_task_list(data)
|
|
425
|
+
except (json.JSONDecodeError, IOError):
|
|
426
|
+
continue
|
|
427
|
+
return None
|
|
428
|
+
|
|
429
|
+
async def update_task(
|
|
430
|
+
self,
|
|
431
|
+
task_list_id: UUID,
|
|
432
|
+
task_id: UUID,
|
|
433
|
+
state: Optional[TaskState] = None,
|
|
434
|
+
name: Optional[str] = None,
|
|
435
|
+
description: Optional[str] = None,
|
|
436
|
+
scope: Scope = Scope.PROJECT,
|
|
437
|
+
) -> None:
|
|
438
|
+
task_list = await self.get(task_list_id, scope)
|
|
439
|
+
if task_list is None:
|
|
440
|
+
raise ValueError(f"Task list not found: {task_list_id}")
|
|
441
|
+
|
|
442
|
+
for task in task_list.tasks:
|
|
443
|
+
if task.id == task_id:
|
|
444
|
+
if state is not None:
|
|
445
|
+
task.state = state
|
|
446
|
+
if name is not None:
|
|
447
|
+
task.name = name
|
|
448
|
+
if description is not None:
|
|
449
|
+
task.description = description
|
|
450
|
+
task.updated_at = datetime.utcnow()
|
|
451
|
+
break
|
|
452
|
+
else:
|
|
453
|
+
raise ValueError(f"Task not found: {task_id}")
|
|
454
|
+
|
|
455
|
+
await self.save(task_list, scope)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class FilePreferencesStore(PreferencesStore):
|
|
460
|
+
"""
|
|
461
|
+
File-based preferences store.
|
|
462
|
+
|
|
463
|
+
Stores preferences in a single JSON file:
|
|
464
|
+
- {base_path}/preferences.json
|
|
465
|
+
"""
|
|
466
|
+
|
|
467
|
+
def __init__(self, project_dir: Optional[Path] = None):
|
|
468
|
+
self._project_dir = project_dir
|
|
469
|
+
|
|
470
|
+
def _get_preferences_path(self, scope: Scope) -> Path:
|
|
471
|
+
return _get_base_path(scope, self._project_dir) / "preferences.json"
|
|
472
|
+
|
|
473
|
+
async def _load_preferences(self, scope: Scope) -> dict:
|
|
474
|
+
path = self._get_preferences_path(scope)
|
|
475
|
+
if not path.exists():
|
|
476
|
+
return {}
|
|
477
|
+
try:
|
|
478
|
+
with open(path, "r") as f:
|
|
479
|
+
return json.load(f)
|
|
480
|
+
except (json.JSONDecodeError, IOError):
|
|
481
|
+
return {}
|
|
482
|
+
|
|
483
|
+
async def _save_preferences(self, preferences: dict, scope: Scope) -> None:
|
|
484
|
+
path = self._get_preferences_path(scope)
|
|
485
|
+
_ensure_dir(path.parent)
|
|
486
|
+
with open(path, "w") as f:
|
|
487
|
+
f.write(_json_dumps(preferences))
|
|
488
|
+
|
|
489
|
+
async def get(self, key: str, scope: Scope = Scope.GLOBAL) -> Optional[Any]:
|
|
490
|
+
preferences = await self._load_preferences(scope)
|
|
491
|
+
return preferences.get(key)
|
|
492
|
+
|
|
493
|
+
async def set(self, key: str, value: Any, scope: Scope = Scope.GLOBAL) -> None:
|
|
494
|
+
preferences = await self._load_preferences(scope)
|
|
495
|
+
preferences[key] = value
|
|
496
|
+
await self._save_preferences(preferences, scope)
|
|
497
|
+
|
|
498
|
+
async def delete(self, key: str, scope: Scope = Scope.GLOBAL) -> bool:
|
|
499
|
+
preferences = await self._load_preferences(scope)
|
|
500
|
+
if key in preferences:
|
|
501
|
+
del preferences[key]
|
|
502
|
+
await self._save_preferences(preferences, scope)
|
|
503
|
+
return True
|
|
504
|
+
return False
|
|
505
|
+
|
|
506
|
+
async def get_all(self, scope: Scope = Scope.GLOBAL) -> dict[str, Any]:
|
|
507
|
+
return await self._load_preferences(scope)
|