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.
- {crow_cli-0.1.25 → crow_cli-0.1.26}/PKG-INFO +2 -2
- {crow_cli-0.1.25 → crow_cli-0.1.26}/README.md +1 -1
- {crow_cli-0.1.25 → crow_cli-0.1.26}/pyproject.toml +1 -1
- {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/configure.py +2 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/default/defaults.py +5 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/main.py +24 -2
- {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/react.py +107 -16
- {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/session.py +43 -1
- {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/tools.py +418 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/cli/main.py +2 -0
- {crow_cli-0.1.25 → 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.25 → crow_cli-0.1.26}/tests/analyze_payload_cache_invalidation.py +37 -17
- {crow_cli-0.1.25 → crow_cli-0.1.26}/tests/analyze_payload_deep.py +33 -14
- {crow_cli-0.1.25 → crow_cli-0.1.26}/tests/conftest.py +50 -0
- {crow_cli-0.1.25/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.25/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.25/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.25 → crow_cli-0.1.26}/tests/unit/test_uv_project_enforcement.py +19 -9
- {crow_cli-0.1.25 → crow_cli-0.1.26}/uv.lock +1 -1
- crow_cli-0.1.25/tests/test_acp_token_loss.py +0 -548
- crow_cli-0.1.25/tests/test_agent_init.py +0 -82
- crow_cli-0.1.25/tests/test_payload_integrity.py +0 -262
- crow_cli-0.1.25/tests/test_session_update_transmission.py +0 -546
- crow_cli-0.1.25/tests/test_token_loss_detection.py +0 -572
- crow_cli-0.1.25/tests/test_token_loss_reproduction.py +0 -259
- crow_cli-0.1.25/tests/unit/test_compact.py +0 -187
- {crow_cli-0.1.25 → crow_cli-0.1.26}/.gitignore +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/.python-version +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/agent.json +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/crow-cli.spec +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/examples/mc_escher_loop.py +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/examples/quick_test.py +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/run_tests.sh +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/__init__.py +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/__init__.py +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/compact.py +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/context.py +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/db.py +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/default/__init__.py +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/default/searxng_settings.py +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/hooks.py +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/llm.py +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/logger.py +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/mcp_client.py +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/prompt.py +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent/slash.py +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/agent_runner.py +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/cli/__init__.py +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/cli/init_cmd.py +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/cli/install.py +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/src/crow_cli/client/__init__.py +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/test_send_request_error_handling.py +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/tests/__init__.py +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/tests/unit/test_compact_last_messages_list_content.json +0 -0
- {crow_cli-0.1.25 → crow_cli-0.1.26}/tests/unit/test_compact_last_messages_list_content.py +0 -0
- {crow_cli-0.1.25/tests → crow_cli-0.1.26/tests/unit}/test_json_repair.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
-
|
|
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 =
|
|
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},
|