crow-cli 0.1.28__tar.gz → 0.1.30__tar.gz
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.
- {crow_cli-0.1.28 → crow_cli-0.1.30}/PKG-INFO +2 -1
- {crow_cli-0.1.28 → crow_cli-0.1.30}/pyproject.toml +12 -1
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/agent/configure.py +4 -4
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/agent/default/defaults.py +2 -28
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/agent/main.py +6 -13
- crow_cli-0.1.30/src/crow_cli/agent/memory.py +164 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/agent/session.py +23 -23
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/cli/init_cmd.py +29 -39
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/cli/main.py +7 -7
- crow_cli-0.1.30/uv.lock +2930 -0
- crow_cli-0.1.28/src/crow_cli/agent/memory.py +0 -267
- crow_cli-0.1.28/uv.lock +0 -1548
- {crow_cli-0.1.28 → crow_cli-0.1.30}/.gitignore +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/.python-version +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/README.md +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/agent.json +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/crow-cli.spec +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/examples/mc_escher_loop.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/examples/quick_test.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/run_tests.sh +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/__init__.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/agent/__init__.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/agent/compact.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/agent/context.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/agent/default/__init__.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/agent/default/searxng_settings.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/agent/hooks.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/agent/llm.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/agent/logger.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/agent/mcp_client.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/agent/prompt.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/agent/react.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/agent/slash.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/agent/tools.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/agent_runner.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/cli/__init__.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/cli/install.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/client/__init__.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/client/main.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/src/crow_cli/client/terminal.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/test_send_request_error_handling.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/tests/__init__.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/tests/analyze_payload_cache_invalidation.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/tests/analyze_payload_deep.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/tests/conftest.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/tests/e2e/test_session_update_transmission.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/tests/integration/test_agent_init.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/tests/integration/test_auth_validation.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/tests/unit/test_client_terminal.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/tests/unit/test_compact.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/tests/unit/test_compact_last_messages_list_content.json +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/tests/unit/test_compact_last_messages_list_content.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/tests/unit/test_json_repair.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/tests/unit/test_request_logging.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/tests/unit/test_session.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/tests/unit/test_stream_processing.py +0 -0
- {crow_cli-0.1.28 → crow_cli-0.1.30}/tests/unit/test_uv_project_enforcement.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crow-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.30
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Project-URL: Homepage, https://github.com/crow-cli/crow-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/crow-cli/crow-cli
|
|
@@ -9,6 +9,7 @@ Project-URL: Documentation, https://github.com/crow-cli/crow-cli#readme
|
|
|
9
9
|
Requires-Python: >=3.14
|
|
10
10
|
Requires-Dist: agent-client-protocol>=0.9.0
|
|
11
11
|
Requires-Dist: coolname>=2.2.0
|
|
12
|
+
Requires-Dist: crow-memory>=0.1.30
|
|
12
13
|
Requires-Dist: directory-tree>=1.0.0
|
|
13
14
|
Requires-Dist: fastmcp>=2.14.5
|
|
14
15
|
Requires-Dist: httpx>=0.28.1
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "crow-cli"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.30"
|
|
4
4
|
description = "Add your description here"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14"
|
|
7
7
|
dependencies = [
|
|
8
8
|
"agent-client-protocol>=0.9.0",
|
|
9
9
|
"coolname>=2.2.0",
|
|
10
|
+
"crow-memory>=0.1.30",
|
|
10
11
|
"directory-tree>=1.0.0",
|
|
11
12
|
"fastmcp>=2.14.5",
|
|
12
13
|
"httpx>=0.28.1",
|
|
@@ -19,6 +20,16 @@ dependencies = [
|
|
|
19
20
|
"typer>=0.24.1",
|
|
20
21
|
]
|
|
21
22
|
|
|
23
|
+
[tool.uv.sources]
|
|
24
|
+
crow-memory = { path = "../crow-memory", editable = true }
|
|
25
|
+
torch = [{ index = "pytorch-cpu" }]
|
|
26
|
+
torchvision = [{ index = "pytorch-cpu" }]
|
|
27
|
+
|
|
28
|
+
[[tool.uv.index]]
|
|
29
|
+
name = "pytorch-cpu"
|
|
30
|
+
url = "https://download.pytorch.org/whl/cpu"
|
|
31
|
+
explicit = true
|
|
32
|
+
|
|
22
33
|
|
|
23
34
|
[project.urls]
|
|
24
35
|
Homepage = "https://github.com/crow-cli/crow-cli"
|
|
@@ -91,7 +91,7 @@ class LLMConfig:
|
|
|
91
91
|
class Config:
|
|
92
92
|
config_dir: Path
|
|
93
93
|
llm: LLMConfig = field(default_factory=LLMConfig)
|
|
94
|
-
|
|
94
|
+
memory_path: str = ""
|
|
95
95
|
mcp_servers: dict[str, Any] = field(default_factory=dict)
|
|
96
96
|
max_retries_per_step: int = 3
|
|
97
97
|
MAX_COMPACT_TOKENS: int = 190000
|
|
@@ -134,7 +134,7 @@ class Config:
|
|
|
134
134
|
_logger.info("No config.yaml found, returning bare Config")
|
|
135
135
|
return cls(
|
|
136
136
|
config_dir=target_dir,
|
|
137
|
-
|
|
137
|
+
memory_path=str(Path.home() / ".crow" / "memory.lance"),
|
|
138
138
|
)
|
|
139
139
|
|
|
140
140
|
with open(config_file) as f:
|
|
@@ -162,7 +162,7 @@ class Config:
|
|
|
162
162
|
)
|
|
163
163
|
|
|
164
164
|
# Parse overrides
|
|
165
|
-
|
|
165
|
+
memory_path = os.path.expanduser(parsed.get("memory_path") or "~/.crow/memory.lance")
|
|
166
166
|
overrides = {}
|
|
167
167
|
for key, typ in (
|
|
168
168
|
("max_retries_per_step", int),
|
|
@@ -184,7 +184,7 @@ class Config:
|
|
|
184
184
|
return cls(
|
|
185
185
|
config_dir=target_dir,
|
|
186
186
|
llm=llm,
|
|
187
|
-
|
|
187
|
+
memory_path=memory_path,
|
|
188
188
|
mcp_servers=mcp_servers,
|
|
189
189
|
system_prompt_path=system_prompt_path,
|
|
190
190
|
**overrides,
|
|
@@ -160,32 +160,6 @@ COMPOSE_YAML = """services:
|
|
|
160
160
|
volumes:
|
|
161
161
|
- ./searxng/:/etc/searxng
|
|
162
162
|
|
|
163
|
-
crow-memory:
|
|
164
|
-
image: ghcr.io/crow-cli/crow-memory:latest
|
|
165
|
-
# For local development, replace image with build:
|
|
166
|
-
# build:
|
|
167
|
-
# context: /path/to/crow-cli/crow-memory
|
|
168
|
-
# dockerfile: Dockerfile
|
|
169
|
-
restart: always
|
|
170
|
-
ports:
|
|
171
|
-
- "8901:8901"
|
|
172
|
-
environment:
|
|
173
|
-
- CROW_MEMORY_PATH=/data/memory.lance
|
|
174
|
-
- CROW_MEMORY_HOST=0.0.0.0
|
|
175
|
-
- CROW_MEMORY_PORT=8901
|
|
176
|
-
- CUDA_VISIBLE_DEVICES=
|
|
177
|
-
volumes:
|
|
178
|
-
- ./memory.lance:/data/memory.lance
|
|
179
|
-
# GPU acceleration — uncomment when you want hardware-accelerated embeddings.
|
|
180
|
-
# CPU mode is ~100ms text / ~2-3s image, which is fine for background agents.
|
|
181
|
-
# deploy:
|
|
182
|
-
# resources:
|
|
183
|
-
# reservations:
|
|
184
|
-
# devices:
|
|
185
|
-
# - driver: nvidia
|
|
186
|
-
# count: 1
|
|
187
|
-
# capabilities: [gpu]
|
|
188
|
-
|
|
189
163
|
litellm:
|
|
190
164
|
image: ghcr.io/berriai/litellm:main-v1.82.6-nightly
|
|
191
165
|
restart: always
|
|
@@ -214,8 +188,8 @@ mcpServers:
|
|
|
214
188
|
args:
|
|
215
189
|
- crow-mcp
|
|
216
190
|
|
|
217
|
-
#
|
|
218
|
-
|
|
191
|
+
# Memory store path (LanceDB, in-process)
|
|
192
|
+
memory_path: ~/.crow/memory.lance
|
|
219
193
|
|
|
220
194
|
# EXAMPLE PROVIDER
|
|
221
195
|
# providers:
|
|
@@ -36,13 +36,6 @@ Now you're guaranteed to have an API key and a base url to access an LLM, run th
|
|
|
36
36
|
uvx crow-cli init
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
# Start the memory service
|
|
40
|
-
crow-cli persists every session to a local [crow-memory](https://github.com/crow-cli/crow-cli/tree/main/crow-memory) service (LanceDB + ColBERT embeddings) running in Docker. The init wizard writes a `compose.yaml` for you:
|
|
41
|
-
```bash
|
|
42
|
-
cd ~/.crow && docker compose up -d
|
|
43
|
-
```
|
|
44
|
-
This starts crow-memory (port 8901) and optionally SearXNG for web search. Without it, sessions won't persist and the memory tools (`list_sessions`, `query_memory`, `query_session`) won't work.
|
|
45
|
-
|
|
46
39
|
Then restart your agent.
|
|
47
40
|
"""
|
|
48
41
|
|
|
@@ -172,7 +165,7 @@ class AcpAgent(Agent):
|
|
|
172
165
|
hooks if hooks is not None else [uv_project_hook]
|
|
173
166
|
)
|
|
174
167
|
self._logger = setup_logger(self._config.config_dir / "logs" / "crow-cli.log")
|
|
175
|
-
self.
|
|
168
|
+
self._memory_path = self._config.memory_path
|
|
176
169
|
self._exit_stack = AsyncExitStack()
|
|
177
170
|
self._agent_id: str | None = None
|
|
178
171
|
self._session_id: str | None = None # stripped version for ACP upstream
|
|
@@ -422,14 +415,14 @@ class AcpAgent(Agent):
|
|
|
422
415
|
|
|
423
416
|
try:
|
|
424
417
|
# Find the highest-indexed agent for this session
|
|
425
|
-
max_idx = AgentSession.get_max_agent_idx(session_id,
|
|
418
|
+
max_idx = AgentSession.get_max_agent_idx(session_id, memory_path=self._memory_path)
|
|
426
419
|
agent_id = f"{session_id}-{max_idx}"
|
|
427
420
|
self._logger.info(
|
|
428
421
|
"LOAD_SESSION: Step 1: Loading agent %s from DB", agent_id
|
|
429
422
|
)
|
|
430
423
|
session = AgentSession.load(
|
|
431
424
|
agent_id,
|
|
432
|
-
|
|
425
|
+
memory_path=self._memory_path,
|
|
433
426
|
)
|
|
434
427
|
self._logger.info("LOAD_SESSION: Step 1 complete: Agent loaded from DB")
|
|
435
428
|
|
|
@@ -513,7 +506,7 @@ class AcpAgent(Agent):
|
|
|
513
506
|
if self._session_id == session_id and self._agent_id:
|
|
514
507
|
agent_id = self._agent_id
|
|
515
508
|
else:
|
|
516
|
-
max_idx = AgentSession.get_max_agent_idx(session_id,
|
|
509
|
+
max_idx = AgentSession.get_max_agent_idx(session_id, memory_path=self._memory_path)
|
|
517
510
|
agent_id = f"{session_id}-{max_idx}"
|
|
518
511
|
|
|
519
512
|
# Initialize if not set
|
|
@@ -565,7 +558,7 @@ class AcpAgent(Agent):
|
|
|
565
558
|
if self._session_id == session_id and self._agent_id:
|
|
566
559
|
agent_id = self._agent_id
|
|
567
560
|
else:
|
|
568
|
-
max_idx = AgentSession.get_max_agent_idx(session_id,
|
|
561
|
+
max_idx = AgentSession.get_max_agent_idx(session_id, memory_path=self._memory_path)
|
|
569
562
|
agent_id = f"{session_id}-{max_idx}"
|
|
570
563
|
|
|
571
564
|
async def _execute_turn() -> PromptResponse:
|
|
@@ -786,7 +779,7 @@ class AcpAgent(Agent):
|
|
|
786
779
|
self._logger.info("Listing sessions for working directory: %s", cwd)
|
|
787
780
|
if cwd is None:
|
|
788
781
|
return ListSessionsResponse(sessions=[], next_cursor=None)
|
|
789
|
-
sessions_info = get_session_by_cwd(cwd, self.
|
|
782
|
+
sessions_info = get_session_by_cwd(cwd, self._memory_path)
|
|
790
783
|
sessions = [SessionInfo(**session) for session in sessions_info]
|
|
791
784
|
return ListSessionsResponse(sessions=sessions, next_cursor=None)
|
|
792
785
|
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""In-process memory layer for crow-cli.
|
|
2
|
+
|
|
3
|
+
Wraps crow-memory's MemoryStore (LanceDB + ColBERT/ColQwen2) directly — no
|
|
4
|
+
HTTP, no service, no retry logic. The embedders are loaded once per process
|
|
5
|
+
via a singleton; the model weights are mmap'd and shared with crow-mcp by the
|
|
6
|
+
kernel's page cache.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
log = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
DEFAULT_MEMORY_PATH = str(Path.home() / ".crow" / "memory.lance")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MemoryServiceError(Exception):
|
|
18
|
+
"""Raised when a memory operation fails (e.g. agent not found)."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, status: int, detail: str):
|
|
21
|
+
self.status = status
|
|
22
|
+
self.detail = detail
|
|
23
|
+
super().__init__(f"memory error {status}: {detail}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MemoryClient:
|
|
27
|
+
"""In-process adapter over MemoryStore.
|
|
28
|
+
|
|
29
|
+
Same interface as the old HTTP client so session.py / main.py don't change
|
|
30
|
+
shape — but backed by a direct LanceDB connection. The `base_url` param is
|
|
31
|
+
now a filesystem path (kept as positional for compat during transition).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, path: str = DEFAULT_MEMORY_PATH, **_kwargs):
|
|
35
|
+
from crow_memory import get_store
|
|
36
|
+
self._store = get_store(path)
|
|
37
|
+
|
|
38
|
+
def close(self):
|
|
39
|
+
pass # nothing to close; store is process-global
|
|
40
|
+
|
|
41
|
+
def __enter__(self):
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
def __exit__(self, *exc):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
# ---- agents ----
|
|
48
|
+
|
|
49
|
+
def create_agent(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
agent_id: str,
|
|
53
|
+
session_id: str,
|
|
54
|
+
agent_idx: int = 1,
|
|
55
|
+
cwd: str = "/tmp",
|
|
56
|
+
prompt_id: str | None = None,
|
|
57
|
+
prompt_args: dict | None = None,
|
|
58
|
+
system_prompt: str = "",
|
|
59
|
+
tool_definitions: list[dict] | None = None,
|
|
60
|
+
request_params: dict | None = None,
|
|
61
|
+
model_identifier: str = "",
|
|
62
|
+
initial_messages: list[dict] | None = None,
|
|
63
|
+
) -> dict:
|
|
64
|
+
agent = self._store.add_agent({
|
|
65
|
+
"agent_id": agent_id,
|
|
66
|
+
"session_id": session_id,
|
|
67
|
+
"agent_idx": agent_idx,
|
|
68
|
+
"cwd": cwd,
|
|
69
|
+
"prompt_id": prompt_id,
|
|
70
|
+
"prompt_args": prompt_args or {},
|
|
71
|
+
"system_prompt": system_prompt,
|
|
72
|
+
"tool_definitions": tool_definitions or [],
|
|
73
|
+
"request_params": request_params or {},
|
|
74
|
+
"model_identifier": model_identifier,
|
|
75
|
+
})
|
|
76
|
+
for m in (initial_messages or []):
|
|
77
|
+
if m.get("role") != "system":
|
|
78
|
+
self._store.add_message(agent_id, m)
|
|
79
|
+
return agent
|
|
80
|
+
|
|
81
|
+
def load(self, agent_id: str, hydrate: bool = False) -> tuple[dict, list[dict]]:
|
|
82
|
+
agent = self._store.get_agent(agent_id)
|
|
83
|
+
if agent is None:
|
|
84
|
+
raise MemoryServiceError(404, f"agent '{agent_id}' not found")
|
|
85
|
+
messages = self._store.load_messages(agent_id, hydrate_images=hydrate)
|
|
86
|
+
return agent, messages
|
|
87
|
+
|
|
88
|
+
def list_agents(self, session_id: str | None = None) -> list[dict]:
|
|
89
|
+
return self._store.list_agents(session_id=session_id)
|
|
90
|
+
|
|
91
|
+
# ---- messages ----
|
|
92
|
+
|
|
93
|
+
def add_message(self, agent_id: str, message: dict, usage: dict | None = None) -> dict:
|
|
94
|
+
return self._store.add_message(agent_id, message, usage)
|
|
95
|
+
|
|
96
|
+
def save_messages(self, agent_id: str, messages: list[dict]) -> dict:
|
|
97
|
+
ids = [self._store.add_message(agent_id, m)["id"] for m in messages]
|
|
98
|
+
return {"agent_id": agent_id, "ids": ids, "count": len(ids)}
|
|
99
|
+
|
|
100
|
+
def query_messages(
|
|
101
|
+
self,
|
|
102
|
+
*,
|
|
103
|
+
session_id: str | None = None,
|
|
104
|
+
agent_id: str | None = None,
|
|
105
|
+
agent_idx: int | None = None,
|
|
106
|
+
roles: list[str] | None = None,
|
|
107
|
+
after: str | None = None,
|
|
108
|
+
before: str | None = None,
|
|
109
|
+
order: str = "asc",
|
|
110
|
+
limit: int = 1_000_000,
|
|
111
|
+
offset: int = 0,
|
|
112
|
+
) -> list[dict]:
|
|
113
|
+
return self._store.query_messages(
|
|
114
|
+
session_id=session_id, agent_id=agent_id, agent_idx=agent_idx,
|
|
115
|
+
roles=roles, after=after, before=before, order=order,
|
|
116
|
+
limit=limit, offset=offset,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# ---- sessions ----
|
|
120
|
+
|
|
121
|
+
def get_max_agent_idx(self, session_id: str) -> int:
|
|
122
|
+
return self._store.get_max_agent_idx(session_id)
|
|
123
|
+
|
|
124
|
+
def list_sessions(self, limit: int = 50, offset: int = 0) -> list[dict]:
|
|
125
|
+
return self._store.list_sessions(limit=limit, offset=offset)
|
|
126
|
+
|
|
127
|
+
# ---- prompts ----
|
|
128
|
+
|
|
129
|
+
def lookup_or_create_prompt(self, template: str, name: str = "crow-default") -> str:
|
|
130
|
+
result = self._store.lookup_or_create_prompt(template, name)
|
|
131
|
+
return result["id"]
|
|
132
|
+
|
|
133
|
+
def get_prompt(self, prompt_id: str) -> dict:
|
|
134
|
+
p = self._store.get_prompt(prompt_id)
|
|
135
|
+
if p is None:
|
|
136
|
+
raise MemoryServiceError(404, f"prompt '{prompt_id}' not found")
|
|
137
|
+
return p
|
|
138
|
+
|
|
139
|
+
# ---- images ----
|
|
140
|
+
|
|
141
|
+
def get_image(self, image_id: str) -> tuple[bytes, str]:
|
|
142
|
+
img = self._store.get_image(image_id)
|
|
143
|
+
if img is None:
|
|
144
|
+
raise MemoryServiceError(404, f"image '{image_id}' not found")
|
|
145
|
+
return img["data"], img["mime"]
|
|
146
|
+
|
|
147
|
+
# ---- search ----
|
|
148
|
+
|
|
149
|
+
def search(
|
|
150
|
+
self,
|
|
151
|
+
query: str | None = None,
|
|
152
|
+
modality: str = "text",
|
|
153
|
+
filters: dict | None = None,
|
|
154
|
+
limit: int = 10,
|
|
155
|
+
query_image: bytes | None = None,
|
|
156
|
+
) -> dict:
|
|
157
|
+
out = {"messages": [], "images": []}
|
|
158
|
+
if modality in ("text", "both") and query:
|
|
159
|
+
out["messages"] = self._store.search_messages(query, filters, limit)
|
|
160
|
+
if modality in ("image", "both"):
|
|
161
|
+
out["images"] = self._store.search_images(
|
|
162
|
+
query=query, query_image=query_image, limit=limit
|
|
163
|
+
)
|
|
164
|
+
return out
|
|
@@ -16,19 +16,19 @@ from typing import Any
|
|
|
16
16
|
import yaml
|
|
17
17
|
from coolname import generate_slug
|
|
18
18
|
|
|
19
|
-
from crow_cli.agent.memory import
|
|
19
|
+
from crow_cli.agent.memory import DEFAULT_MEMORY_PATH, MemoryClient, MemoryServiceError
|
|
20
20
|
from crow_cli.agent.prompt import render_template
|
|
21
21
|
from crow_cli.agent.context import get_directory_tree
|
|
22
22
|
|
|
23
23
|
from crow_cli.agent.configure import Config
|
|
24
24
|
|
|
25
|
-
def get_session_by_cwd(cwd,
|
|
25
|
+
def get_session_by_cwd(cwd, memory_path=DEFAULT_MEMORY_PATH):
|
|
26
26
|
"""
|
|
27
|
-
Lookup agents by working directory via the
|
|
27
|
+
Lookup agents by working directory via the in-process memory store.
|
|
28
28
|
|
|
29
29
|
Returns list of session info dicts with session_id, title, updated_at.
|
|
30
30
|
"""
|
|
31
|
-
client = MemoryClient(
|
|
31
|
+
client = MemoryClient(memory_path)
|
|
32
32
|
try:
|
|
33
33
|
result = []
|
|
34
34
|
for agent in client.list_agents():
|
|
@@ -80,12 +80,12 @@ def get_coolname() -> str:
|
|
|
80
80
|
def lookup_or_create_prompt(
|
|
81
81
|
template: str,
|
|
82
82
|
name: str,
|
|
83
|
-
|
|
83
|
+
memory_path: str = DEFAULT_MEMORY_PATH,
|
|
84
84
|
) -> str:
|
|
85
85
|
"""
|
|
86
86
|
Lookup existing prompt by template content, or create new one if not found.
|
|
87
87
|
"""
|
|
88
|
-
client = MemoryClient(
|
|
88
|
+
client = MemoryClient(memory_path)
|
|
89
89
|
try:
|
|
90
90
|
return client.lookup_or_create_prompt(template, name)
|
|
91
91
|
finally:
|
|
@@ -94,7 +94,7 @@ def lookup_or_create_prompt(
|
|
|
94
94
|
|
|
95
95
|
class AgentSession:
|
|
96
96
|
"""
|
|
97
|
-
Manages conversation state and persistence via the
|
|
97
|
+
Manages conversation state and persistence via the in-process memory store (LanceDB).
|
|
98
98
|
|
|
99
99
|
agent_id = "{session_id}-{agent_idx}" is the key.
|
|
100
100
|
session_id is derived for ACP upstream routing only.
|
|
@@ -105,13 +105,13 @@ class AgentSession:
|
|
|
105
105
|
agent_id: str,
|
|
106
106
|
session_id: str,
|
|
107
107
|
agent_idx: int = 0,
|
|
108
|
-
|
|
108
|
+
memory_path: str = DEFAULT_MEMORY_PATH,
|
|
109
109
|
cwd: str = "/tmp",
|
|
110
110
|
):
|
|
111
111
|
self.agent_id = agent_id
|
|
112
112
|
self.session_id = session_id
|
|
113
113
|
self.agent_idx = agent_idx
|
|
114
|
-
self.
|
|
114
|
+
self.memory_path = memory_path
|
|
115
115
|
self.cwd = cwd
|
|
116
116
|
self.messages: list[dict] = []
|
|
117
117
|
self._client = None
|
|
@@ -119,9 +119,9 @@ class AgentSession:
|
|
|
119
119
|
|
|
120
120
|
@property
|
|
121
121
|
def client(self) -> MemoryClient:
|
|
122
|
-
"""Lazy-load the
|
|
122
|
+
"""Lazy-load the in-process memory client."""
|
|
123
123
|
if self._client is None:
|
|
124
|
-
self._client = MemoryClient(self.
|
|
124
|
+
self._client = MemoryClient(self.memory_path)
|
|
125
125
|
return self._client
|
|
126
126
|
|
|
127
127
|
def add_message(self, msg: dict, usage: dict | None = None):
|
|
@@ -190,14 +190,14 @@ class AgentSession:
|
|
|
190
190
|
tool_definitions: list[dict],
|
|
191
191
|
request_params: dict[str, Any],
|
|
192
192
|
model_identifier: str,
|
|
193
|
-
|
|
193
|
+
memory_path: str = DEFAULT_MEMORY_PATH,
|
|
194
194
|
cwd: str = "/tmp",
|
|
195
195
|
agent_idx: int = 1,
|
|
196
196
|
session_id: str | None = None,
|
|
197
197
|
initial_messages: list[dict[str, Any]] | None = None,
|
|
198
198
|
) -> "AgentSession":
|
|
199
199
|
"""Factory method to create a new agent session."""
|
|
200
|
-
client = MemoryClient(
|
|
200
|
+
client = MemoryClient(memory_path)
|
|
201
201
|
|
|
202
202
|
# Load and render prompt
|
|
203
203
|
try:
|
|
@@ -228,7 +228,7 @@ class AgentSession:
|
|
|
228
228
|
client.close()
|
|
229
229
|
|
|
230
230
|
# Build session instance
|
|
231
|
-
session = cls(agent_id, session_id, agent_idx,
|
|
231
|
+
session = cls(agent_id, session_id, agent_idx, memory_path, cwd=cwd)
|
|
232
232
|
session.model_identifier = model_identifier
|
|
233
233
|
session.tools = tool_definitions
|
|
234
234
|
session.request_params = request_params
|
|
@@ -251,10 +251,10 @@ class AgentSession:
|
|
|
251
251
|
def get_max_agent_idx(
|
|
252
252
|
cls,
|
|
253
253
|
session_id: str,
|
|
254
|
-
|
|
254
|
+
memory_path: str = DEFAULT_MEMORY_PATH,
|
|
255
255
|
) -> int:
|
|
256
256
|
"""Return the highest agent_idx for a given session_id."""
|
|
257
|
-
client = MemoryClient(
|
|
257
|
+
client = MemoryClient(memory_path)
|
|
258
258
|
try:
|
|
259
259
|
return client.get_max_agent_idx(session_id)
|
|
260
260
|
finally:
|
|
@@ -265,10 +265,10 @@ class AgentSession:
|
|
|
265
265
|
cls,
|
|
266
266
|
limit: int = 50,
|
|
267
267
|
offset: int = 0,
|
|
268
|
-
|
|
268
|
+
memory_path: str = DEFAULT_MEMORY_PATH,
|
|
269
269
|
) -> list[dict]:
|
|
270
270
|
"""List sessions ordered by most-recent message activity (desc)."""
|
|
271
|
-
client = MemoryClient(
|
|
271
|
+
client = MemoryClient(memory_path)
|
|
272
272
|
try:
|
|
273
273
|
return client.list_sessions(limit=limit, offset=offset)
|
|
274
274
|
finally:
|
|
@@ -278,10 +278,10 @@ class AgentSession:
|
|
|
278
278
|
def load(
|
|
279
279
|
cls,
|
|
280
280
|
agent_id: str,
|
|
281
|
-
|
|
281
|
+
memory_path: str = DEFAULT_MEMORY_PATH,
|
|
282
282
|
) -> "AgentSession":
|
|
283
283
|
"""Factory method to load existing agent session from the service."""
|
|
284
|
-
client = MemoryClient(
|
|
284
|
+
client = MemoryClient(memory_path)
|
|
285
285
|
try:
|
|
286
286
|
agent, messages = client.load(agent_id, hydrate=True)
|
|
287
287
|
except MemoryServiceError as e:
|
|
@@ -295,7 +295,7 @@ class AgentSession:
|
|
|
295
295
|
agent_id=agent["agent_id"],
|
|
296
296
|
session_id=agent["session_id"],
|
|
297
297
|
agent_idx=agent["agent_idx"],
|
|
298
|
-
|
|
298
|
+
memory_path=memory_path,
|
|
299
299
|
cwd=agent["cwd"],
|
|
300
300
|
)
|
|
301
301
|
session.model_identifier = agent["model_identifier"]
|
|
@@ -446,7 +446,7 @@ def make_agent_session(
|
|
|
446
446
|
else:
|
|
447
447
|
template = config.system_prompt
|
|
448
448
|
prompt_id = lookup_or_create_prompt(
|
|
449
|
-
template, name="crow-default",
|
|
449
|
+
template, name="crow-default", memory_path=config.memory_path
|
|
450
450
|
)
|
|
451
451
|
skills = get_skills(config.config_dir / "skills")
|
|
452
452
|
|
|
@@ -471,7 +471,7 @@ def make_agent_session(
|
|
|
471
471
|
tool_definitions=tools,
|
|
472
472
|
request_params={"temperature": 0.2},
|
|
473
473
|
model_identifier=model_id,
|
|
474
|
-
|
|
474
|
+
memory_path=config.memory_path,
|
|
475
475
|
cwd=cwd,
|
|
476
476
|
agent_idx=agent_idx,
|
|
477
477
|
session_id=session_id,
|
|
@@ -285,8 +285,8 @@ def run_init(config_dir: Path, yes: bool = False):
|
|
|
285
285
|
# =========================================================================
|
|
286
286
|
console.print("\n[bold cyan]═══ Step 3: Review ═══[/bold cyan]\n")
|
|
287
287
|
|
|
288
|
-
|
|
289
|
-
console.print(f"[dim]
|
|
288
|
+
memory_path = str(Path.home() / ".crow" / "memory.lance")
|
|
289
|
+
console.print(f"[dim]Memory store: {memory_path}[/dim]")
|
|
290
290
|
|
|
291
291
|
if providers:
|
|
292
292
|
p_table = Table(title="Providers", show_header=True)
|
|
@@ -309,11 +309,11 @@ def run_init(config_dir: Path, yes: bool = False):
|
|
|
309
309
|
s_table.add_column("Service", style="cyan")
|
|
310
310
|
s_table.add_column("Status", style="green")
|
|
311
311
|
s_table.add_row("SearXNG", "✓ Docker" if setup_searxng else "✗ Skip")
|
|
312
|
-
s_table.add_row("Memory", "
|
|
312
|
+
s_table.add_row("Memory", "in-process LanceDB")
|
|
313
313
|
console.print(s_table)
|
|
314
314
|
|
|
315
315
|
console.print(f"\n[dim]Config directory: {config_dir}[/dim]")
|
|
316
|
-
console.print(f"[dim]Memory
|
|
316
|
+
console.print(f"[dim]Memory store: {memory_path}[/dim]")
|
|
317
317
|
|
|
318
318
|
if not yes and not Confirm.ask("\nLooks good?", default=True):
|
|
319
319
|
console.print("[red]Aborted. No files were written.[/red]")
|
|
@@ -345,7 +345,7 @@ def run_init(config_dir: Path, yes: bool = False):
|
|
|
345
345
|
"args": ["crow-mcp"],
|
|
346
346
|
}
|
|
347
347
|
},
|
|
348
|
-
"
|
|
348
|
+
"memory_path": memory_path,
|
|
349
349
|
"providers": providers,
|
|
350
350
|
"models": models,
|
|
351
351
|
"MAX_COMPACT_TOKENS": 190000,
|
|
@@ -384,19 +384,15 @@ def run_init(config_dir: Path, yes: bool = False):
|
|
|
384
384
|
available_services = compose_template.get("services", {})
|
|
385
385
|
active_services: dict[str, Any] = {}
|
|
386
386
|
|
|
387
|
-
# crow-memory is always included — it's the persistence layer
|
|
388
|
-
if "crow-memory" in available_services:
|
|
389
|
-
active_services["crow-memory"] = available_services["crow-memory"]
|
|
390
|
-
|
|
391
387
|
if setup_searxng and "searxng" in available_services:
|
|
392
388
|
active_services["searxng"] = available_services["searxng"]
|
|
393
389
|
|
|
390
|
+
compose_file = config_dir / "compose.yaml"
|
|
394
391
|
if active_services:
|
|
395
392
|
compose_data: dict[str, Any] = {
|
|
396
393
|
"services": active_services,
|
|
397
394
|
"volumes": compose_template.get("volumes", {}),
|
|
398
395
|
}
|
|
399
|
-
compose_file = config_dir / "compose.yaml"
|
|
400
396
|
with open(compose_file, "w") as f:
|
|
401
397
|
yaml.dump(compose_data, f, default_flow_style=False, sort_keys=False)
|
|
402
398
|
console.print(f"[green]✓[/green] Written {compose_file}")
|
|
@@ -407,20 +403,17 @@ def run_init(config_dir: Path, yes: bool = False):
|
|
|
407
403
|
# =========================================================================
|
|
408
404
|
# STEP 5: Start services (just instructions)
|
|
409
405
|
# =========================================================================
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
title="[yellow]Start " + " + ".join(services_list) + "[/yellow]",
|
|
421
|
-
border_style="yellow",
|
|
406
|
+
if active_services:
|
|
407
|
+
console.print("\n[bold cyan]═══ Step 5: Start Services ═══[/bold cyan]\n")
|
|
408
|
+
services_list = list(active_services.keys())
|
|
409
|
+
console.print(
|
|
410
|
+
Panel(
|
|
411
|
+
f"[bold white]cd {config_dir} && docker compose up -d[/bold white]\n\n"
|
|
412
|
+
f"[dim]Starts: {', '.join(services_list)}[/dim]",
|
|
413
|
+
title="[yellow]Start " + " + ".join(services_list) + "[/yellow]",
|
|
414
|
+
border_style="yellow",
|
|
415
|
+
)
|
|
422
416
|
)
|
|
423
|
-
)
|
|
424
417
|
|
|
425
418
|
# =========================================================================
|
|
426
419
|
# Done
|
|
@@ -429,22 +422,19 @@ def run_init(config_dir: Path, yes: bool = False):
|
|
|
429
422
|
system_prompt_dir = config_dir / "prompts"
|
|
430
423
|
|
|
431
424
|
console.print()
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
border_style="green",
|
|
446
|
-
)
|
|
447
|
-
)
|
|
425
|
+
done_lines = [
|
|
426
|
+
"[bold green]✓ Configuration complete![/bold green]\n",
|
|
427
|
+
f"Config: [cyan]{config_file}[/cyan]",
|
|
428
|
+
f"Memory: [cyan]{config_dir / 'memory.lance'}[/cyan] [dim](in-process, no service)[/dim]",
|
|
429
|
+
f"Logs: [cyan]{config_logs}[/cyan]",
|
|
430
|
+
f"Prompt: [cyan]{system_prompt_dir}/system_prompt.jinja2[/cyan]",
|
|
431
|
+
f"Secrets: [cyan]{env_file}[/cyan]",
|
|
432
|
+
]
|
|
433
|
+
if active_services:
|
|
434
|
+
done_lines.append(f"Compose: [cyan]{compose_file}[/cyan]")
|
|
435
|
+
done_lines.append(f"\n[dim]Start services:[/dim] [bold red]cd {config_dir} && docker compose up -d[/bold red]")
|
|
436
|
+
done_lines.append(f'\n[dim]Test:[/dim] [bold white]crow-cli run "hey"[/bold white]')
|
|
437
|
+
console.print(Panel.fit("\n".join(done_lines), border_style="green"))
|
|
448
438
|
|
|
449
439
|
|
|
450
440
|
# For typer integration
|