runtime-memory 3.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.
- runtime_memory/__init__.py +28 -0
- runtime_memory/claude_code/__init__.py +48 -0
- runtime_memory/claude_code/commands.py +698 -0
- runtime_memory/claude_code/daemon.py +852 -0
- runtime_memory/claude_code/hooks.py +722 -0
- runtime_memory/cli/__init__.py +8 -0
- runtime_memory/cli/main.py +1936 -0
- runtime_memory/core/__init__.py +216 -0
- runtime_memory/core/config.py +473 -0
- runtime_memory/core/embeddings.py +908 -0
- runtime_memory/core/engine.py +1007 -0
- runtime_memory/core/exceptions.py +547 -0
- runtime_memory/core/legacy_env.py +39 -0
- runtime_memory/core/logging.py +160 -0
- runtime_memory/core/models.py +1051 -0
- runtime_memory/core/observability.py +725 -0
- runtime_memory/core/paths.py +30 -0
- runtime_memory/core/resilience.py +511 -0
- runtime_memory/core/retrieval.py +819 -0
- runtime_memory/core/storage.py +1105 -0
- runtime_memory/extraction/__init__.py +36 -0
- runtime_memory/extraction/extractor.py +1143 -0
- runtime_memory/hermes/__init__.py +39 -0
- runtime_memory/hermes/_base.py +154 -0
- runtime_memory/hermes/bridge.py +119 -0
- runtime_memory/hermes/plugin.yaml +13 -0
- runtime_memory/hermes/provider.py +536 -0
- runtime_memory/hermes/tools.py +230 -0
- runtime_memory/hermes/trace.py +177 -0
- runtime_memory/plugin/__init__.py +646 -0
- runtime_memory/sdk/__init__.py +97 -0
- runtime_memory/sdk/client.py +1577 -0
- runtime_memory/server/__init__.py +75 -0
- runtime_memory/server/api.py +1665 -0
- runtime_memory/server/mcp.py +1574 -0
- runtime_memory/server/static/css/styles.css +1110 -0
- runtime_memory/server/static/index.html +264 -0
- runtime_memory/server/static/js/api.js +294 -0
- runtime_memory/server/static/js/app.js +771 -0
- runtime_memory/tasks/__init__.py +114 -0
- runtime_memory/tasks/adapter.py +501 -0
- runtime_memory/tasks/claude_code_adapter.py +495 -0
- runtime_memory/tasks/claude_code_parser.py +339 -0
- runtime_memory/tasks/cli_bridge.py +415 -0
- runtime_memory/tasks/linking.py +397 -0
- runtime_memory/tasks/models.py +520 -0
- runtime_memory/tasks/outcomes.py +320 -0
- runtime_memory/tasks/parser.py +305 -0
- runtime_memory/tasks/unified_adapter.py +661 -0
- runtime_memory-3.0.0.dist-info/METADATA +497 -0
- runtime_memory-3.0.0.dist-info/RECORD +54 -0
- runtime_memory-3.0.0.dist-info/WHEEL +4 -0
- runtime_memory-3.0.0.dist-info/entry_points.txt +6 -0
- runtime_memory-3.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1577 @@
|
|
|
1
|
+
"""Python SDK for Runtime Memory.
|
|
2
|
+
|
|
3
|
+
This module provides a unified client interface for accessing Runtime Memory
|
|
4
|
+
functionality in both local (direct engine access) and remote (REST API) modes.
|
|
5
|
+
|
|
6
|
+
Example usage:
|
|
7
|
+
```python
|
|
8
|
+
from runtime_memory.sdk import MemoryClient
|
|
9
|
+
|
|
10
|
+
# Local mode (direct engine access)
|
|
11
|
+
async with MemoryClient(mode="local") as client:
|
|
12
|
+
memory = await client.add("Use async/await for I/O", category="pattern")
|
|
13
|
+
results = await client.search("async patterns")
|
|
14
|
+
|
|
15
|
+
# Remote mode (REST API)
|
|
16
|
+
async with MemoryClient(mode="remote", base_url="http://localhost:8080") as client:
|
|
17
|
+
memory = await client.add("Use async/await for I/O", category="pattern")
|
|
18
|
+
results = await client.search("async patterns")
|
|
19
|
+
|
|
20
|
+
# Synchronous wrapper
|
|
21
|
+
from runtime_memory.sdk import SyncMemoryClient
|
|
22
|
+
with SyncMemoryClient(mode="local") as client:
|
|
23
|
+
memory = client.add("Use async/await for I/O", category="pattern")
|
|
24
|
+
```
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import asyncio
|
|
30
|
+
import logging
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
from datetime import datetime
|
|
33
|
+
from enum import Enum
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
36
|
+
|
|
37
|
+
import httpx
|
|
38
|
+
|
|
39
|
+
from runtime_memory.core.paths import default_db_path
|
|
40
|
+
from runtime_memory.core.models import (
|
|
41
|
+
ContextResponse,
|
|
42
|
+
Memory,
|
|
43
|
+
MemoryCategory,
|
|
44
|
+
MemoryScope,
|
|
45
|
+
MemorySource,
|
|
46
|
+
Outcome,
|
|
47
|
+
SearchResult,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if TYPE_CHECKING:
|
|
51
|
+
from collections.abc import Sequence
|
|
52
|
+
|
|
53
|
+
from runtime_memory.core.engine import EngineStats, MemoryEngine
|
|
54
|
+
|
|
55
|
+
logger = logging.getLogger(__name__)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# =============================================================================
|
|
59
|
+
# Configuration
|
|
60
|
+
# =============================================================================
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ClientMode(str, Enum):
|
|
64
|
+
"""SDK client operating mode."""
|
|
65
|
+
|
|
66
|
+
LOCAL = "local"
|
|
67
|
+
"""Direct access to MemoryEngine (no network)."""
|
|
68
|
+
|
|
69
|
+
REMOTE = "remote"
|
|
70
|
+
"""Access via REST API."""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class ClientConfig:
|
|
75
|
+
"""Configuration for the Memory Client."""
|
|
76
|
+
|
|
77
|
+
# Mode selection
|
|
78
|
+
mode: ClientMode = ClientMode.LOCAL
|
|
79
|
+
"""Operating mode: local or remote."""
|
|
80
|
+
|
|
81
|
+
# Local mode settings
|
|
82
|
+
db_path: str | Path = field(default_factory=lambda: str(default_db_path()))
|
|
83
|
+
"""Path to SQLite database file (local mode only)."""
|
|
84
|
+
|
|
85
|
+
embedding_provider: str = "local"
|
|
86
|
+
"""Embedding provider type (local mode only)."""
|
|
87
|
+
|
|
88
|
+
# Remote mode settings
|
|
89
|
+
base_url: str = "http://127.0.0.1:8080"
|
|
90
|
+
"""Base URL for REST API (remote mode only)."""
|
|
91
|
+
|
|
92
|
+
api_key: str | None = None
|
|
93
|
+
"""Optional API key for authentication."""
|
|
94
|
+
|
|
95
|
+
# HTTP client settings (remote mode)
|
|
96
|
+
timeout: float = 30.0
|
|
97
|
+
"""Request timeout in seconds."""
|
|
98
|
+
|
|
99
|
+
max_retries: int = 3
|
|
100
|
+
"""Maximum retry attempts for failed requests."""
|
|
101
|
+
|
|
102
|
+
retry_delay: float = 1.0
|
|
103
|
+
"""Initial delay between retries (exponential backoff)."""
|
|
104
|
+
|
|
105
|
+
# Connection pooling
|
|
106
|
+
max_connections: int = 10
|
|
107
|
+
"""Maximum number of concurrent connections."""
|
|
108
|
+
|
|
109
|
+
def __post_init__(self) -> None:
|
|
110
|
+
"""Normalize configuration values."""
|
|
111
|
+
if isinstance(self.mode, str):
|
|
112
|
+
self.mode = ClientMode(self.mode)
|
|
113
|
+
if isinstance(self.db_path, str):
|
|
114
|
+
self.db_path = Path(self.db_path).expanduser()
|
|
115
|
+
# Ensure base_url doesn't have trailing slash
|
|
116
|
+
self.base_url = self.base_url.rstrip("/")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# =============================================================================
|
|
120
|
+
# Exceptions
|
|
121
|
+
# =============================================================================
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class SDKError(Exception):
|
|
125
|
+
"""Base exception for SDK errors."""
|
|
126
|
+
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class ConnectionError(SDKError):
|
|
131
|
+
"""Error connecting to remote server."""
|
|
132
|
+
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class AuthenticationError(SDKError):
|
|
137
|
+
"""Authentication failed."""
|
|
138
|
+
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class NotFoundError(SDKError):
|
|
143
|
+
"""Resource not found."""
|
|
144
|
+
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class ValidationError(SDKError):
|
|
149
|
+
"""Request validation failed."""
|
|
150
|
+
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class RateLimitError(SDKError):
|
|
155
|
+
"""Rate limit exceeded."""
|
|
156
|
+
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# =============================================================================
|
|
161
|
+
# Response Types (for remote mode)
|
|
162
|
+
# =============================================================================
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass
|
|
166
|
+
class StatsDict:
|
|
167
|
+
"""Statistics response from the API."""
|
|
168
|
+
|
|
169
|
+
total_memories: int
|
|
170
|
+
active_memories: int
|
|
171
|
+
archived_memories: int
|
|
172
|
+
by_category: dict[str, int]
|
|
173
|
+
by_scope: dict[str, int]
|
|
174
|
+
by_source: dict[str, int]
|
|
175
|
+
avg_outcome_score: float
|
|
176
|
+
total_uses: int
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def from_dict(cls, data: dict[str, Any]) -> StatsDict:
|
|
180
|
+
"""Create from API response."""
|
|
181
|
+
return cls(
|
|
182
|
+
total_memories=data.get("total_memories", 0),
|
|
183
|
+
active_memories=data.get("active_memories", 0),
|
|
184
|
+
archived_memories=data.get("archived_memories", 0),
|
|
185
|
+
by_category=data.get("by_category", {}),
|
|
186
|
+
by_scope=data.get("by_scope", {}),
|
|
187
|
+
by_source=data.get("by_source", {}),
|
|
188
|
+
avg_outcome_score=data.get("avg_outcome_score", 0.0),
|
|
189
|
+
total_uses=data.get("total_uses", 0),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# =============================================================================
|
|
194
|
+
# Memory Client (Async)
|
|
195
|
+
# =============================================================================
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class MemoryClient:
|
|
199
|
+
"""Async client for Runtime Memory.
|
|
200
|
+
|
|
201
|
+
Provides a unified interface for memory operations in both local
|
|
202
|
+
(direct engine access) and remote (REST API) modes.
|
|
203
|
+
|
|
204
|
+
Example:
|
|
205
|
+
```python
|
|
206
|
+
# Local mode
|
|
207
|
+
async with MemoryClient(mode="local") as client:
|
|
208
|
+
memory = await client.add(
|
|
209
|
+
content="Use async/await for I/O",
|
|
210
|
+
category="pattern",
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Remote mode
|
|
214
|
+
async with MemoryClient(
|
|
215
|
+
mode="remote",
|
|
216
|
+
base_url="http://localhost:8080",
|
|
217
|
+
api_key="secret",
|
|
218
|
+
) as client:
|
|
219
|
+
results = await client.search("async patterns")
|
|
220
|
+
```
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
def __init__(
|
|
224
|
+
self,
|
|
225
|
+
config: ClientConfig | None = None,
|
|
226
|
+
*,
|
|
227
|
+
mode: str | ClientMode = ClientMode.LOCAL,
|
|
228
|
+
db_path: str | Path | None = None,
|
|
229
|
+
embedding_provider: str = "local",
|
|
230
|
+
base_url: str | None = None,
|
|
231
|
+
api_key: str | None = None,
|
|
232
|
+
timeout: float = 30.0,
|
|
233
|
+
max_retries: int = 3,
|
|
234
|
+
) -> None:
|
|
235
|
+
"""Initialize the Memory Client.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
config: Full configuration object (overrides other params).
|
|
239
|
+
mode: Operating mode: "local" or "remote".
|
|
240
|
+
db_path: Database path for local mode.
|
|
241
|
+
embedding_provider: Embedding provider for local mode.
|
|
242
|
+
base_url: API base URL for remote mode.
|
|
243
|
+
api_key: API key for authentication.
|
|
244
|
+
timeout: Request timeout in seconds.
|
|
245
|
+
max_retries: Maximum retry attempts.
|
|
246
|
+
"""
|
|
247
|
+
if config:
|
|
248
|
+
self.config = config
|
|
249
|
+
else:
|
|
250
|
+
self.config = ClientConfig(
|
|
251
|
+
mode=ClientMode(mode) if isinstance(mode, str) else mode,
|
|
252
|
+
db_path=db_path or str(default_db_path()),
|
|
253
|
+
embedding_provider=embedding_provider,
|
|
254
|
+
base_url=base_url or "http://127.0.0.1:8080",
|
|
255
|
+
api_key=api_key,
|
|
256
|
+
timeout=timeout,
|
|
257
|
+
max_retries=max_retries,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Internal state
|
|
261
|
+
self._engine: MemoryEngine | None = None
|
|
262
|
+
self._http_client: httpx.AsyncClient | None = None
|
|
263
|
+
self._initialized = False
|
|
264
|
+
|
|
265
|
+
async def initialize(self) -> None:
|
|
266
|
+
"""Initialize the client.
|
|
267
|
+
|
|
268
|
+
For local mode, creates and initializes the MemoryEngine.
|
|
269
|
+
For remote mode, creates the HTTP client with connection pooling.
|
|
270
|
+
"""
|
|
271
|
+
if self._initialized:
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
if self.config.mode == ClientMode.LOCAL:
|
|
275
|
+
await self._init_local()
|
|
276
|
+
else:
|
|
277
|
+
await self._init_remote()
|
|
278
|
+
|
|
279
|
+
self._initialized = True
|
|
280
|
+
logger.info(f"MemoryClient initialized in {self.config.mode.value} mode")
|
|
281
|
+
|
|
282
|
+
async def close(self) -> None:
|
|
283
|
+
"""Close the client and release resources."""
|
|
284
|
+
if self.config.mode == ClientMode.LOCAL and self._engine:
|
|
285
|
+
await self._engine.close()
|
|
286
|
+
self._engine = None
|
|
287
|
+
elif self.config.mode == ClientMode.REMOTE and self._http_client:
|
|
288
|
+
await self._http_client.aclose()
|
|
289
|
+
self._http_client = None
|
|
290
|
+
|
|
291
|
+
self._initialized = False
|
|
292
|
+
logger.info("MemoryClient closed")
|
|
293
|
+
|
|
294
|
+
async def __aenter__(self) -> MemoryClient:
|
|
295
|
+
"""Async context manager entry."""
|
|
296
|
+
await self.initialize()
|
|
297
|
+
return self
|
|
298
|
+
|
|
299
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
300
|
+
"""Async context manager exit."""
|
|
301
|
+
await self.close()
|
|
302
|
+
|
|
303
|
+
def _ensure_initialized(self) -> None:
|
|
304
|
+
"""Ensure the client is initialized."""
|
|
305
|
+
if not self._initialized:
|
|
306
|
+
raise SDKError(
|
|
307
|
+
"Client not initialized. Call initialize() first or use async context manager."
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# =========================================================================
|
|
311
|
+
# Local Mode Initialization
|
|
312
|
+
# =========================================================================
|
|
313
|
+
|
|
314
|
+
async def _init_local(self) -> None:
|
|
315
|
+
"""Initialize local mode with MemoryEngine."""
|
|
316
|
+
from runtime_memory.core.engine import EngineConfig, MemoryEngine
|
|
317
|
+
|
|
318
|
+
engine_config = EngineConfig(
|
|
319
|
+
db_path=self.config.db_path,
|
|
320
|
+
embedding_provider=self.config.embedding_provider,
|
|
321
|
+
)
|
|
322
|
+
self._engine = MemoryEngine(config=engine_config)
|
|
323
|
+
await self._engine.initialize()
|
|
324
|
+
|
|
325
|
+
# =========================================================================
|
|
326
|
+
# Remote Mode Initialization and Helpers
|
|
327
|
+
# =========================================================================
|
|
328
|
+
|
|
329
|
+
async def _init_remote(self) -> None:
|
|
330
|
+
"""Initialize remote mode with HTTP client."""
|
|
331
|
+
headers = {}
|
|
332
|
+
if self.config.api_key:
|
|
333
|
+
headers["X-API-Key"] = self.config.api_key
|
|
334
|
+
|
|
335
|
+
limits = httpx.Limits(
|
|
336
|
+
max_connections=self.config.max_connections,
|
|
337
|
+
max_keepalive_connections=self.config.max_connections,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
self._http_client = httpx.AsyncClient(
|
|
341
|
+
base_url=self.config.base_url,
|
|
342
|
+
headers=headers,
|
|
343
|
+
timeout=httpx.Timeout(self.config.timeout),
|
|
344
|
+
limits=limits,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Verify connection with health check
|
|
348
|
+
try:
|
|
349
|
+
response = await self._http_client.get("/health")
|
|
350
|
+
response.raise_for_status()
|
|
351
|
+
except httpx.ConnectError as e:
|
|
352
|
+
await self._http_client.aclose()
|
|
353
|
+
self._http_client = None
|
|
354
|
+
raise ConnectionError(f"Failed to connect to {self.config.base_url}: {e}") from e
|
|
355
|
+
|
|
356
|
+
async def _request(
|
|
357
|
+
self,
|
|
358
|
+
method: str,
|
|
359
|
+
path: str,
|
|
360
|
+
*,
|
|
361
|
+
json: dict[str, Any] | None = None,
|
|
362
|
+
params: dict[str, Any] | None = None,
|
|
363
|
+
) -> dict[str, Any]:
|
|
364
|
+
"""Make an HTTP request with retry logic.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
method: HTTP method (GET, POST, etc.).
|
|
368
|
+
path: API path.
|
|
369
|
+
json: JSON body for POST/PATCH requests.
|
|
370
|
+
params: Query parameters.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
Response JSON as dictionary.
|
|
374
|
+
|
|
375
|
+
Raises:
|
|
376
|
+
Various SDKError subclasses for different error conditions.
|
|
377
|
+
"""
|
|
378
|
+
assert self._http_client is not None
|
|
379
|
+
|
|
380
|
+
last_error: Exception | None = None
|
|
381
|
+
delay = self.config.retry_delay
|
|
382
|
+
|
|
383
|
+
for attempt in range(self.config.max_retries):
|
|
384
|
+
try:
|
|
385
|
+
response = await self._http_client.request(
|
|
386
|
+
method,
|
|
387
|
+
path,
|
|
388
|
+
json=json,
|
|
389
|
+
params=params,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
# Handle different status codes
|
|
393
|
+
if response.status_code == 200:
|
|
394
|
+
return response.json()
|
|
395
|
+
elif response.status_code == 201:
|
|
396
|
+
return response.json()
|
|
397
|
+
elif response.status_code == 204:
|
|
398
|
+
return {}
|
|
399
|
+
elif response.status_code == 401:
|
|
400
|
+
raise AuthenticationError("Invalid API key")
|
|
401
|
+
elif response.status_code == 404:
|
|
402
|
+
raise NotFoundError(response.json().get("detail", "Not found"))
|
|
403
|
+
elif response.status_code == 422:
|
|
404
|
+
raise ValidationError(response.json().get("detail", "Validation error"))
|
|
405
|
+
elif response.status_code == 429:
|
|
406
|
+
raise RateLimitError("Rate limit exceeded")
|
|
407
|
+
else:
|
|
408
|
+
response.raise_for_status()
|
|
409
|
+
|
|
410
|
+
except (httpx.ConnectError, httpx.TimeoutException) as e:
|
|
411
|
+
last_error = e
|
|
412
|
+
if attempt < self.config.max_retries - 1:
|
|
413
|
+
logger.warning(f"Request failed (attempt {attempt + 1}), retrying in {delay}s")
|
|
414
|
+
await asyncio.sleep(delay)
|
|
415
|
+
delay *= 2 # Exponential backoff
|
|
416
|
+
continue
|
|
417
|
+
|
|
418
|
+
except (AuthenticationError, NotFoundError, ValidationError, RateLimitError):
|
|
419
|
+
raise
|
|
420
|
+
|
|
421
|
+
except httpx.HTTPStatusError as e:
|
|
422
|
+
raise SDKError(f"HTTP error: {e}") from e
|
|
423
|
+
|
|
424
|
+
raise ConnectionError(f"Failed after {self.config.max_retries} attempts: {last_error}")
|
|
425
|
+
|
|
426
|
+
# =========================================================================
|
|
427
|
+
# CRUD Operations
|
|
428
|
+
# =========================================================================
|
|
429
|
+
|
|
430
|
+
async def add(
|
|
431
|
+
self,
|
|
432
|
+
content: str,
|
|
433
|
+
category: str | MemoryCategory,
|
|
434
|
+
project: str | None = None,
|
|
435
|
+
scope: str | MemoryScope = MemoryScope.PROJECT,
|
|
436
|
+
source: str | MemorySource = MemorySource.EXPLICIT,
|
|
437
|
+
confidence: float = 1.0,
|
|
438
|
+
importance: float = 0.5,
|
|
439
|
+
tags: list[str] | None = None,
|
|
440
|
+
entities: list[str] | None = None,
|
|
441
|
+
supersedes: str | None = None,
|
|
442
|
+
metadata: dict[str, Any] | None = None,
|
|
443
|
+
) -> Memory:
|
|
444
|
+
"""Add a new memory.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
content: The memory content text.
|
|
448
|
+
category: Classification category (string or enum).
|
|
449
|
+
project: Project scope (None for global).
|
|
450
|
+
scope: Visibility scope.
|
|
451
|
+
source: How the memory was created.
|
|
452
|
+
confidence: Source reliability (0.0-1.0).
|
|
453
|
+
importance: Importance weight (0.0-1.0).
|
|
454
|
+
tags: Optional tags for categorization.
|
|
455
|
+
entities: Detected entities (files, functions, etc.).
|
|
456
|
+
supersedes: ID of memory this one replaces.
|
|
457
|
+
metadata: Additional metadata.
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
The created Memory.
|
|
461
|
+
"""
|
|
462
|
+
self._ensure_initialized()
|
|
463
|
+
|
|
464
|
+
# Normalize category to enum
|
|
465
|
+
if isinstance(category, str):
|
|
466
|
+
category = MemoryCategory(category)
|
|
467
|
+
if isinstance(scope, str):
|
|
468
|
+
scope = MemoryScope(scope)
|
|
469
|
+
if isinstance(source, str):
|
|
470
|
+
source = MemorySource(source)
|
|
471
|
+
|
|
472
|
+
if self.config.mode == ClientMode.LOCAL:
|
|
473
|
+
assert self._engine is not None
|
|
474
|
+
return await self._engine.add(
|
|
475
|
+
content=content,
|
|
476
|
+
category=category,
|
|
477
|
+
project=project,
|
|
478
|
+
scope=scope,
|
|
479
|
+
source=source,
|
|
480
|
+
confidence=confidence,
|
|
481
|
+
importance=importance,
|
|
482
|
+
tags=tags,
|
|
483
|
+
entities=entities,
|
|
484
|
+
supersedes=supersedes,
|
|
485
|
+
metadata=metadata,
|
|
486
|
+
)
|
|
487
|
+
else:
|
|
488
|
+
# Remote mode
|
|
489
|
+
response = await self._request(
|
|
490
|
+
"POST",
|
|
491
|
+
"/memories",
|
|
492
|
+
json={
|
|
493
|
+
"content": content,
|
|
494
|
+
"category": category.value,
|
|
495
|
+
"project": project,
|
|
496
|
+
"tags": tags or [],
|
|
497
|
+
"importance": importance,
|
|
498
|
+
"entities": entities or [],
|
|
499
|
+
},
|
|
500
|
+
)
|
|
501
|
+
return Memory.from_dict(response)
|
|
502
|
+
|
|
503
|
+
async def get(self, memory_id: str) -> Memory:
|
|
504
|
+
"""Get a memory by ID.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
memory_id: The memory ID.
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
The Memory.
|
|
511
|
+
|
|
512
|
+
Raises:
|
|
513
|
+
NotFoundError: If memory not found.
|
|
514
|
+
"""
|
|
515
|
+
self._ensure_initialized()
|
|
516
|
+
|
|
517
|
+
if self.config.mode == ClientMode.LOCAL:
|
|
518
|
+
assert self._engine is not None
|
|
519
|
+
try:
|
|
520
|
+
return await self._engine.get(memory_id)
|
|
521
|
+
except Exception as e:
|
|
522
|
+
if "not found" in str(e).lower():
|
|
523
|
+
raise NotFoundError(f"Memory not found: {memory_id}") from e
|
|
524
|
+
raise
|
|
525
|
+
else:
|
|
526
|
+
response = await self._request("GET", f"/memories/{memory_id}")
|
|
527
|
+
return Memory.from_dict(response)
|
|
528
|
+
|
|
529
|
+
async def update(
|
|
530
|
+
self,
|
|
531
|
+
memory_id: str,
|
|
532
|
+
content: str | None = None,
|
|
533
|
+
category: str | MemoryCategory | None = None,
|
|
534
|
+
confidence: float | None = None,
|
|
535
|
+
importance: float | None = None,
|
|
536
|
+
tags: list[str] | None = None,
|
|
537
|
+
entities: list[str] | None = None,
|
|
538
|
+
metadata: dict[str, Any] | None = None,
|
|
539
|
+
) -> Memory:
|
|
540
|
+
"""Update an existing memory.
|
|
541
|
+
|
|
542
|
+
Only provided fields are updated.
|
|
543
|
+
|
|
544
|
+
Args:
|
|
545
|
+
memory_id: ID of memory to update.
|
|
546
|
+
content: New content (optional).
|
|
547
|
+
category: New category (optional).
|
|
548
|
+
confidence: New confidence (optional).
|
|
549
|
+
importance: New importance (optional).
|
|
550
|
+
tags: New tags (optional).
|
|
551
|
+
entities: New entities (optional).
|
|
552
|
+
metadata: New metadata (optional).
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
The updated Memory.
|
|
556
|
+
|
|
557
|
+
Raises:
|
|
558
|
+
NotFoundError: If memory not found.
|
|
559
|
+
"""
|
|
560
|
+
self._ensure_initialized()
|
|
561
|
+
|
|
562
|
+
# Normalize category to enum
|
|
563
|
+
if isinstance(category, str):
|
|
564
|
+
category = MemoryCategory(category)
|
|
565
|
+
|
|
566
|
+
if self.config.mode == ClientMode.LOCAL:
|
|
567
|
+
assert self._engine is not None
|
|
568
|
+
return await self._engine.update(
|
|
569
|
+
memory_id=memory_id,
|
|
570
|
+
content=content,
|
|
571
|
+
category=category,
|
|
572
|
+
confidence=confidence,
|
|
573
|
+
importance=importance,
|
|
574
|
+
tags=tags,
|
|
575
|
+
entities=entities,
|
|
576
|
+
metadata=metadata,
|
|
577
|
+
)
|
|
578
|
+
else:
|
|
579
|
+
payload: dict[str, Any] = {}
|
|
580
|
+
if content is not None:
|
|
581
|
+
payload["content"] = content
|
|
582
|
+
if category is not None:
|
|
583
|
+
payload["category"] = category.value
|
|
584
|
+
if tags is not None:
|
|
585
|
+
payload["tags"] = tags
|
|
586
|
+
if importance is not None:
|
|
587
|
+
payload["importance"] = importance
|
|
588
|
+
|
|
589
|
+
response = await self._request("PATCH", f"/memories/{memory_id}", json=payload)
|
|
590
|
+
return Memory.from_dict(response)
|
|
591
|
+
|
|
592
|
+
async def delete(self, memory_id: str, hard_delete: bool = False) -> None:
|
|
593
|
+
"""Delete a memory.
|
|
594
|
+
|
|
595
|
+
By default, performs a soft delete (archive).
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
memory_id: ID of memory to delete.
|
|
599
|
+
hard_delete: If True, permanently delete.
|
|
600
|
+
|
|
601
|
+
Raises:
|
|
602
|
+
NotFoundError: If memory not found.
|
|
603
|
+
"""
|
|
604
|
+
self._ensure_initialized()
|
|
605
|
+
|
|
606
|
+
if self.config.mode == ClientMode.LOCAL:
|
|
607
|
+
assert self._engine is not None
|
|
608
|
+
await self._engine.delete(memory_id, hard_delete=hard_delete)
|
|
609
|
+
else:
|
|
610
|
+
await self._request("DELETE", f"/memories/{memory_id}")
|
|
611
|
+
|
|
612
|
+
# =========================================================================
|
|
613
|
+
# Search and List
|
|
614
|
+
# =========================================================================
|
|
615
|
+
|
|
616
|
+
async def search(
|
|
617
|
+
self,
|
|
618
|
+
query: str,
|
|
619
|
+
limit: int = 10,
|
|
620
|
+
categories: list[str | MemoryCategory] | None = None,
|
|
621
|
+
project: str | None = None,
|
|
622
|
+
min_score: float = -1.0,
|
|
623
|
+
include_archived: bool = False,
|
|
624
|
+
) -> list[SearchResult]:
|
|
625
|
+
"""Search for relevant memories.
|
|
626
|
+
|
|
627
|
+
Uses hybrid retrieval combining BM25 text search and vector
|
|
628
|
+
similarity for intelligent ranking.
|
|
629
|
+
|
|
630
|
+
Args:
|
|
631
|
+
query: Search query text.
|
|
632
|
+
limit: Maximum number of results.
|
|
633
|
+
categories: Filter by categories.
|
|
634
|
+
project: Filter by project.
|
|
635
|
+
min_score: Minimum outcome score threshold.
|
|
636
|
+
include_archived: Whether to include archived memories.
|
|
637
|
+
|
|
638
|
+
Returns:
|
|
639
|
+
List of SearchResults sorted by relevance.
|
|
640
|
+
"""
|
|
641
|
+
self._ensure_initialized()
|
|
642
|
+
|
|
643
|
+
# Normalize categories
|
|
644
|
+
if categories:
|
|
645
|
+
categories = [
|
|
646
|
+
MemoryCategory(c) if isinstance(c, str) else c
|
|
647
|
+
for c in categories
|
|
648
|
+
]
|
|
649
|
+
|
|
650
|
+
if self.config.mode == ClientMode.LOCAL:
|
|
651
|
+
assert self._engine is not None
|
|
652
|
+
# Note: Local mode only supports single category filter
|
|
653
|
+
category = categories[0] if categories and len(categories) == 1 else None
|
|
654
|
+
return await self._engine.search(
|
|
655
|
+
query=query,
|
|
656
|
+
limit=limit,
|
|
657
|
+
category=category,
|
|
658
|
+
project=project,
|
|
659
|
+
min_score=min_score,
|
|
660
|
+
include_archived=include_archived,
|
|
661
|
+
)
|
|
662
|
+
else:
|
|
663
|
+
payload: dict[str, Any] = {
|
|
664
|
+
"query": query,
|
|
665
|
+
"limit": limit,
|
|
666
|
+
"min_score": min_score,
|
|
667
|
+
}
|
|
668
|
+
if categories:
|
|
669
|
+
payload["categories"] = [c.value for c in categories]
|
|
670
|
+
if project:
|
|
671
|
+
payload["project"] = project
|
|
672
|
+
|
|
673
|
+
response = await self._request("POST", "/memories/search", json=payload)
|
|
674
|
+
results = []
|
|
675
|
+
for item in response.get("results", []):
|
|
676
|
+
memory = Memory.from_dict(item["memory"])
|
|
677
|
+
results.append(SearchResult(
|
|
678
|
+
memory=memory,
|
|
679
|
+
score=item.get("score", 0.0),
|
|
680
|
+
semantic_score=item.get("semantic_score", 0.0),
|
|
681
|
+
recency_score=item.get("recency_score", 0.0),
|
|
682
|
+
frequency_score=item.get("frequency_score", 0.0),
|
|
683
|
+
))
|
|
684
|
+
return results
|
|
685
|
+
|
|
686
|
+
async def list(
|
|
687
|
+
self,
|
|
688
|
+
project: str | None = None,
|
|
689
|
+
category: str | MemoryCategory | None = None,
|
|
690
|
+
limit: int = 100,
|
|
691
|
+
include_archived: bool = False,
|
|
692
|
+
) -> list[Memory]:
|
|
693
|
+
"""List memories with filtering.
|
|
694
|
+
|
|
695
|
+
Args:
|
|
696
|
+
project: Filter by project.
|
|
697
|
+
category: Filter by category.
|
|
698
|
+
limit: Maximum results to return.
|
|
699
|
+
include_archived: Whether to include archived memories.
|
|
700
|
+
|
|
701
|
+
Returns:
|
|
702
|
+
List of memories matching filters.
|
|
703
|
+
"""
|
|
704
|
+
self._ensure_initialized()
|
|
705
|
+
|
|
706
|
+
# Normalize category
|
|
707
|
+
if isinstance(category, str):
|
|
708
|
+
category = MemoryCategory(category)
|
|
709
|
+
|
|
710
|
+
if self.config.mode == ClientMode.LOCAL:
|
|
711
|
+
assert self._engine is not None
|
|
712
|
+
return await self._engine.list(
|
|
713
|
+
project=project,
|
|
714
|
+
category=category,
|
|
715
|
+
limit=limit,
|
|
716
|
+
include_archived=include_archived,
|
|
717
|
+
)
|
|
718
|
+
else:
|
|
719
|
+
params: dict[str, Any] = {
|
|
720
|
+
"limit": limit,
|
|
721
|
+
"include_archived": include_archived,
|
|
722
|
+
}
|
|
723
|
+
if project:
|
|
724
|
+
params["project"] = project
|
|
725
|
+
if category:
|
|
726
|
+
params["category"] = category.value
|
|
727
|
+
|
|
728
|
+
response = await self._request("GET", "/memories", params=params)
|
|
729
|
+
return [Memory.from_dict(m) for m in response.get("memories", [])]
|
|
730
|
+
|
|
731
|
+
# =========================================================================
|
|
732
|
+
# Outcome Recording
|
|
733
|
+
# =========================================================================
|
|
734
|
+
|
|
735
|
+
async def record_outcome(
|
|
736
|
+
self,
|
|
737
|
+
memory_ids: list[str] | str,
|
|
738
|
+
outcome: str | Outcome,
|
|
739
|
+
) -> list[Memory]:
|
|
740
|
+
"""Record outcome feedback for memories.
|
|
741
|
+
|
|
742
|
+
Updates outcome scores for specified memories based on feedback.
|
|
743
|
+
|
|
744
|
+
Args:
|
|
745
|
+
memory_ids: Memory ID(s) to update.
|
|
746
|
+
outcome: The outcome (WORKED, FAILED, PARTIAL).
|
|
747
|
+
|
|
748
|
+
Returns:
|
|
749
|
+
List of updated memories (local mode only, empty list for remote).
|
|
750
|
+
"""
|
|
751
|
+
self._ensure_initialized()
|
|
752
|
+
|
|
753
|
+
# Normalize inputs
|
|
754
|
+
if isinstance(memory_ids, str):
|
|
755
|
+
memory_ids = [memory_ids]
|
|
756
|
+
if isinstance(outcome, str):
|
|
757
|
+
outcome = Outcome(outcome)
|
|
758
|
+
|
|
759
|
+
if self.config.mode == ClientMode.LOCAL:
|
|
760
|
+
assert self._engine is not None
|
|
761
|
+
return await self._engine.record_outcome(memory_ids, outcome)
|
|
762
|
+
else:
|
|
763
|
+
await self._request(
|
|
764
|
+
"POST",
|
|
765
|
+
"/memories/outcome",
|
|
766
|
+
json={
|
|
767
|
+
"memory_ids": memory_ids,
|
|
768
|
+
"outcome": outcome.value,
|
|
769
|
+
},
|
|
770
|
+
)
|
|
771
|
+
# Remote mode doesn't return updated memories
|
|
772
|
+
return []
|
|
773
|
+
|
|
774
|
+
# =========================================================================
|
|
775
|
+
# Context
|
|
776
|
+
# =========================================================================
|
|
777
|
+
|
|
778
|
+
async def get_context(
|
|
779
|
+
self,
|
|
780
|
+
project: str | None = None,
|
|
781
|
+
query: str | None = None,
|
|
782
|
+
limit: int = 10,
|
|
783
|
+
format: str = "markdown",
|
|
784
|
+
) -> ContextResponse:
|
|
785
|
+
"""Get formatted context for injection into prompts.
|
|
786
|
+
|
|
787
|
+
Args:
|
|
788
|
+
project: Project filter.
|
|
789
|
+
query: Optional query to find relevant memories.
|
|
790
|
+
limit: Maximum memories to include.
|
|
791
|
+
format: Output format (markdown, brief, detailed).
|
|
792
|
+
|
|
793
|
+
Returns:
|
|
794
|
+
ContextResponse with formatted context.
|
|
795
|
+
"""
|
|
796
|
+
self._ensure_initialized()
|
|
797
|
+
|
|
798
|
+
if self.config.mode == ClientMode.LOCAL:
|
|
799
|
+
assert self._engine is not None
|
|
800
|
+
return await self._engine.get_context(
|
|
801
|
+
query=query,
|
|
802
|
+
project=project,
|
|
803
|
+
max_memories=limit,
|
|
804
|
+
)
|
|
805
|
+
else:
|
|
806
|
+
params: dict[str, Any] = {
|
|
807
|
+
"limit": limit,
|
|
808
|
+
"format": format,
|
|
809
|
+
}
|
|
810
|
+
if project:
|
|
811
|
+
params["project"] = project
|
|
812
|
+
|
|
813
|
+
response = await self._request("GET", "/context", params=params)
|
|
814
|
+
|
|
815
|
+
memories = [Memory.from_dict(m) for m in response.get("memories", [])]
|
|
816
|
+
return ContextResponse(
|
|
817
|
+
memories=memories,
|
|
818
|
+
project=response.get("project"),
|
|
819
|
+
total_count=response.get("total_count", 0),
|
|
820
|
+
included_count=response.get("included_count", len(memories)),
|
|
821
|
+
formatted=response.get("formatted", ""),
|
|
822
|
+
categories={},
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
# =========================================================================
|
|
826
|
+
# Statistics
|
|
827
|
+
# =========================================================================
|
|
828
|
+
|
|
829
|
+
async def stats(self, project: str | None = None) -> StatsDict | Any:
|
|
830
|
+
"""Get memory statistics.
|
|
831
|
+
|
|
832
|
+
Args:
|
|
833
|
+
project: Optional project filter.
|
|
834
|
+
|
|
835
|
+
Returns:
|
|
836
|
+
Statistics dictionary (StatsDict for remote, EngineStats for local).
|
|
837
|
+
"""
|
|
838
|
+
self._ensure_initialized()
|
|
839
|
+
|
|
840
|
+
if self.config.mode == ClientMode.LOCAL:
|
|
841
|
+
assert self._engine is not None
|
|
842
|
+
return await self._engine.stats(project=project)
|
|
843
|
+
else:
|
|
844
|
+
params: dict[str, Any] = {}
|
|
845
|
+
if project:
|
|
846
|
+
params["project"] = project
|
|
847
|
+
|
|
848
|
+
response = await self._request("GET", "/stats", params=params)
|
|
849
|
+
return StatsDict.from_dict(response)
|
|
850
|
+
|
|
851
|
+
# =========================================================================
|
|
852
|
+
# Health Check
|
|
853
|
+
# =========================================================================
|
|
854
|
+
|
|
855
|
+
async def health(self) -> dict[str, Any]:
|
|
856
|
+
"""Check client health.
|
|
857
|
+
|
|
858
|
+
Returns:
|
|
859
|
+
Health status dictionary.
|
|
860
|
+
"""
|
|
861
|
+
self._ensure_initialized()
|
|
862
|
+
|
|
863
|
+
if self.config.mode == ClientMode.LOCAL:
|
|
864
|
+
assert self._engine is not None
|
|
865
|
+
return await self._engine.health_check()
|
|
866
|
+
else:
|
|
867
|
+
response = await self._request("GET", "/health")
|
|
868
|
+
return response
|
|
869
|
+
|
|
870
|
+
# =========================================================================
|
|
871
|
+
# Beads Integration
|
|
872
|
+
# =========================================================================
|
|
873
|
+
|
|
874
|
+
async def beads_sync(self, task_id: str | None = None) -> dict[str, Any]:
|
|
875
|
+
"""Sync outcomes for completed Beads tasks.
|
|
876
|
+
|
|
877
|
+
When a task completes, memories that helped solve it get their
|
|
878
|
+
outcome scores boosted.
|
|
879
|
+
|
|
880
|
+
Args:
|
|
881
|
+
task_id: Optional specific task ID to sync (syncs all if None).
|
|
882
|
+
|
|
883
|
+
Returns:
|
|
884
|
+
Sync result dictionary with tasks_found, outcomes_recorded, etc.
|
|
885
|
+
"""
|
|
886
|
+
self._ensure_initialized()
|
|
887
|
+
|
|
888
|
+
if self.config.mode == ClientMode.LOCAL:
|
|
889
|
+
from runtime_memory.tasks import BeadsAdapter, BeadsTaskStatus
|
|
890
|
+
|
|
891
|
+
assert self._engine is not None
|
|
892
|
+
adapter = BeadsAdapter(self._engine)
|
|
893
|
+
await adapter.initialize()
|
|
894
|
+
|
|
895
|
+
if not adapter.is_available:
|
|
896
|
+
return {"success": False, "error": "Beads not available"}
|
|
897
|
+
|
|
898
|
+
if task_id:
|
|
899
|
+
task = adapter.get_task(task_id)
|
|
900
|
+
if not task:
|
|
901
|
+
return {"success": False, "error": f"Task {task_id} not found"}
|
|
902
|
+
|
|
903
|
+
if task.status == BeadsTaskStatus.DONE:
|
|
904
|
+
count = await adapter.on_task_done(task_id)
|
|
905
|
+
elif task.status == BeadsTaskStatus.CANCELLED:
|
|
906
|
+
count = await adapter.on_task_cancelled(task_id)
|
|
907
|
+
elif task.status == BeadsTaskStatus.BLOCKED:
|
|
908
|
+
count = await adapter.on_task_blocked(task_id)
|
|
909
|
+
else:
|
|
910
|
+
count = 0
|
|
911
|
+
|
|
912
|
+
return {
|
|
913
|
+
"success": True,
|
|
914
|
+
"task_id": task_id,
|
|
915
|
+
"outcomes_recorded": count,
|
|
916
|
+
}
|
|
917
|
+
else:
|
|
918
|
+
result = await adapter.sync()
|
|
919
|
+
return result.to_dict()
|
|
920
|
+
else:
|
|
921
|
+
data = {}
|
|
922
|
+
if task_id:
|
|
923
|
+
data["task_id"] = task_id
|
|
924
|
+
response = await self._request("POST", "/beads/sync", json=data)
|
|
925
|
+
return response
|
|
926
|
+
|
|
927
|
+
async def beads_context(
|
|
928
|
+
self,
|
|
929
|
+
task_id: str | None = None,
|
|
930
|
+
limit: int = 10,
|
|
931
|
+
) -> dict[str, Any]:
|
|
932
|
+
"""Get unified context for a Beads task.
|
|
933
|
+
|
|
934
|
+
Combines task info with relevant memories.
|
|
935
|
+
|
|
936
|
+
Args:
|
|
937
|
+
task_id: Task ID (uses current task if None).
|
|
938
|
+
limit: Maximum memories to include.
|
|
939
|
+
|
|
940
|
+
Returns:
|
|
941
|
+
Context dictionary with task info and memories.
|
|
942
|
+
"""
|
|
943
|
+
self._ensure_initialized()
|
|
944
|
+
|
|
945
|
+
if self.config.mode == ClientMode.LOCAL:
|
|
946
|
+
from runtime_memory.tasks import BeadsAdapter
|
|
947
|
+
|
|
948
|
+
assert self._engine is not None
|
|
949
|
+
adapter = BeadsAdapter(self._engine)
|
|
950
|
+
await adapter.initialize()
|
|
951
|
+
|
|
952
|
+
if not adapter.is_available:
|
|
953
|
+
return {"success": False, "error": "Beads not available"}
|
|
954
|
+
|
|
955
|
+
context = await adapter.get_unified_context(task_id, limit)
|
|
956
|
+
if not context:
|
|
957
|
+
return {"success": False, "error": "No task found"}
|
|
958
|
+
|
|
959
|
+
return {
|
|
960
|
+
"success": True,
|
|
961
|
+
"task_id": context.task.id,
|
|
962
|
+
"task_title": context.task.title,
|
|
963
|
+
"task_status": context.task.status.value,
|
|
964
|
+
"memories_count": len(context.memories),
|
|
965
|
+
"formatted": context.formatted,
|
|
966
|
+
}
|
|
967
|
+
else:
|
|
968
|
+
params: dict[str, Any] = {"limit": limit}
|
|
969
|
+
if task_id:
|
|
970
|
+
params["task_id"] = task_id
|
|
971
|
+
response = await self._request("GET", "/beads/context", params=params)
|
|
972
|
+
return response
|
|
973
|
+
|
|
974
|
+
async def beads_link(
|
|
975
|
+
self,
|
|
976
|
+
memory_id: str,
|
|
977
|
+
task_id: str | None = None,
|
|
978
|
+
context: str | None = None,
|
|
979
|
+
) -> dict[str, Any]:
|
|
980
|
+
"""Link a memory to a Beads task.
|
|
981
|
+
|
|
982
|
+
When the task completes, the memory's outcome will be recorded.
|
|
983
|
+
|
|
984
|
+
Args:
|
|
985
|
+
memory_id: Memory ID to link.
|
|
986
|
+
task_id: Task ID (uses current task if None).
|
|
987
|
+
context: Optional context about how memory is used.
|
|
988
|
+
|
|
989
|
+
Returns:
|
|
990
|
+
Link result dictionary.
|
|
991
|
+
"""
|
|
992
|
+
self._ensure_initialized()
|
|
993
|
+
|
|
994
|
+
if self.config.mode == ClientMode.LOCAL:
|
|
995
|
+
from runtime_memory.tasks import BeadsAdapter
|
|
996
|
+
|
|
997
|
+
assert self._engine is not None
|
|
998
|
+
adapter = BeadsAdapter(self._engine)
|
|
999
|
+
await adapter.initialize()
|
|
1000
|
+
|
|
1001
|
+
if not adapter.is_available:
|
|
1002
|
+
return {"success": False, "error": "Beads not available"}
|
|
1003
|
+
|
|
1004
|
+
if task_id:
|
|
1005
|
+
task = adapter.get_task(task_id)
|
|
1006
|
+
if not task:
|
|
1007
|
+
return {"success": False, "error": f"Task {task_id} not found"}
|
|
1008
|
+
else:
|
|
1009
|
+
task = adapter.get_current_task()
|
|
1010
|
+
if not task:
|
|
1011
|
+
return {"success": False, "error": "No current task found"}
|
|
1012
|
+
task_id = task.id
|
|
1013
|
+
|
|
1014
|
+
try:
|
|
1015
|
+
await self._engine.get(memory_id)
|
|
1016
|
+
except Exception:
|
|
1017
|
+
return {"success": False, "error": f"Memory {memory_id} not found"}
|
|
1018
|
+
|
|
1019
|
+
await adapter.link_memory_to_task(task_id, memory_id, context)
|
|
1020
|
+
return {"success": True, "memory_id": memory_id, "task_id": task_id}
|
|
1021
|
+
else:
|
|
1022
|
+
data: dict[str, Any] = {"memory_id": memory_id}
|
|
1023
|
+
if task_id:
|
|
1024
|
+
data["task_id"] = task_id
|
|
1025
|
+
if context:
|
|
1026
|
+
data["context"] = context
|
|
1027
|
+
response = await self._request("POST", "/beads/link", json=data)
|
|
1028
|
+
return response
|
|
1029
|
+
|
|
1030
|
+
async def beads_tasks(
|
|
1031
|
+
self,
|
|
1032
|
+
status: str | None = None,
|
|
1033
|
+
limit: int = 20,
|
|
1034
|
+
) -> dict[str, Any]:
|
|
1035
|
+
"""List Beads tasks.
|
|
1036
|
+
|
|
1037
|
+
Args:
|
|
1038
|
+
status: Optional status filter (pending, in_progress, done, etc).
|
|
1039
|
+
limit: Maximum tasks to return.
|
|
1040
|
+
|
|
1041
|
+
Returns:
|
|
1042
|
+
Dictionary with task list.
|
|
1043
|
+
"""
|
|
1044
|
+
self._ensure_initialized()
|
|
1045
|
+
|
|
1046
|
+
if self.config.mode == ClientMode.LOCAL:
|
|
1047
|
+
from runtime_memory.tasks import BeadsAdapter, BeadsTaskStatus
|
|
1048
|
+
|
|
1049
|
+
assert self._engine is not None
|
|
1050
|
+
adapter = BeadsAdapter(self._engine)
|
|
1051
|
+
await adapter.initialize()
|
|
1052
|
+
|
|
1053
|
+
if not adapter.is_available:
|
|
1054
|
+
return {"success": False, "error": "Beads not available"}
|
|
1055
|
+
|
|
1056
|
+
status_enum = None
|
|
1057
|
+
if status:
|
|
1058
|
+
try:
|
|
1059
|
+
status_enum = BeadsTaskStatus(status)
|
|
1060
|
+
except ValueError:
|
|
1061
|
+
return {"success": False, "error": f"Invalid status: {status}"}
|
|
1062
|
+
|
|
1063
|
+
tasks = adapter.list_tasks(status=status_enum)[:limit]
|
|
1064
|
+
return {
|
|
1065
|
+
"success": True,
|
|
1066
|
+
"count": len(tasks),
|
|
1067
|
+
"tasks": [
|
|
1068
|
+
{
|
|
1069
|
+
"id": t.id,
|
|
1070
|
+
"title": t.title,
|
|
1071
|
+
"status": t.status.value,
|
|
1072
|
+
"is_ready": t.is_ready,
|
|
1073
|
+
"is_completed": t.is_completed,
|
|
1074
|
+
}
|
|
1075
|
+
for t in tasks
|
|
1076
|
+
],
|
|
1077
|
+
}
|
|
1078
|
+
else:
|
|
1079
|
+
params: dict[str, Any] = {"limit": limit}
|
|
1080
|
+
if status:
|
|
1081
|
+
params["status"] = status
|
|
1082
|
+
response = await self._request("GET", "/beads/tasks", params=params)
|
|
1083
|
+
return response
|
|
1084
|
+
|
|
1085
|
+
async def beads_stats(self) -> dict[str, Any]:
|
|
1086
|
+
"""Get Beads integration statistics.
|
|
1087
|
+
|
|
1088
|
+
Returns:
|
|
1089
|
+
Statistics dictionary.
|
|
1090
|
+
"""
|
|
1091
|
+
self._ensure_initialized()
|
|
1092
|
+
|
|
1093
|
+
if self.config.mode == ClientMode.LOCAL:
|
|
1094
|
+
from runtime_memory.tasks import BeadsAdapter
|
|
1095
|
+
|
|
1096
|
+
assert self._engine is not None
|
|
1097
|
+
adapter = BeadsAdapter(self._engine)
|
|
1098
|
+
await adapter.initialize()
|
|
1099
|
+
|
|
1100
|
+
return await adapter.get_stats()
|
|
1101
|
+
else:
|
|
1102
|
+
response = await self._request("GET", "/beads/stats")
|
|
1103
|
+
return response
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
# =============================================================================
|
|
1107
|
+
# Synchronous Wrapper
|
|
1108
|
+
# =============================================================================
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
class SyncMemoryClient:
|
|
1112
|
+
"""Synchronous wrapper for MemoryClient.
|
|
1113
|
+
|
|
1114
|
+
Provides a sync interface by running async operations in an event loop.
|
|
1115
|
+
|
|
1116
|
+
Example:
|
|
1117
|
+
```python
|
|
1118
|
+
with SyncMemoryClient(mode="local") as client:
|
|
1119
|
+
memory = client.add(
|
|
1120
|
+
content="Use async/await for I/O",
|
|
1121
|
+
category="pattern",
|
|
1122
|
+
)
|
|
1123
|
+
results = client.search("async patterns")
|
|
1124
|
+
```
|
|
1125
|
+
"""
|
|
1126
|
+
|
|
1127
|
+
def __init__(
|
|
1128
|
+
self,
|
|
1129
|
+
config: ClientConfig | None = None,
|
|
1130
|
+
*,
|
|
1131
|
+
mode: str | ClientMode = ClientMode.LOCAL,
|
|
1132
|
+
db_path: str | Path | None = None,
|
|
1133
|
+
embedding_provider: str = "local",
|
|
1134
|
+
base_url: str | None = None,
|
|
1135
|
+
api_key: str | None = None,
|
|
1136
|
+
timeout: float = 30.0,
|
|
1137
|
+
max_retries: int = 3,
|
|
1138
|
+
) -> None:
|
|
1139
|
+
"""Initialize the Sync Memory Client.
|
|
1140
|
+
|
|
1141
|
+
Args:
|
|
1142
|
+
config: Full configuration object.
|
|
1143
|
+
mode: Operating mode: "local" or "remote".
|
|
1144
|
+
db_path: Database path for local mode.
|
|
1145
|
+
embedding_provider: Embedding provider for local mode.
|
|
1146
|
+
base_url: API base URL for remote mode.
|
|
1147
|
+
api_key: API key for authentication.
|
|
1148
|
+
timeout: Request timeout in seconds.
|
|
1149
|
+
max_retries: Maximum retry attempts.
|
|
1150
|
+
"""
|
|
1151
|
+
self._async_client = MemoryClient(
|
|
1152
|
+
config=config,
|
|
1153
|
+
mode=mode,
|
|
1154
|
+
db_path=db_path,
|
|
1155
|
+
embedding_provider=embedding_provider,
|
|
1156
|
+
base_url=base_url,
|
|
1157
|
+
api_key=api_key,
|
|
1158
|
+
timeout=timeout,
|
|
1159
|
+
max_retries=max_retries,
|
|
1160
|
+
)
|
|
1161
|
+
self._loop: asyncio.AbstractEventLoop | None = None
|
|
1162
|
+
self._owns_loop = False
|
|
1163
|
+
|
|
1164
|
+
def _get_loop(self) -> asyncio.AbstractEventLoop:
|
|
1165
|
+
"""Get or create event loop."""
|
|
1166
|
+
if self._loop is None or self._loop.is_closed():
|
|
1167
|
+
try:
|
|
1168
|
+
self._loop = asyncio.get_running_loop()
|
|
1169
|
+
self._owns_loop = False
|
|
1170
|
+
except RuntimeError:
|
|
1171
|
+
self._loop = asyncio.new_event_loop()
|
|
1172
|
+
self._owns_loop = True
|
|
1173
|
+
return self._loop
|
|
1174
|
+
|
|
1175
|
+
def _run(self, coro: Any) -> Any:
|
|
1176
|
+
"""Run a coroutine synchronously."""
|
|
1177
|
+
loop = self._get_loop()
|
|
1178
|
+
if loop.is_running():
|
|
1179
|
+
# If loop is running, we need to use run_coroutine_threadsafe
|
|
1180
|
+
import concurrent.futures
|
|
1181
|
+
future = asyncio.run_coroutine_threadsafe(coro, loop)
|
|
1182
|
+
return future.result()
|
|
1183
|
+
else:
|
|
1184
|
+
return loop.run_until_complete(coro)
|
|
1185
|
+
|
|
1186
|
+
def initialize(self) -> None:
|
|
1187
|
+
"""Initialize the client."""
|
|
1188
|
+
self._run(self._async_client.initialize())
|
|
1189
|
+
|
|
1190
|
+
def close(self) -> None:
|
|
1191
|
+
"""Close the client."""
|
|
1192
|
+
self._run(self._async_client.close())
|
|
1193
|
+
if self._owns_loop and self._loop:
|
|
1194
|
+
self._loop.close()
|
|
1195
|
+
self._loop = None
|
|
1196
|
+
|
|
1197
|
+
def __enter__(self) -> SyncMemoryClient:
|
|
1198
|
+
"""Context manager entry."""
|
|
1199
|
+
self.initialize()
|
|
1200
|
+
return self
|
|
1201
|
+
|
|
1202
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
1203
|
+
"""Context manager exit."""
|
|
1204
|
+
self.close()
|
|
1205
|
+
|
|
1206
|
+
# =========================================================================
|
|
1207
|
+
# Sync Methods (wrap async methods)
|
|
1208
|
+
# =========================================================================
|
|
1209
|
+
|
|
1210
|
+
def add(
|
|
1211
|
+
self,
|
|
1212
|
+
content: str,
|
|
1213
|
+
category: str | MemoryCategory,
|
|
1214
|
+
project: str | None = None,
|
|
1215
|
+
scope: str | MemoryScope = MemoryScope.PROJECT,
|
|
1216
|
+
source: str | MemorySource = MemorySource.EXPLICIT,
|
|
1217
|
+
confidence: float = 1.0,
|
|
1218
|
+
importance: float = 0.5,
|
|
1219
|
+
tags: list[str] | None = None,
|
|
1220
|
+
entities: list[str] | None = None,
|
|
1221
|
+
supersedes: str | None = None,
|
|
1222
|
+
metadata: dict[str, Any] | None = None,
|
|
1223
|
+
) -> Memory:
|
|
1224
|
+
"""Add a new memory (sync)."""
|
|
1225
|
+
return self._run(self._async_client.add(
|
|
1226
|
+
content=content,
|
|
1227
|
+
category=category,
|
|
1228
|
+
project=project,
|
|
1229
|
+
scope=scope,
|
|
1230
|
+
source=source,
|
|
1231
|
+
confidence=confidence,
|
|
1232
|
+
importance=importance,
|
|
1233
|
+
tags=tags,
|
|
1234
|
+
entities=entities,
|
|
1235
|
+
supersedes=supersedes,
|
|
1236
|
+
metadata=metadata,
|
|
1237
|
+
))
|
|
1238
|
+
|
|
1239
|
+
def get(self, memory_id: str) -> Memory:
|
|
1240
|
+
"""Get a memory by ID (sync)."""
|
|
1241
|
+
return self._run(self._async_client.get(memory_id))
|
|
1242
|
+
|
|
1243
|
+
def update(
|
|
1244
|
+
self,
|
|
1245
|
+
memory_id: str,
|
|
1246
|
+
content: str | None = None,
|
|
1247
|
+
category: str | MemoryCategory | None = None,
|
|
1248
|
+
confidence: float | None = None,
|
|
1249
|
+
importance: float | None = None,
|
|
1250
|
+
tags: list[str] | None = None,
|
|
1251
|
+
entities: list[str] | None = None,
|
|
1252
|
+
metadata: dict[str, Any] | None = None,
|
|
1253
|
+
) -> Memory:
|
|
1254
|
+
"""Update an existing memory (sync)."""
|
|
1255
|
+
return self._run(self._async_client.update(
|
|
1256
|
+
memory_id=memory_id,
|
|
1257
|
+
content=content,
|
|
1258
|
+
category=category,
|
|
1259
|
+
confidence=confidence,
|
|
1260
|
+
importance=importance,
|
|
1261
|
+
tags=tags,
|
|
1262
|
+
entities=entities,
|
|
1263
|
+
metadata=metadata,
|
|
1264
|
+
))
|
|
1265
|
+
|
|
1266
|
+
def delete(self, memory_id: str, hard_delete: bool = False) -> None:
|
|
1267
|
+
"""Delete a memory (sync)."""
|
|
1268
|
+
return self._run(self._async_client.delete(memory_id, hard_delete=hard_delete))
|
|
1269
|
+
|
|
1270
|
+
def search(
|
|
1271
|
+
self,
|
|
1272
|
+
query: str,
|
|
1273
|
+
limit: int = 10,
|
|
1274
|
+
categories: list[str | MemoryCategory] | None = None,
|
|
1275
|
+
project: str | None = None,
|
|
1276
|
+
min_score: float = -1.0,
|
|
1277
|
+
include_archived: bool = False,
|
|
1278
|
+
) -> list[SearchResult]:
|
|
1279
|
+
"""Search for relevant memories (sync)."""
|
|
1280
|
+
return self._run(self._async_client.search(
|
|
1281
|
+
query=query,
|
|
1282
|
+
limit=limit,
|
|
1283
|
+
categories=categories,
|
|
1284
|
+
project=project,
|
|
1285
|
+
min_score=min_score,
|
|
1286
|
+
include_archived=include_archived,
|
|
1287
|
+
))
|
|
1288
|
+
|
|
1289
|
+
def list(
|
|
1290
|
+
self,
|
|
1291
|
+
project: str | None = None,
|
|
1292
|
+
category: str | MemoryCategory | None = None,
|
|
1293
|
+
limit: int = 100,
|
|
1294
|
+
include_archived: bool = False,
|
|
1295
|
+
) -> list[Memory]:
|
|
1296
|
+
"""List memories with filtering (sync)."""
|
|
1297
|
+
return self._run(self._async_client.list(
|
|
1298
|
+
project=project,
|
|
1299
|
+
category=category,
|
|
1300
|
+
limit=limit,
|
|
1301
|
+
include_archived=include_archived,
|
|
1302
|
+
))
|
|
1303
|
+
|
|
1304
|
+
def record_outcome(
|
|
1305
|
+
self,
|
|
1306
|
+
memory_ids: list[str] | str,
|
|
1307
|
+
outcome: str | Outcome,
|
|
1308
|
+
) -> list[Memory]:
|
|
1309
|
+
"""Record outcome feedback for memories (sync)."""
|
|
1310
|
+
return self._run(self._async_client.record_outcome(memory_ids, outcome))
|
|
1311
|
+
|
|
1312
|
+
def get_context(
|
|
1313
|
+
self,
|
|
1314
|
+
project: str | None = None,
|
|
1315
|
+
query: str | None = None,
|
|
1316
|
+
limit: int = 10,
|
|
1317
|
+
format: str = "markdown",
|
|
1318
|
+
) -> ContextResponse:
|
|
1319
|
+
"""Get formatted context (sync)."""
|
|
1320
|
+
return self._run(self._async_client.get_context(
|
|
1321
|
+
project=project,
|
|
1322
|
+
query=query,
|
|
1323
|
+
limit=limit,
|
|
1324
|
+
format=format,
|
|
1325
|
+
))
|
|
1326
|
+
|
|
1327
|
+
def stats(self, project: str | None = None) -> StatsDict | Any:
|
|
1328
|
+
"""Get memory statistics (sync)."""
|
|
1329
|
+
return self._run(self._async_client.stats(project=project))
|
|
1330
|
+
|
|
1331
|
+
def health(self) -> dict[str, Any]:
|
|
1332
|
+
"""Check client health (sync)."""
|
|
1333
|
+
return self._run(self._async_client.health())
|
|
1334
|
+
|
|
1335
|
+
# Beads integration methods
|
|
1336
|
+
|
|
1337
|
+
def beads_sync(self, task_id: str | None = None) -> dict[str, Any]:
|
|
1338
|
+
"""Sync outcomes for completed Beads tasks (sync)."""
|
|
1339
|
+
return self._run(self._async_client.beads_sync(task_id=task_id))
|
|
1340
|
+
|
|
1341
|
+
def beads_context(
|
|
1342
|
+
self,
|
|
1343
|
+
task_id: str | None = None,
|
|
1344
|
+
limit: int = 10,
|
|
1345
|
+
) -> dict[str, Any]:
|
|
1346
|
+
"""Get unified context for a Beads task (sync)."""
|
|
1347
|
+
return self._run(self._async_client.beads_context(task_id=task_id, limit=limit))
|
|
1348
|
+
|
|
1349
|
+
def beads_link(
|
|
1350
|
+
self,
|
|
1351
|
+
memory_id: str,
|
|
1352
|
+
task_id: str | None = None,
|
|
1353
|
+
context: str | None = None,
|
|
1354
|
+
) -> dict[str, Any]:
|
|
1355
|
+
"""Link a memory to a Beads task (sync)."""
|
|
1356
|
+
return self._run(self._async_client.beads_link(
|
|
1357
|
+
memory_id=memory_id,
|
|
1358
|
+
task_id=task_id,
|
|
1359
|
+
context=context,
|
|
1360
|
+
))
|
|
1361
|
+
|
|
1362
|
+
def beads_tasks(
|
|
1363
|
+
self,
|
|
1364
|
+
status: str | None = None,
|
|
1365
|
+
limit: int = 20,
|
|
1366
|
+
) -> dict[str, Any]:
|
|
1367
|
+
"""List Beads tasks (sync)."""
|
|
1368
|
+
return self._run(self._async_client.beads_tasks(status=status, limit=limit))
|
|
1369
|
+
|
|
1370
|
+
def beads_stats(self) -> dict[str, Any]:
|
|
1371
|
+
"""Get Beads integration statistics (sync)."""
|
|
1372
|
+
return self._run(self._async_client.beads_stats())
|
|
1373
|
+
|
|
1374
|
+
|
|
1375
|
+
# =============================================================================
|
|
1376
|
+
# Module-Level Convenience Functions
|
|
1377
|
+
# =============================================================================
|
|
1378
|
+
|
|
1379
|
+
# Global default client (lazily initialized)
|
|
1380
|
+
_default_client: MemoryClient | None = None
|
|
1381
|
+
_default_config: ClientConfig | None = None
|
|
1382
|
+
|
|
1383
|
+
|
|
1384
|
+
def configure(
|
|
1385
|
+
mode: str | ClientMode = ClientMode.LOCAL,
|
|
1386
|
+
db_path: str | Path | None = None,
|
|
1387
|
+
base_url: str | None = None,
|
|
1388
|
+
api_key: str | None = None,
|
|
1389
|
+
**kwargs: Any,
|
|
1390
|
+
) -> None:
|
|
1391
|
+
"""Configure the default client.
|
|
1392
|
+
|
|
1393
|
+
Call this before using module-level convenience functions.
|
|
1394
|
+
|
|
1395
|
+
Args:
|
|
1396
|
+
mode: Operating mode: "local" or "remote".
|
|
1397
|
+
db_path: Database path for local mode.
|
|
1398
|
+
base_url: API base URL for remote mode.
|
|
1399
|
+
api_key: API key for authentication.
|
|
1400
|
+
**kwargs: Additional ClientConfig parameters.
|
|
1401
|
+
"""
|
|
1402
|
+
global _default_config, _default_client
|
|
1403
|
+
|
|
1404
|
+
_default_config = ClientConfig(
|
|
1405
|
+
mode=ClientMode(mode) if isinstance(mode, str) else mode,
|
|
1406
|
+
db_path=db_path or str(default_db_path()),
|
|
1407
|
+
base_url=base_url or "http://127.0.0.1:8080",
|
|
1408
|
+
api_key=api_key,
|
|
1409
|
+
**kwargs,
|
|
1410
|
+
)
|
|
1411
|
+
_default_client = None # Reset client to use new config
|
|
1412
|
+
|
|
1413
|
+
|
|
1414
|
+
async def _get_client() -> MemoryClient:
|
|
1415
|
+
"""Get the default client, initializing if needed."""
|
|
1416
|
+
global _default_client
|
|
1417
|
+
|
|
1418
|
+
if _default_client is None:
|
|
1419
|
+
_default_client = MemoryClient(config=_default_config or ClientConfig())
|
|
1420
|
+
await _default_client.initialize()
|
|
1421
|
+
|
|
1422
|
+
return _default_client
|
|
1423
|
+
|
|
1424
|
+
|
|
1425
|
+
async def add(
|
|
1426
|
+
content: str,
|
|
1427
|
+
category: str | MemoryCategory,
|
|
1428
|
+
project: str | None = None,
|
|
1429
|
+
**kwargs: Any,
|
|
1430
|
+
) -> Memory:
|
|
1431
|
+
"""Add a new memory using the default client.
|
|
1432
|
+
|
|
1433
|
+
Args:
|
|
1434
|
+
content: The memory content text.
|
|
1435
|
+
category: Classification category.
|
|
1436
|
+
project: Project scope.
|
|
1437
|
+
**kwargs: Additional memory parameters.
|
|
1438
|
+
|
|
1439
|
+
Returns:
|
|
1440
|
+
The created Memory.
|
|
1441
|
+
|
|
1442
|
+
Example:
|
|
1443
|
+
```python
|
|
1444
|
+
from runtime_memory.sdk import add, configure
|
|
1445
|
+
|
|
1446
|
+
configure(mode="local")
|
|
1447
|
+
memory = await add("Use async/await for I/O", category="pattern")
|
|
1448
|
+
```
|
|
1449
|
+
"""
|
|
1450
|
+
client = await _get_client()
|
|
1451
|
+
return await client.add(content=content, category=category, project=project, **kwargs)
|
|
1452
|
+
|
|
1453
|
+
|
|
1454
|
+
async def search(
|
|
1455
|
+
query: str,
|
|
1456
|
+
limit: int = 10,
|
|
1457
|
+
project: str | None = None,
|
|
1458
|
+
**kwargs: Any,
|
|
1459
|
+
) -> list[SearchResult]:
|
|
1460
|
+
"""Search for relevant memories using the default client.
|
|
1461
|
+
|
|
1462
|
+
Args:
|
|
1463
|
+
query: Search query text.
|
|
1464
|
+
limit: Maximum number of results.
|
|
1465
|
+
project: Filter by project.
|
|
1466
|
+
**kwargs: Additional search parameters.
|
|
1467
|
+
|
|
1468
|
+
Returns:
|
|
1469
|
+
List of SearchResults.
|
|
1470
|
+
|
|
1471
|
+
Example:
|
|
1472
|
+
```python
|
|
1473
|
+
from runtime_memory.sdk import search, configure
|
|
1474
|
+
|
|
1475
|
+
configure(mode="local")
|
|
1476
|
+
results = await search("async patterns")
|
|
1477
|
+
```
|
|
1478
|
+
"""
|
|
1479
|
+
client = await _get_client()
|
|
1480
|
+
return await client.search(query=query, limit=limit, project=project, **kwargs)
|
|
1481
|
+
|
|
1482
|
+
|
|
1483
|
+
async def get_context(
|
|
1484
|
+
project: str | None = None,
|
|
1485
|
+
limit: int = 10,
|
|
1486
|
+
**kwargs: Any,
|
|
1487
|
+
) -> ContextResponse:
|
|
1488
|
+
"""Get formatted context using the default client.
|
|
1489
|
+
|
|
1490
|
+
Args:
|
|
1491
|
+
project: Project filter.
|
|
1492
|
+
limit: Maximum memories to include.
|
|
1493
|
+
**kwargs: Additional context parameters.
|
|
1494
|
+
|
|
1495
|
+
Returns:
|
|
1496
|
+
ContextResponse with formatted context.
|
|
1497
|
+
"""
|
|
1498
|
+
client = await _get_client()
|
|
1499
|
+
return await client.get_context(project=project, limit=limit, **kwargs)
|
|
1500
|
+
|
|
1501
|
+
|
|
1502
|
+
async def record_outcome(
|
|
1503
|
+
memory_ids: list[str] | str,
|
|
1504
|
+
outcome: str | Outcome,
|
|
1505
|
+
) -> list[Memory]:
|
|
1506
|
+
"""Record outcome feedback using the default client.
|
|
1507
|
+
|
|
1508
|
+
Args:
|
|
1509
|
+
memory_ids: Memory ID(s) to update.
|
|
1510
|
+
outcome: The outcome (worked, failed, partial).
|
|
1511
|
+
|
|
1512
|
+
Returns:
|
|
1513
|
+
List of updated memories.
|
|
1514
|
+
"""
|
|
1515
|
+
client = await _get_client()
|
|
1516
|
+
return await client.record_outcome(memory_ids=memory_ids, outcome=outcome)
|
|
1517
|
+
|
|
1518
|
+
|
|
1519
|
+
async def close_default_client() -> None:
|
|
1520
|
+
"""Close the default client if initialized."""
|
|
1521
|
+
global _default_client
|
|
1522
|
+
|
|
1523
|
+
if _default_client is not None:
|
|
1524
|
+
await _default_client.close()
|
|
1525
|
+
_default_client = None
|
|
1526
|
+
|
|
1527
|
+
|
|
1528
|
+
# =============================================================================
|
|
1529
|
+
# Beads Integration Convenience Functions
|
|
1530
|
+
# =============================================================================
|
|
1531
|
+
|
|
1532
|
+
|
|
1533
|
+
async def beads_sync(task_id: str | None = None) -> dict[str, Any]:
|
|
1534
|
+
"""Sync outcomes for completed Beads tasks.
|
|
1535
|
+
|
|
1536
|
+
Args:
|
|
1537
|
+
task_id: Optional specific task ID to sync.
|
|
1538
|
+
|
|
1539
|
+
Returns:
|
|
1540
|
+
Sync result dictionary.
|
|
1541
|
+
"""
|
|
1542
|
+
client = await _get_client()
|
|
1543
|
+
return await client.beads_sync(task_id=task_id)
|
|
1544
|
+
|
|
1545
|
+
|
|
1546
|
+
async def beads_context(
|
|
1547
|
+
task_id: str | None = None,
|
|
1548
|
+
limit: int = 10,
|
|
1549
|
+
) -> dict[str, Any]:
|
|
1550
|
+
"""Get unified context for a Beads task.
|
|
1551
|
+
|
|
1552
|
+
Args:
|
|
1553
|
+
task_id: Task ID (uses current task if None).
|
|
1554
|
+
limit: Maximum memories to include.
|
|
1555
|
+
|
|
1556
|
+
Returns:
|
|
1557
|
+
Context dictionary.
|
|
1558
|
+
"""
|
|
1559
|
+
client = await _get_client()
|
|
1560
|
+
return await client.beads_context(task_id=task_id, limit=limit)
|
|
1561
|
+
|
|
1562
|
+
|
|
1563
|
+
async def beads_tasks(
|
|
1564
|
+
status: str | None = None,
|
|
1565
|
+
limit: int = 20,
|
|
1566
|
+
) -> dict[str, Any]:
|
|
1567
|
+
"""List Beads tasks.
|
|
1568
|
+
|
|
1569
|
+
Args:
|
|
1570
|
+
status: Optional status filter.
|
|
1571
|
+
limit: Maximum tasks to return.
|
|
1572
|
+
|
|
1573
|
+
Returns:
|
|
1574
|
+
Dictionary with task list.
|
|
1575
|
+
"""
|
|
1576
|
+
client = await _get_client()
|
|
1577
|
+
return await client.beads_tasks(status=status, limit=limit)
|