crow-cli 0.1.24__tar.gz → 0.1.26__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.24 → crow_cli-0.1.26}/PKG-INFO +2 -2
- {crow_cli-0.1.24 → crow_cli-0.1.26}/README.md +1 -1
- {crow_cli-0.1.24 → crow_cli-0.1.26}/pyproject.toml +1 -1
- {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/configure.py +18 -3
- {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/default/defaults.py +17 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/llm.py +2 -1
- {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/main.py +30 -3
- {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/mcp_client.py +14 -3
- {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/react.py +107 -16
- {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/session.py +65 -6
- {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/tools.py +418 -0
- crow_cli-0.1.26/src/crow_cli/cli/install.py +243 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/cli/main.py +53 -14
- {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/client/main.py +137 -3
- crow_cli-0.1.26/src/crow_cli/client/terminal.py +218 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/tests/analyze_payload_cache_invalidation.py +37 -17
- {crow_cli-0.1.24 → crow_cli-0.1.26}/tests/analyze_payload_deep.py +33 -14
- {crow_cli-0.1.24 → crow_cli-0.1.26}/tests/conftest.py +50 -0
- {crow_cli-0.1.24/tests → crow_cli-0.1.26/tests/e2e}/test_live_llm_integration.py +12 -13
- crow_cli-0.1.26/tests/e2e/test_session_update_transmission.py +251 -0
- crow_cli-0.1.26/tests/integration/test_agent_init.py +65 -0
- {crow_cli-0.1.24/tests → crow_cli-0.1.26/tests/integration}/test_auth_validation.py +2 -1
- crow_cli-0.1.26/tests/unit/test_client_terminal.py +189 -0
- crow_cli-0.1.26/tests/unit/test_compact.py +157 -0
- {crow_cli-0.1.24/tests → crow_cli-0.1.26/tests/unit}/test_persistence_integrity.py +17 -17
- crow_cli-0.1.26/tests/unit/test_request_logging.py +82 -0
- crow_cli-0.1.26/tests/unit/test_stream_processing.py +337 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/tests/unit/test_uv_project_enforcement.py +19 -9
- {crow_cli-0.1.24 → crow_cli-0.1.26}/uv.lock +1 -1
- crow_cli-0.1.24/tests/test_acp_token_loss.py +0 -548
- crow_cli-0.1.24/tests/test_agent_init.py +0 -82
- crow_cli-0.1.24/tests/test_payload_integrity.py +0 -262
- crow_cli-0.1.24/tests/test_session_update_transmission.py +0 -546
- crow_cli-0.1.24/tests/test_token_loss_detection.py +0 -572
- crow_cli-0.1.24/tests/test_token_loss_reproduction.py +0 -259
- crow_cli-0.1.24/tests/unit/test_compact.py +0 -187
- {crow_cli-0.1.24 → crow_cli-0.1.26}/.gitignore +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/.python-version +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/agent.json +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/crow-cli.spec +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/examples/mc_escher_loop.py +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/examples/quick_test.py +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/run_tests.sh +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/__init__.py +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/__init__.py +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/compact.py +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/context.py +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/db.py +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/default/__init__.py +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/default/searxng_settings.py +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/hooks.py +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/logger.py +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/prompt.py +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/slash.py +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent_runner.py +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/cli/__init__.py +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/cli/init_cmd.py +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/client/__init__.py +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/test_send_request_error_handling.py +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/tests/__init__.py +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/tests/unit/test_compact_last_messages_list_content.json +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/tests/unit/test_compact_last_messages_list_content.py +0 -0
- {crow_cli-0.1.24/tests → crow_cli-0.1.26/tests/unit}/test_json_repair.py +0 -0
- {crow_cli-0.1.24 → crow_cli-0.1.26}/tests/unit/test_session.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.26
|
|
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
|
|
@@ -594,4 +594,4 @@ If you see errors about malformed tool call arguments:
|
|
|
594
594
|
|
|
595
595
|
## License
|
|
596
596
|
|
|
597
|
-
|
|
597
|
+
MIT
|
|
@@ -21,6 +21,7 @@ from crow_cli.agent.default import (
|
|
|
21
21
|
SEARXNG_SETTINGS_YML,
|
|
22
22
|
SYSTEM_PROMPT,
|
|
23
23
|
)
|
|
24
|
+
from crow_cli.agent.logger import setup_logger
|
|
24
25
|
|
|
25
26
|
ENV_PATTERN = re.compile(r"\$\{([^}]+)\}")
|
|
26
27
|
|
|
@@ -95,16 +96,18 @@ class Config:
|
|
|
95
96
|
max_retries_per_step: int = 3
|
|
96
97
|
MAX_COMPACT_TOKENS: int = 190000
|
|
97
98
|
MAX_TOKENS: int = 38192
|
|
99
|
+
TEMPERATURE: float = 0.6
|
|
98
100
|
chunk_log: bool = False # Write every raw chunk to JSONL for debugging
|
|
99
101
|
system_prompt: str = SYSTEM_PROMPT
|
|
100
|
-
|
|
102
|
+
system_prompt_path: Path | None = None
|
|
101
103
|
@property
|
|
102
104
|
def log_path(self) -> str:
|
|
103
105
|
return str(self.config_dir / "logs" / "crow-cli.log")
|
|
104
106
|
|
|
105
107
|
def get_builtin_mcp_config(self) -> dict[str, Any]:
|
|
106
108
|
"""Return MCP config dict in FastMCP format."""
|
|
107
|
-
|
|
109
|
+
cfg = {"mcpServers": dict(self.mcp_servers or {})}
|
|
110
|
+
return cfg
|
|
108
111
|
|
|
109
112
|
@property
|
|
110
113
|
def is_configured(self) -> bool:
|
|
@@ -114,6 +117,7 @@ class Config:
|
|
|
114
117
|
@classmethod
|
|
115
118
|
def load(cls, config_dir: Path | None = None) -> "Config":
|
|
116
119
|
target_dir = get_default_config_dir(config_dir)
|
|
120
|
+
_logger = setup_logger(target_dir / "logs" / "crow-cli.log")
|
|
117
121
|
|
|
118
122
|
# Write defaults if nothing exists yet
|
|
119
123
|
if not (target_dir / "config.yaml").exists():
|
|
@@ -127,6 +131,7 @@ class Config:
|
|
|
127
131
|
# If no config.yaml, return a bare Config
|
|
128
132
|
config_file = target_dir / "config.yaml"
|
|
129
133
|
if not config_file.exists():
|
|
134
|
+
_logger.info("No config.yaml found, returning bare Config")
|
|
130
135
|
return cls(
|
|
131
136
|
config_dir=target_dir,
|
|
132
137
|
db_uri=os.getenv("DATABASE_PATH", str(target_dir / "crow.db")),
|
|
@@ -135,7 +140,9 @@ class Config:
|
|
|
135
140
|
with open(config_file) as f:
|
|
136
141
|
raw = yaml.safe_load(f) or {}
|
|
137
142
|
|
|
143
|
+
_logger.info("RAW config.yaml mcpServers: %s", raw.get("mcpServers", {}))
|
|
138
144
|
parsed = resolve_env_vars(raw)
|
|
145
|
+
_logger.info("PARSED config.yaml mcpServers after resolve_env_vars: %s", parsed.get("mcpServers", {}))
|
|
139
146
|
|
|
140
147
|
# Parse providers
|
|
141
148
|
llm = LLMConfig()
|
|
@@ -163,16 +170,24 @@ class Config:
|
|
|
163
170
|
("max_retries_per_step", int),
|
|
164
171
|
("MAX_COMPACT_TOKENS", int),
|
|
165
172
|
("MAX_TOKENS", int),
|
|
173
|
+
("TEMPERATURE", float),
|
|
166
174
|
):
|
|
167
175
|
if key in parsed:
|
|
168
176
|
overrides[key] = typ(parsed[key])
|
|
169
177
|
if "chunk_log" in parsed:
|
|
170
178
|
overrides["chunk_log"] = bool(parsed["chunk_log"])
|
|
171
179
|
|
|
180
|
+
mcp_servers = parsed.get("mcpServers", {})
|
|
181
|
+
_logger.info("FINAL mcp_servers stored in Config: %s", mcp_servers)
|
|
182
|
+
system_prompt_path = None
|
|
183
|
+
if "system_prompt_path" in parsed:
|
|
184
|
+
system_prompt_path = Path(parsed["system_prompt_path"])
|
|
185
|
+
|
|
172
186
|
return cls(
|
|
173
187
|
config_dir=target_dir,
|
|
174
188
|
llm=llm,
|
|
175
189
|
db_uri=db_uri,
|
|
176
|
-
mcp_servers=
|
|
190
|
+
mcp_servers=mcp_servers,
|
|
191
|
+
system_prompt_path=system_prompt_path,
|
|
177
192
|
**overrides,
|
|
178
193
|
)
|
|
@@ -123,7 +123,12 @@ AGENTS.md:
|
|
|
123
123
|
* Use `AGENTS.md` under the repository root as your persistent memory for repository-specific knowledge and context.
|
|
124
124
|
* Add important insights, patterns, and learnings to this file to improve future task performance.
|
|
125
125
|
* This repository skill is automatically loaded for every conversation and helps maintain context across sessions.
|
|
126
|
+
* You can use query_memory to access information from previous sessions.
|
|
126
127
|
</MEMORY>
|
|
128
|
+
|
|
129
|
+
<QUERY_MEMORY>
|
|
130
|
+
When another agent finishes and you get a notification, DO NOT just sit there wondering what happened. Call `query_memory(session_id=<their_sid>, limit=1)` and actually read the damn message. That is how you know what they did. If you just need the latest answer, `limit=1` is your friend. If you need context, use `query` and `context`. I PITY THE FOOL WHO IGNORES THE CONTEXT OF PREVIOUS AGENTS.
|
|
131
|
+
</QUERY_MEMORY>
|
|
127
132
|
"""
|
|
128
133
|
|
|
129
134
|
COMPOSE_YAML = """services:
|
|
@@ -139,6 +144,18 @@ COMPOSE_YAML = """services:
|
|
|
139
144
|
volumes:
|
|
140
145
|
- ./searxng/:/etc/searxng
|
|
141
146
|
|
|
147
|
+
litellm:
|
|
148
|
+
image: ghcr.io/berriai/litellm:main-v1.82.6-nightly
|
|
149
|
+
restart: always
|
|
150
|
+
ports:
|
|
151
|
+
- ${LITELLM_PORT}:4000
|
|
152
|
+
environment:
|
|
153
|
+
- DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY}
|
|
154
|
+
- LITELLM_API_KEY=${LITELLM_API_KEY}
|
|
155
|
+
volumes:
|
|
156
|
+
- ./litellm/config.yaml:/app/config.yaml
|
|
157
|
+
command: ["--config", "/app/config.yaml"]
|
|
158
|
+
|
|
142
159
|
volumes:
|
|
143
160
|
database_data:
|
|
144
161
|
driver: local
|
|
@@ -7,6 +7,7 @@ from openai import AsyncOpenAI
|
|
|
7
7
|
|
|
8
8
|
from crow_cli.agent.configure import LLMProvider
|
|
9
9
|
from logging import Logger
|
|
10
|
+
from importlib.metadata import version
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
|
|
@@ -46,7 +47,7 @@ def configure_llm(
|
|
|
46
47
|
api_key=api_key,
|
|
47
48
|
base_url=base_url,
|
|
48
49
|
http_client=http_client,
|
|
49
|
-
default_headers={"User-Agent": "
|
|
50
|
+
default_headers={"User-Agent": f"CrowCLI/{version('crow-cli')}"},
|
|
50
51
|
)
|
|
51
52
|
logger.info(f"USER-AGENT: {client.user_agent}") # =
|
|
52
53
|
return client
|
|
@@ -61,6 +61,7 @@ from acp import (
|
|
|
61
61
|
LoadSessionResponse,
|
|
62
62
|
NewSessionResponse,
|
|
63
63
|
PromptResponse,
|
|
64
|
+
RequestError,
|
|
64
65
|
SetSessionModeResponse,
|
|
65
66
|
run_agent,
|
|
66
67
|
text_block,
|
|
@@ -184,6 +185,7 @@ class AcpAgent(Agent):
|
|
|
184
185
|
self._config_values: dict[
|
|
185
186
|
str, dict[str, str]
|
|
186
187
|
] = {} # session_id -> {config_id: value}
|
|
188
|
+
self._notification_queue: list[dict] = [] # queued extension notifications
|
|
187
189
|
|
|
188
190
|
def _default_model_value(self) -> str:
|
|
189
191
|
model = next(iter(self._config.llm.models.values()), None)
|
|
@@ -329,6 +331,8 @@ class AcpAgent(Agent):
|
|
|
329
331
|
# mcp servers == tools == system prompt
|
|
330
332
|
# ######################################
|
|
331
333
|
|
|
334
|
+
self._logger.info("new_session mcp_servers from ACP: %s", mcp_servers)
|
|
335
|
+
|
|
332
336
|
# Use default MCP config if no servers provided
|
|
333
337
|
builtin_config = self._config.get_builtin_mcp_config()
|
|
334
338
|
self._logger.info("builtin_config: %s", builtin_config)
|
|
@@ -340,6 +344,7 @@ class AcpAgent(Agent):
|
|
|
340
344
|
builtin_config=builtin_config,
|
|
341
345
|
logger=self._logger,
|
|
342
346
|
)
|
|
347
|
+
self._logger.info("new_session merged config from create_mcp_client_from_acp: %s", config)
|
|
343
348
|
self._config.mcp_servers = config
|
|
344
349
|
# CRITICAL: Use AsyncExitStack for lifecycle management
|
|
345
350
|
mcp_client = await self._exit_stack.enter_async_context(mcp_client)
|
|
@@ -460,6 +465,7 @@ class AcpAgent(Agent):
|
|
|
460
465
|
|
|
461
466
|
# Setup MCP client (same as new_session)
|
|
462
467
|
# Use default config if no servers given
|
|
468
|
+
self._logger.info("LOAD_SESSION: mcp_servers from ACP: %s", mcp_servers)
|
|
463
469
|
self._logger.info("LOAD_SESSION: Step 2: Getting builtin config")
|
|
464
470
|
builtin_config = self._config.get_builtin_mcp_config()
|
|
465
471
|
self._logger.info(
|
|
@@ -475,6 +481,7 @@ class AcpAgent(Agent):
|
|
|
475
481
|
builtin_config=builtin_config,
|
|
476
482
|
logger=self._logger,
|
|
477
483
|
)
|
|
484
|
+
self._logger.info("LOAD_SESSION merged config: %s", config)
|
|
478
485
|
self._logger.info("LOAD_SESSION: Step 3 complete: MCP client created")
|
|
479
486
|
|
|
480
487
|
# CRITICAL: Use AsyncExitStack for lifecycle management
|
|
@@ -727,6 +734,12 @@ class AcpAgent(Agent):
|
|
|
727
734
|
elif chunk_type == "tool_args":
|
|
728
735
|
self._logger.debug("Tool args: %s", chunk["token"])
|
|
729
736
|
|
|
737
|
+
elif chunk_type == "compaction":
|
|
738
|
+
await self._conn.session_update(
|
|
739
|
+
session_id=session.session_id,
|
|
740
|
+
update=update_agent_message(text_block(chunk["token"])),
|
|
741
|
+
)
|
|
742
|
+
|
|
730
743
|
elif chunk_type == "final_history":
|
|
731
744
|
break
|
|
732
745
|
|
|
@@ -750,7 +763,14 @@ class AcpAgent(Agent):
|
|
|
750
763
|
return PromptResponse(stop_reason="cancelled")
|
|
751
764
|
except Exception as e:
|
|
752
765
|
self._session_logger.error("Error in prompt handling: %s", e, exc_info=True)
|
|
753
|
-
|
|
766
|
+
# Surface the failure as an ACP JSON-RPC error (code -32603) instead
|
|
767
|
+
# of a clean end_turn. A clean end_turn made the client think the
|
|
768
|
+
# turn finished normally and re-fire the task/nag loop — so a
|
|
769
|
+
# failing agent (e.g. LLM context overflow) got nagged thousands
|
|
770
|
+
# of times. The acp SDK serializes a raised RequestError into an
|
|
771
|
+
# error response; the client gates its task loop on that Err and
|
|
772
|
+
# broadcasts the message to the user.
|
|
773
|
+
raise RequestError.internal_error({"error": str(e)})
|
|
754
774
|
finally:
|
|
755
775
|
# 4. Cleanup the task reference when done
|
|
756
776
|
self._prompt_tasks.pop(session_id, None)
|
|
@@ -770,7 +790,14 @@ class AcpAgent(Agent):
|
|
|
770
790
|
|
|
771
791
|
async def ext_notification(self, method: str, params: dict[str, Any]) -> None:
|
|
772
792
|
"""Handle extension notifications"""
|
|
773
|
-
self._logger.info("Extension notification: %s", method)
|
|
793
|
+
self._logger.info("Extension notification: %s with params: %s", method, params)
|
|
794
|
+
|
|
795
|
+
# Queue _send notifications for processing
|
|
796
|
+
if method == "_send":
|
|
797
|
+
if not hasattr(self, '_notification_queue'):
|
|
798
|
+
self._notification_queue = []
|
|
799
|
+
self._notification_queue.append(params)
|
|
800
|
+
self._logger.info("Queued _send notification, queue size: %d", len(self._notification_queue))
|
|
774
801
|
|
|
775
802
|
async def cleanup(self) -> None:
|
|
776
803
|
"""
|
|
@@ -786,7 +813,7 @@ class AcpAgent(Agent):
|
|
|
786
813
|
async def list_sessions(
|
|
787
814
|
self, cursor: str | None = None, cwd: str | None = None, **kwargs: Any
|
|
788
815
|
) -> ListSessionsResponse:
|
|
789
|
-
self._logger.info("Listing sessions for working directory:
|
|
816
|
+
self._logger.info("Listing sessions for working directory: %s", cwd)
|
|
790
817
|
if cwd is None:
|
|
791
818
|
return ListSessionsResponse(sessions=[], next_cursor=None)
|
|
792
819
|
sessions_info = get_session_by_cwd(cwd, self._db_uri)
|
|
@@ -17,6 +17,7 @@ from crow_cli.agent.configure import Config
|
|
|
17
17
|
|
|
18
18
|
def acp_to_fastmcp_config(
|
|
19
19
|
mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio],
|
|
20
|
+
logger: Logger | None = None,
|
|
20
21
|
) -> dict[str, Any]:
|
|
21
22
|
"""
|
|
22
23
|
Convert ACP mcp_servers to FastMCP configuration dict.
|
|
@@ -36,6 +37,8 @@ def acp_to_fastmcp_config(
|
|
|
36
37
|
if isinstance(server, McpServerStdio):
|
|
37
38
|
# Convert List[EnvVariable] to dict[str, str]
|
|
38
39
|
env_dict = {e.name: e.value for e in server.env}
|
|
40
|
+
if logger:
|
|
41
|
+
logger.info(" acp_to_fastmcp_config: server '%s' env_dict=%s", server.name, env_dict)
|
|
39
42
|
|
|
40
43
|
config["mcpServers"][server.name] = {
|
|
41
44
|
"transport": "stdio",
|
|
@@ -83,21 +86,29 @@ def create_mcp_client_from_acp(
|
|
|
83
86
|
Returns:
|
|
84
87
|
FastMCP Client instance (must be used with async with)
|
|
85
88
|
"""
|
|
89
|
+
logger.info("create_mcp_client_from_acp called")
|
|
90
|
+
logger.info(" mcp_servers param: %s", mcp_servers)
|
|
91
|
+
logger.info(" builtin_config param: %s", builtin_config)
|
|
92
|
+
|
|
86
93
|
# Start with fallback config as base
|
|
87
94
|
config: dict[str, Any] = (
|
|
88
95
|
dict(builtin_config) if builtin_config else {"mcpServers": {}}
|
|
89
96
|
)
|
|
97
|
+
logger.info(" base config after builtin: %s", config)
|
|
90
98
|
|
|
91
99
|
# Convert any ACP mcp_servers and merge into config
|
|
92
100
|
if mcp_servers:
|
|
93
|
-
acp_config = acp_to_fastmcp_config(mcp_servers)
|
|
101
|
+
acp_config = acp_to_fastmcp_config(mcp_servers, logger=logger)
|
|
102
|
+
logger.info(" acp_config converted: %s", acp_config)
|
|
94
103
|
config["mcpServers"].update(acp_config["mcpServers"])
|
|
104
|
+
logger.info(" config after merging acp: %s", config)
|
|
95
105
|
|
|
96
106
|
# Add cwd to each server config
|
|
97
|
-
for server_config in config["mcpServers"].
|
|
107
|
+
for name, server_config in config["mcpServers"].items():
|
|
98
108
|
server_config["cwd"] = cwd
|
|
109
|
+
logger.info(" server '%s' env: %s", name, server_config.get("env"))
|
|
99
110
|
|
|
100
|
-
logger.info(" final config: %s", config)
|
|
111
|
+
logger.info(" final config (before transport): %s", config)
|
|
101
112
|
if not config.get("mcpServers"):
|
|
102
113
|
raise ValueError("No MCP servers defined in the config")
|
|
103
114
|
|
|
@@ -27,6 +27,12 @@ from crow_cli.agent.tools import (
|
|
|
27
27
|
execute_acp_tool,
|
|
28
28
|
execute_acp_write,
|
|
29
29
|
execute_acp_prompt,
|
|
30
|
+
execute_orchestration_send_prompt,
|
|
31
|
+
execute_orchestration_task_read,
|
|
32
|
+
execute_orchestration_task_write,
|
|
33
|
+
execute_orchestration_task_send,
|
|
34
|
+
execute_orchestration_orchestrator_task_read,
|
|
35
|
+
execute_orchestration_orchestrator_task_write,
|
|
30
36
|
)
|
|
31
37
|
|
|
32
38
|
|
|
@@ -41,6 +47,8 @@ async def send_request(
|
|
|
41
47
|
max_tokens: int,
|
|
42
48
|
max_retries: int = 3,
|
|
43
49
|
retry_delay: float = 1.0,
|
|
50
|
+
temperature: float = 0.6,
|
|
51
|
+
request_log_path: str | None = None,
|
|
44
52
|
):
|
|
45
53
|
"""
|
|
46
54
|
Send request to LLM with error handling and retry logic.
|
|
@@ -60,26 +68,45 @@ async def send_request(
|
|
|
60
68
|
RateLimitError: If rate limit is exceeded.
|
|
61
69
|
APIConnectionError: If connection to API fails.
|
|
62
70
|
"""
|
|
71
|
+
# Normalize messages once — deterministic, no need to redo per retry.
|
|
72
|
+
# Handles both list format (multimodal) and string format.
|
|
73
|
+
normalized_messages = []
|
|
74
|
+
for msg in session.messages:
|
|
75
|
+
normalized_msg = dict(msg)
|
|
76
|
+
content = msg.get("content")
|
|
77
|
+
# If content is a list of content blocks, keep it as-is (for multimodal)
|
|
78
|
+
# but if it's a list with only text blocks in the wrong format, fix it.
|
|
79
|
+
if isinstance(content, list):
|
|
80
|
+
normalized_msg["content"] = normalize_blocks(content)
|
|
81
|
+
normalized_messages.append(normalized_msg)
|
|
82
|
+
|
|
83
|
+
# Under --debug, dump the exact request payload (the append-only chat
|
|
84
|
+
# history + params) so immutable-history analysis can diff consecutive
|
|
85
|
+
# turns. Sits beside the response chunk log in the same chunk_log_dir.
|
|
86
|
+
if request_log_path:
|
|
87
|
+
request_payload = {
|
|
88
|
+
"model": session.model_identifier,
|
|
89
|
+
"messages": normalized_messages,
|
|
90
|
+
"tools": tools,
|
|
91
|
+
"temperature": temperature,
|
|
92
|
+
"max_tokens": max_tokens,
|
|
93
|
+
"parallel_tool_calls": True,
|
|
94
|
+
"stream_options": {"include_usage": True},
|
|
95
|
+
}
|
|
96
|
+
Path(request_log_path).write_text(
|
|
97
|
+
json.dumps(request_payload, ensure_ascii=False, indent=2)
|
|
98
|
+
)
|
|
99
|
+
|
|
63
100
|
last_exception = None
|
|
64
101
|
|
|
65
102
|
for attempt in range(max_retries):
|
|
66
103
|
try:
|
|
67
|
-
# Normalize messages - handle both list format (multimodal) and string format
|
|
68
|
-
normalized_messages = []
|
|
69
|
-
for msg in session.messages:
|
|
70
|
-
normalized_msg = dict(msg)
|
|
71
|
-
content = msg.get("content")
|
|
72
|
-
# If content is a list of content blocks, keep it as-is (for multimodal)
|
|
73
|
-
# But if it's a list with only text blocks and they're in the wrong format, fix it
|
|
74
|
-
if isinstance(content, list):
|
|
75
|
-
normalized_msg["content"] = normalize_blocks(content)
|
|
76
|
-
normalized_messages.append(normalized_msg)
|
|
77
|
-
|
|
78
104
|
return await llm.chat.completions.create(
|
|
79
105
|
model=session.model_identifier,
|
|
80
106
|
messages=normalized_messages,
|
|
81
107
|
tools=tools,
|
|
82
108
|
stream=True,
|
|
109
|
+
temperature=temperature,
|
|
83
110
|
max_tokens=max_tokens,
|
|
84
111
|
parallel_tool_calls=True,
|
|
85
112
|
stream_options={"include_usage": True}, # Get usage in final chunk
|
|
@@ -468,7 +495,60 @@ async def execute_tool_calls(
|
|
|
468
495
|
args=arg_dict,
|
|
469
496
|
logger=logger,
|
|
470
497
|
)
|
|
471
|
-
|
|
498
|
+
elif tool_name == "send_prompt":
|
|
499
|
+
result_content = await execute_orchestration_send_prompt(
|
|
500
|
+
conn=conn,
|
|
501
|
+
turn_id=turn_id,
|
|
502
|
+
agent_id=agent_id,
|
|
503
|
+
tool_call_id=llm_tool_call_id,
|
|
504
|
+
args=arg_dict,
|
|
505
|
+
logger=logger,
|
|
506
|
+
)
|
|
507
|
+
elif tool_name == "task_read":
|
|
508
|
+
result_content = await execute_orchestration_task_read(
|
|
509
|
+
conn=conn,
|
|
510
|
+
turn_id=turn_id,
|
|
511
|
+
agent_id=agent_id,
|
|
512
|
+
tool_call_id=llm_tool_call_id,
|
|
513
|
+
args=arg_dict,
|
|
514
|
+
logger=logger,
|
|
515
|
+
)
|
|
516
|
+
elif tool_name == "task_write":
|
|
517
|
+
result_content = await execute_orchestration_task_write(
|
|
518
|
+
conn=conn,
|
|
519
|
+
turn_id=turn_id,
|
|
520
|
+
agent_id=agent_id,
|
|
521
|
+
tool_call_id=llm_tool_call_id,
|
|
522
|
+
args=arg_dict,
|
|
523
|
+
logger=logger,
|
|
524
|
+
)
|
|
525
|
+
elif tool_name == "task_send":
|
|
526
|
+
result_content = await execute_orchestration_task_send(
|
|
527
|
+
conn=conn,
|
|
528
|
+
turn_id=turn_id,
|
|
529
|
+
agent_id=agent_id,
|
|
530
|
+
tool_call_id=llm_tool_call_id,
|
|
531
|
+
args=arg_dict,
|
|
532
|
+
logger=logger,
|
|
533
|
+
)
|
|
534
|
+
elif tool_name == "orchestrator_task_read":
|
|
535
|
+
result_content = await execute_orchestration_orchestrator_task_read(
|
|
536
|
+
conn=conn,
|
|
537
|
+
turn_id=turn_id,
|
|
538
|
+
agent_id=agent_id,
|
|
539
|
+
tool_call_id=llm_tool_call_id,
|
|
540
|
+
args=arg_dict,
|
|
541
|
+
logger=logger,
|
|
542
|
+
)
|
|
543
|
+
elif tool_name == "orchestrator_task_write":
|
|
544
|
+
result_content = await execute_orchestration_orchestrator_task_write(
|
|
545
|
+
conn=conn,
|
|
546
|
+
turn_id=turn_id,
|
|
547
|
+
agent_id=agent_id,
|
|
548
|
+
tool_call_id=llm_tool_call_id,
|
|
549
|
+
args=arg_dict,
|
|
550
|
+
logger=logger,
|
|
551
|
+
)
|
|
472
552
|
else:
|
|
473
553
|
result_content = await execute_acp_tool(
|
|
474
554
|
conn=conn,
|
|
@@ -541,21 +621,27 @@ async def react_loop(
|
|
|
541
621
|
session_id = session_from_agent_id(agent_id)
|
|
542
622
|
chunk_index = 0
|
|
543
623
|
for turn in range(max_turns):
|
|
624
|
+
# Under --debug, log both the request payload and the response chunks
|
|
625
|
+
# for this turn into the same chunk_log_dir (sibling filenames).
|
|
626
|
+
chunk_log_path = None
|
|
627
|
+
request_log_path = None
|
|
628
|
+
if chunk_log_dir:
|
|
629
|
+
chunk_log_path = str(Path(chunk_log_dir) / f"turn-{turn_id}.jsonl")
|
|
630
|
+
request_log_path = str(Path(chunk_log_dir) / f"turn-{turn_id}-request.json")
|
|
631
|
+
|
|
544
632
|
response = await send_request(
|
|
545
633
|
llm,
|
|
546
634
|
session,
|
|
547
635
|
tools,
|
|
548
636
|
config.MAX_TOKENS,
|
|
637
|
+
temperature=config.TEMPERATURE,
|
|
638
|
+
request_log_path=request_log_path,
|
|
549
639
|
)
|
|
550
640
|
state_accumulator = state_accumulators.get(
|
|
551
641
|
session_id, {"thinking": [], "content": [], "tool_calls": {}}
|
|
552
642
|
)
|
|
553
643
|
thinking, content, tool_call_inputs, usage = [], [], [], None
|
|
554
644
|
|
|
555
|
-
chunk_log_path = None
|
|
556
|
-
if chunk_log_dir:
|
|
557
|
-
chunk_log_path = str(Path(chunk_log_dir) / f"turn-{turn_id}.jsonl")
|
|
558
|
-
|
|
559
645
|
try:
|
|
560
646
|
async for msg_type, token in process_response(
|
|
561
647
|
response, state_accumulator, chunk_log_path=chunk_log_path
|
|
@@ -607,6 +693,11 @@ async def react_loop(
|
|
|
607
693
|
if usage and usage["total_tokens"] > config.MAX_COMPACT_TOKENS:
|
|
608
694
|
logger.info("Token threshold crossed. Initiating compaction...")
|
|
609
695
|
|
|
696
|
+
yield {
|
|
697
|
+
"type": "compaction",
|
|
698
|
+
"token": f"\n\nCompaction threshold of {config.MAX_COMPACT_TOKENS} reached — compacting conversation history...\n\n",
|
|
699
|
+
}
|
|
700
|
+
|
|
610
701
|
old_agent_id = agent_id
|
|
611
702
|
logger.info(f"Pre-compacted session length: {len(session.messages)}")
|
|
612
703
|
session = await compact(
|
|
@@ -66,8 +66,17 @@ def get_session_by_cwd(cwd, db_uri):
|
|
|
66
66
|
data = msgs[1].data
|
|
67
67
|
if isinstance(data, str):
|
|
68
68
|
data = json.loads(data)
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
content = data.get("content", "") if data else ""
|
|
70
|
+
# Handle content blocks (list of dicts) vs plain string
|
|
71
|
+
if isinstance(content, list):
|
|
72
|
+
title = "".join(
|
|
73
|
+
block.get("text", "") for block in content if block.get("type") == "text"
|
|
74
|
+
)[:50]
|
|
75
|
+
else:
|
|
76
|
+
title = str(content)[:50]
|
|
77
|
+
if not title:
|
|
78
|
+
title = "Untitled Chat"
|
|
79
|
+
except (json.JSONDecodeError, AttributeError, TypeError):
|
|
71
80
|
title = "Untitled Chat"
|
|
72
81
|
else:
|
|
73
82
|
title = "Untitled Chat"
|
|
@@ -249,7 +258,7 @@ class AgentSession:
|
|
|
249
258
|
model_identifier: str,
|
|
250
259
|
db_uri: str = "sqlite:///crow.db",
|
|
251
260
|
cwd: str = "/tmp",
|
|
252
|
-
agent_idx: int =
|
|
261
|
+
agent_idx: int = 1,
|
|
253
262
|
session_id: str | None = None,
|
|
254
263
|
initial_messages: list[dict[str, Any]] | None = None,
|
|
255
264
|
) -> "AgentSession":
|
|
@@ -369,6 +378,46 @@ class AgentSession:
|
|
|
369
378
|
self._model = None
|
|
370
379
|
|
|
371
380
|
|
|
381
|
+
def get_skills_catalog(skills_dir: Path) -> str:
|
|
382
|
+
"""Scan skills dir, parse SKILL.md frontmatter, return brief catalog."""
|
|
383
|
+
if not skills_dir.exists():
|
|
384
|
+
return ""
|
|
385
|
+
entries = []
|
|
386
|
+
for skill_dir in sorted(skills_dir.iterdir()):
|
|
387
|
+
skill_md = skill_dir / "SKILL.md"
|
|
388
|
+
if not skill_md.exists():
|
|
389
|
+
continue
|
|
390
|
+
try:
|
|
391
|
+
text = skill_md.read_text()
|
|
392
|
+
# Parse YAML frontmatter between --- markers
|
|
393
|
+
if not text.startswith("---"):
|
|
394
|
+
continue
|
|
395
|
+
end = text.index("---", 3)
|
|
396
|
+
frontmatter = text[3:end]
|
|
397
|
+
name = None
|
|
398
|
+
desc_lines = []
|
|
399
|
+
in_desc = False
|
|
400
|
+
for line in frontmatter.splitlines():
|
|
401
|
+
if line.startswith("name:"):
|
|
402
|
+
name = line[5:].strip()
|
|
403
|
+
in_desc = False
|
|
404
|
+
elif line.startswith("description:"):
|
|
405
|
+
desc_lines.append(line[12:].strip())
|
|
406
|
+
in_desc = True
|
|
407
|
+
elif in_desc and line.startswith(" "):
|
|
408
|
+
desc_lines.append(line.strip())
|
|
409
|
+
else:
|
|
410
|
+
in_desc = False
|
|
411
|
+
if name and desc_lines:
|
|
412
|
+
description = " ".join(desc_lines)
|
|
413
|
+
entries.append(f"- **{name}**: {description}")
|
|
414
|
+
except (ValueError, OSError):
|
|
415
|
+
continue
|
|
416
|
+
if not entries:
|
|
417
|
+
return ""
|
|
418
|
+
return "Available skills (~/.crow/skills/):\n" + "\n".join(entries)
|
|
419
|
+
|
|
420
|
+
|
|
372
421
|
def make_agent_session(
|
|
373
422
|
config: Config,
|
|
374
423
|
tools: list[dict],
|
|
@@ -377,7 +426,9 @@ def make_agent_session(
|
|
|
377
426
|
session_id: str | None = None,
|
|
378
427
|
agent_idx: int | None = None,
|
|
379
428
|
):
|
|
380
|
-
if
|
|
429
|
+
if config.system_prompt_path:
|
|
430
|
+
template = config.system_prompt_path.read_text()
|
|
431
|
+
elif not config.system_prompt:
|
|
381
432
|
template_path = config.config_dir / "prompts" / "system_prompt.jinja2"
|
|
382
433
|
template = template_path.read_text()
|
|
383
434
|
else:
|
|
@@ -386,12 +437,19 @@ def make_agent_session(
|
|
|
386
437
|
template, name="crow-default", db_uri=config.db_uri
|
|
387
438
|
)
|
|
388
439
|
display_tree = get_directory_tree(cwd)
|
|
389
|
-
|
|
440
|
+
skills_catalog = get_skills_catalog(config.config_dir / "skills")
|
|
441
|
+
agent_path = os.path.join(cwd, "AGENTS.typ")
|
|
390
442
|
if os.path.exists(agent_path):
|
|
391
443
|
with open(agent_path, "r") as f:
|
|
392
444
|
agents_content = f.read()
|
|
393
445
|
else:
|
|
394
|
-
|
|
446
|
+
# Fallback to markdown
|
|
447
|
+
agent_path = os.path.join(cwd, "AGENTS.md")
|
|
448
|
+
if os.path.exists(agent_path):
|
|
449
|
+
with open(agent_path, "r") as f:
|
|
450
|
+
agents_content = f.read()
|
|
451
|
+
else:
|
|
452
|
+
agents_content = "No AGENTS.md found"
|
|
395
453
|
if session_id is None:
|
|
396
454
|
session_id = get_coolname()
|
|
397
455
|
if agent_idx is None:
|
|
@@ -403,6 +461,7 @@ def make_agent_session(
|
|
|
403
461
|
"display_tree": display_tree,
|
|
404
462
|
"agents_content": agents_content,
|
|
405
463
|
"session_id": session_id,
|
|
464
|
+
"skills_catalog": skills_catalog,
|
|
406
465
|
},
|
|
407
466
|
tool_definitions=tools,
|
|
408
467
|
request_params={"temperature": 0.2},
|