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.
Files changed (64) hide show
  1. {crow_cli-0.1.24 → crow_cli-0.1.26}/PKG-INFO +2 -2
  2. {crow_cli-0.1.24 → crow_cli-0.1.26}/README.md +1 -1
  3. {crow_cli-0.1.24 → crow_cli-0.1.26}/pyproject.toml +1 -1
  4. {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/configure.py +18 -3
  5. {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/default/defaults.py +17 -0
  6. {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/llm.py +2 -1
  7. {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/main.py +30 -3
  8. {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/mcp_client.py +14 -3
  9. {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/react.py +107 -16
  10. {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/session.py +65 -6
  11. {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/tools.py +418 -0
  12. crow_cli-0.1.26/src/crow_cli/cli/install.py +243 -0
  13. {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/cli/main.py +53 -14
  14. {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/client/main.py +137 -3
  15. crow_cli-0.1.26/src/crow_cli/client/terminal.py +218 -0
  16. {crow_cli-0.1.24 → crow_cli-0.1.26}/tests/analyze_payload_cache_invalidation.py +37 -17
  17. {crow_cli-0.1.24 → crow_cli-0.1.26}/tests/analyze_payload_deep.py +33 -14
  18. {crow_cli-0.1.24 → crow_cli-0.1.26}/tests/conftest.py +50 -0
  19. {crow_cli-0.1.24/tests → crow_cli-0.1.26/tests/e2e}/test_live_llm_integration.py +12 -13
  20. crow_cli-0.1.26/tests/e2e/test_session_update_transmission.py +251 -0
  21. crow_cli-0.1.26/tests/integration/test_agent_init.py +65 -0
  22. {crow_cli-0.1.24/tests → crow_cli-0.1.26/tests/integration}/test_auth_validation.py +2 -1
  23. crow_cli-0.1.26/tests/unit/test_client_terminal.py +189 -0
  24. crow_cli-0.1.26/tests/unit/test_compact.py +157 -0
  25. {crow_cli-0.1.24/tests → crow_cli-0.1.26/tests/unit}/test_persistence_integrity.py +17 -17
  26. crow_cli-0.1.26/tests/unit/test_request_logging.py +82 -0
  27. crow_cli-0.1.26/tests/unit/test_stream_processing.py +337 -0
  28. {crow_cli-0.1.24 → crow_cli-0.1.26}/tests/unit/test_uv_project_enforcement.py +19 -9
  29. {crow_cli-0.1.24 → crow_cli-0.1.26}/uv.lock +1 -1
  30. crow_cli-0.1.24/tests/test_acp_token_loss.py +0 -548
  31. crow_cli-0.1.24/tests/test_agent_init.py +0 -82
  32. crow_cli-0.1.24/tests/test_payload_integrity.py +0 -262
  33. crow_cli-0.1.24/tests/test_session_update_transmission.py +0 -546
  34. crow_cli-0.1.24/tests/test_token_loss_detection.py +0 -572
  35. crow_cli-0.1.24/tests/test_token_loss_reproduction.py +0 -259
  36. crow_cli-0.1.24/tests/unit/test_compact.py +0 -187
  37. {crow_cli-0.1.24 → crow_cli-0.1.26}/.gitignore +0 -0
  38. {crow_cli-0.1.24 → crow_cli-0.1.26}/.python-version +0 -0
  39. {crow_cli-0.1.24 → crow_cli-0.1.26}/agent.json +0 -0
  40. {crow_cli-0.1.24 → crow_cli-0.1.26}/crow-cli.spec +0 -0
  41. {crow_cli-0.1.24 → crow_cli-0.1.26}/examples/mc_escher_loop.py +0 -0
  42. {crow_cli-0.1.24 → crow_cli-0.1.26}/examples/quick_test.py +0 -0
  43. {crow_cli-0.1.24 → crow_cli-0.1.26}/run_tests.sh +0 -0
  44. {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/__init__.py +0 -0
  45. {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/__init__.py +0 -0
  46. {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/compact.py +0 -0
  47. {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/context.py +0 -0
  48. {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/db.py +0 -0
  49. {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/default/__init__.py +0 -0
  50. {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/default/searxng_settings.py +0 -0
  51. {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/hooks.py +0 -0
  52. {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/logger.py +0 -0
  53. {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/prompt.py +0 -0
  54. {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent/slash.py +0 -0
  55. {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/agent_runner.py +0 -0
  56. {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/cli/__init__.py +0 -0
  57. {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/cli/init_cmd.py +0 -0
  58. {crow_cli-0.1.24 → crow_cli-0.1.26}/src/crow_cli/client/__init__.py +0 -0
  59. {crow_cli-0.1.24 → crow_cli-0.1.26}/test_send_request_error_handling.py +0 -0
  60. {crow_cli-0.1.24 → crow_cli-0.1.26}/tests/__init__.py +0 -0
  61. {crow_cli-0.1.24 → crow_cli-0.1.26}/tests/unit/test_compact_last_messages_list_content.json +0 -0
  62. {crow_cli-0.1.24 → crow_cli-0.1.26}/tests/unit/test_compact_last_messages_list_content.py +0 -0
  63. {crow_cli-0.1.24/tests → crow_cli-0.1.26/tests/unit}/test_json_repair.py +0 -0
  64. {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.24
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
- Apache-2.0
597
+ MIT
@@ -570,4 +570,4 @@ If you see errors about malformed tool call arguments:
570
570
 
571
571
  ## License
572
572
 
573
- Apache-2.0
573
+ MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "crow-cli"
3
- version = "0.1.24"
3
+ version = "0.1.26"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14"
@@ -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
- return {"mcpServers": dict(self.mcp_servers or {})}
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=parsed.get("mcpServers", {}),
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": "KimiCLI/1.41.0"},
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
- return PromptResponse(stop_reason="end_turn")
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: {cwd}", cwd=cwd)
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"].values():
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
- title = data.get("content", "")[:50] if data else "Untitled Chat"
70
- except json.JSONDecodeError, AttributeError, TypeError:
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 = 0,
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 not config.system_prompt:
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
- agent_path = os.path.join(cwd, "AGENTS.md")
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
- agents_content = "No AGENTS.md found"
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},