crow-cli 0.1.25__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.25 → crow_cli-0.1.26}/PKG-INFO +2 -2
  2. {crow_cli-0.1.25 → crow_cli-0.1.26}/README.md +1 -1
  3. {crow_cli-0.1.25 → crow_cli-0.1.26}/pyproject.toml +1 -1
  4. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/configure.py +2 -0
  5. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/default/defaults.py +5 -0
  6. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/main.py +24 -2
  7. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/react.py +107 -16
  8. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/session.py +43 -1
  9. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/tools.py +418 -0
  10. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/cli/main.py +2 -0
  11. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/client/main.py +137 -3
  12. crow_cli-0.1.26/src/crow_cli/client/terminal.py +218 -0
  13. {crow_cli-0.1.25 → crow_cli-0.1.26}/tests/analyze_payload_cache_invalidation.py +37 -17
  14. {crow_cli-0.1.25 → crow_cli-0.1.26}/tests/analyze_payload_deep.py +33 -14
  15. {crow_cli-0.1.25 → crow_cli-0.1.26}/tests/conftest.py +50 -0
  16. {crow_cli-0.1.25/tests → crow_cli-0.1.26/tests/e2e}/test_live_llm_integration.py +12 -13
  17. crow_cli-0.1.26/tests/e2e/test_session_update_transmission.py +251 -0
  18. crow_cli-0.1.26/tests/integration/test_agent_init.py +65 -0
  19. {crow_cli-0.1.25/tests → crow_cli-0.1.26/tests/integration}/test_auth_validation.py +2 -1
  20. crow_cli-0.1.26/tests/unit/test_client_terminal.py +189 -0
  21. crow_cli-0.1.26/tests/unit/test_compact.py +157 -0
  22. {crow_cli-0.1.25/tests → crow_cli-0.1.26/tests/unit}/test_persistence_integrity.py +17 -17
  23. crow_cli-0.1.26/tests/unit/test_request_logging.py +82 -0
  24. crow_cli-0.1.26/tests/unit/test_stream_processing.py +337 -0
  25. {crow_cli-0.1.25 → crow_cli-0.1.26}/tests/unit/test_uv_project_enforcement.py +19 -9
  26. {crow_cli-0.1.25 → crow_cli-0.1.26}/uv.lock +1 -1
  27. crow_cli-0.1.25/tests/test_acp_token_loss.py +0 -548
  28. crow_cli-0.1.25/tests/test_agent_init.py +0 -82
  29. crow_cli-0.1.25/tests/test_payload_integrity.py +0 -262
  30. crow_cli-0.1.25/tests/test_session_update_transmission.py +0 -546
  31. crow_cli-0.1.25/tests/test_token_loss_detection.py +0 -572
  32. crow_cli-0.1.25/tests/test_token_loss_reproduction.py +0 -259
  33. crow_cli-0.1.25/tests/unit/test_compact.py +0 -187
  34. {crow_cli-0.1.25 → crow_cli-0.1.26}/.gitignore +0 -0
  35. {crow_cli-0.1.25 → crow_cli-0.1.26}/.python-version +0 -0
  36. {crow_cli-0.1.25 → crow_cli-0.1.26}/agent.json +0 -0
  37. {crow_cli-0.1.25 → crow_cli-0.1.26}/crow-cli.spec +0 -0
  38. {crow_cli-0.1.25 → crow_cli-0.1.26}/examples/mc_escher_loop.py +0 -0
  39. {crow_cli-0.1.25 → crow_cli-0.1.26}/examples/quick_test.py +0 -0
  40. {crow_cli-0.1.25 → crow_cli-0.1.26}/run_tests.sh +0 -0
  41. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/__init__.py +0 -0
  42. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/__init__.py +0 -0
  43. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/compact.py +0 -0
  44. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/context.py +0 -0
  45. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/db.py +0 -0
  46. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/default/__init__.py +0 -0
  47. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/default/searxng_settings.py +0 -0
  48. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/hooks.py +0 -0
  49. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/llm.py +0 -0
  50. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/logger.py +0 -0
  51. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/mcp_client.py +0 -0
  52. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/prompt.py +0 -0
  53. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/slash.py +0 -0
  54. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent_runner.py +0 -0
  55. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/cli/__init__.py +0 -0
  56. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/cli/init_cmd.py +0 -0
  57. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/cli/install.py +0 -0
  58. {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/client/__init__.py +0 -0
  59. {crow_cli-0.1.25 → crow_cli-0.1.26}/test_send_request_error_handling.py +0 -0
  60. {crow_cli-0.1.25 → crow_cli-0.1.26}/tests/__init__.py +0 -0
  61. {crow_cli-0.1.25 → crow_cli-0.1.26}/tests/unit/test_compact_last_messages_list_content.json +0 -0
  62. {crow_cli-0.1.25 → crow_cli-0.1.26}/tests/unit/test_compact_last_messages_list_content.py +0 -0
  63. {crow_cli-0.1.25/tests → crow_cli-0.1.26/tests/unit}/test_json_repair.py +0 -0
  64. {crow_cli-0.1.25 → 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.25
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.25"
3
+ version = "0.1.26"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14"
@@ -96,6 +96,7 @@ class Config:
96
96
  max_retries_per_step: int = 3
97
97
  MAX_COMPACT_TOKENS: int = 190000
98
98
  MAX_TOKENS: int = 38192
99
+ TEMPERATURE: float = 0.6
99
100
  chunk_log: bool = False # Write every raw chunk to JSONL for debugging
100
101
  system_prompt: str = SYSTEM_PROMPT
101
102
  system_prompt_path: Path | None = None
@@ -169,6 +170,7 @@ class Config:
169
170
  ("max_retries_per_step", int),
170
171
  ("MAX_COMPACT_TOKENS", int),
171
172
  ("MAX_TOKENS", int),
173
+ ("TEMPERATURE", float),
172
174
  ):
173
175
  if key in parsed:
174
176
  overrides[key] = typ(parsed[key])
@@ -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:
@@ -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)
@@ -732,6 +734,12 @@ class AcpAgent(Agent):
732
734
  elif chunk_type == "tool_args":
733
735
  self._logger.debug("Tool args: %s", chunk["token"])
734
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
+
735
743
  elif chunk_type == "final_history":
736
744
  break
737
745
 
@@ -755,7 +763,14 @@ class AcpAgent(Agent):
755
763
  return PromptResponse(stop_reason="cancelled")
756
764
  except Exception as e:
757
765
  self._session_logger.error("Error in prompt handling: %s", e, exc_info=True)
758
- 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)})
759
774
  finally:
760
775
  # 4. Cleanup the task reference when done
761
776
  self._prompt_tasks.pop(session_id, None)
@@ -775,7 +790,14 @@ class AcpAgent(Agent):
775
790
 
776
791
  async def ext_notification(self, method: str, params: dict[str, Any]) -> None:
777
792
  """Handle extension notifications"""
778
- 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))
779
801
 
780
802
  async def cleanup(self) -> None:
781
803
  """
@@ -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(
@@ -258,7 +258,7 @@ class AgentSession:
258
258
  model_identifier: str,
259
259
  db_uri: str = "sqlite:///crow.db",
260
260
  cwd: str = "/tmp",
261
- agent_idx: int = 0,
261
+ agent_idx: int = 1,
262
262
  session_id: str | None = None,
263
263
  initial_messages: list[dict[str, Any]] | None = None,
264
264
  ) -> "AgentSession":
@@ -378,6 +378,46 @@ class AgentSession:
378
378
  self._model = None
379
379
 
380
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
+
381
421
  def make_agent_session(
382
422
  config: Config,
383
423
  tools: list[dict],
@@ -397,6 +437,7 @@ def make_agent_session(
397
437
  template, name="crow-default", db_uri=config.db_uri
398
438
  )
399
439
  display_tree = get_directory_tree(cwd)
440
+ skills_catalog = get_skills_catalog(config.config_dir / "skills")
400
441
  agent_path = os.path.join(cwd, "AGENTS.typ")
401
442
  if os.path.exists(agent_path):
402
443
  with open(agent_path, "r") as f:
@@ -420,6 +461,7 @@ def make_agent_session(
420
461
  "display_tree": display_tree,
421
462
  "agents_content": agents_content,
422
463
  "session_id": session_id,
464
+ "skills_catalog": skills_catalog,
423
465
  },
424
466
  tool_definitions=tools,
425
467
  request_params={"temperature": 0.2},