ata-coder 2.4.9__tar.gz → 2.5.0__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.
- {ata_coder-2.4.9/ata_coder.egg-info → ata_coder-2.5.0}/PKG-INFO +1 -1
- {ata_coder-2.4.9 → ata_coder-2.5.0}/agent.py +28 -13
- {ata_coder-2.4.9 → ata_coder-2.5.0}/agent_compact.py +2 -2
- {ata_coder-2.4.9 → ata_coder-2.5.0}/agent_controller.py +3 -1
- {ata_coder-2.4.9 → ata_coder-2.5.0}/agent_routing.py +1 -1
- {ata_coder-2.4.9 → ata_coder-2.5.0}/agent_tools.py +18 -8
- {ata_coder-2.4.9 → ata_coder-2.5.0}/anthropic_client.py +41 -12
- {ata_coder-2.4.9 → ata_coder-2.5.0/ata_coder.egg-info}/PKG-INFO +1 -1
- {ata_coder-2.4.9 → ata_coder-2.5.0}/ata_coder.egg-info/SOURCES.txt +0 -4
- {ata_coder-2.4.9 → ata_coder-2.5.0}/change_tracker.py +8 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/config.py +1 -1
- {ata_coder-2.4.9 → ata_coder-2.5.0}/core/queue.py +1 -1
- {ata_coder-2.4.9 → ata_coder-2.5.0}/extension.py +29 -15
- {ata_coder-2.4.9 → ata_coder-2.5.0}/fool_proof.py +7 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/git_workflow.py +3 -2
- {ata_coder-2.4.9 → ata_coder-2.5.0}/llm_client.py +12 -11
- {ata_coder-2.4.9 → ata_coder-2.5.0}/main.py +1 -1
- {ata_coder-2.4.9 → ata_coder-2.5.0}/mcp_client.py +9 -10
- {ata_coder-2.4.9 → ata_coder-2.5.0}/memory.py +40 -32
- {ata_coder-2.4.9 → ata_coder-2.5.0}/permissions.py +6 -4
- {ata_coder-2.4.9 → ata_coder-2.5.0}/privilege.py +20 -10
- {ata_coder-2.4.9 → ata_coder-2.5.0}/project.py +1 -1
- {ata_coder-2.4.9 → ata_coder-2.5.0}/prompt_template.py +60 -19
- {ata_coder-2.4.9 → ata_coder-2.5.0}/pyproject.toml +1 -1
- {ata_coder-2.4.9 → ata_coder-2.5.0}/repl_theme.py +1 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/repl_ui.py +6 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/safety_guard.py +15 -9
- {ata_coder-2.4.9 → ata_coder-2.5.0}/self_correct.py +5 -1
- {ata_coder-2.4.9 → ata_coder-2.5.0}/server.py +17 -4
- {ata_coder-2.4.9 → ata_coder-2.5.0}/server_rate_limit.py +8 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/server_session.py +3 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/server_shell.py +8 -6
- {ata_coder-2.4.9 → ata_coder-2.5.0}/session.py +4 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/settings.py +2 -1
- {ata_coder-2.4.9 → ata_coder-2.5.0}/setup_wizard.py +6 -1
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills.py +1 -1
- {ata_coder-2.4.9 → ata_coder-2.5.0}/sub_agent.py +16 -12
- {ata_coder-2.4.9 → ata_coder-2.5.0}/sub_agent_manager.py +12 -4
- {ata_coder-2.4.9 → ata_coder-2.5.0}/terminal.py +1 -2
- {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_safety_guard.py +3 -3
- {ata_coder-2.4.9 → ata_coder-2.5.0}/tools/executor.py +17 -12
- {ata_coder-2.4.9 → ata_coder-2.5.0}/tools/subagent.py +16 -5
- ata_coder-2.4.9/agent_undo.py +0 -63
- ata_coder-2.4.9/tools/file_ops.py +0 -183
- {ata_coder-2.4.9 → ata_coder-2.5.0}/LICENSE +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/MANIFEST.in +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/README.md +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/__init__.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/agent_extension.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/agent_subsystems.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/ata_coder.egg-info/dependency_links.txt +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/ata_coder.egg-info/entry_points.txt +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/ata_coder.egg-info/requires.txt +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/ata_coder.egg-info/top_level.txt +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/clawd_integration.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/commands/__init__.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/commands/_core.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/commands/_safety.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/commands/_settings.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/commands/_workflow.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/context_manager.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/core/__init__.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/core/events.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/core/state.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/event_queue.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/extensions/__init__.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/extensions/hello_skill.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/gui.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/model_registry.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/model_router.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/prompts/auto-mode.md +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/prompts/coding-rules.md +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/prompts/execution-guardrails.md +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/prompts/memory-system.md +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/prompts/output-style.md +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/prompts/safety.md +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/prompts/slash-commands.md +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/prompts/sub-agents.md +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/prompts/system-reminders.md +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/prompts/system.md +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/prompts/tool-policy.md +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/py.typed +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/repl_tracker.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/setup.cfg +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skill_extension.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/architect/SKILL.md +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/code-reviewer/SKILL.md +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/codecraft/SKILL.md +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/debugger/SKILL.md +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/doc-writer/SKILL.md +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/general-coder/SKILL.md +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/math-calculator/README.md +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/math-calculator/SKILL.md +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/math-calculator/handler.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/math-calculator/prompts/system.md +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/math-calculator/requirements.txt +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/math-calculator/resources/constants.json +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/math-calculator/tests/test_handler.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/security-auditor/SKILL.md +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/test-writer/SKILL.md +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/weather-skill/README.md +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/weather-skill/handler.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/weather-skill/manifest.json +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/weather-skill/prompts/system_prompt.txt +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/weather-skill/prompts/user_prompt_template.txt +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/weather-skill/requirements.txt +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/weather-skill/resources/city_list.json +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/weather-skill/resources/error_messages.json +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/weather-skill/tests/test_handler.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/weather-skill/weather_utils.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/system_prompt_builder.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/task_planner.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_agent.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_change_tracker.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_config.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_event_queue.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_extension.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_fibonacci.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_fool_proof.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_llm_client.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_memory.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_model_registry.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_permissions.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_privilege.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_prompt_template.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_server.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_skill_handlers.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_sub_agent.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_tools.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/token_counter.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/tools/__init__.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/tools/definitions.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/tools/result.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/tools/strategy.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/tools/web.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/types.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/utils.py +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/web/css/style.css +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/web/index.html +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/web/js/app.js +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/web/package-lock.json +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/web/package.json +0 -0
- {ata_coder-2.4.9 → ata_coder-2.5.0}/web/tsconfig.json +0 -0
|
@@ -319,14 +319,17 @@ class CoderAgent(CompactionMixin, ToolExecutionMixin,
|
|
|
319
319
|
raise
|
|
320
320
|
except Exception as e:
|
|
321
321
|
logger.critical("Agent fatal error: %s", e, exc_info=True)
|
|
322
|
-
|
|
323
|
-
|
|
322
|
+
# Sanitize — full details are in the log; never leak exception
|
|
323
|
+
# messages (which may contain paths / keys) to the caller.
|
|
324
|
+
self._emit(ErrorEvent("An unexpected error occurred. Check logs for details."))
|
|
325
|
+
return "An unexpected error occurred. Please check the logs for details."
|
|
324
326
|
finally:
|
|
325
327
|
self._state.phase = AgentPhase.SHUTDOWN
|
|
326
328
|
# Auto-save session after every task (best-effort, never crashes)
|
|
327
329
|
self._auto_save_session()
|
|
328
|
-
#
|
|
329
|
-
|
|
330
|
+
# Deactivate skill only for fresh-context runs; persistent
|
|
331
|
+
# (reset_context=False) sessions keep their skill active.
|
|
332
|
+
if self.skills and reset_context:
|
|
330
333
|
self.skills.deactivate()
|
|
331
334
|
|
|
332
335
|
async def _run_loop(self, task: str, stream: bool = True) -> str:
|
|
@@ -445,7 +448,7 @@ class CoderAgent(CompactionMixin, ToolExecutionMixin,
|
|
|
445
448
|
self._append_message(assistant_msg)
|
|
446
449
|
for tc, result in zip(tool_calls, results, strict=True):
|
|
447
450
|
self._warn_if_large_result(result, tc["function"]["name"])
|
|
448
|
-
self._store_tool_result(result, tc["id"])
|
|
451
|
+
self._store_tool_result(result, tc["id"], tool_name=tc["function"]["name"])
|
|
449
452
|
else:
|
|
450
453
|
# Clawd: one PreToolUse for the batch (not per-tool)
|
|
451
454
|
get_clawd().tool_use(
|
|
@@ -470,7 +473,7 @@ class CoderAgent(CompactionMixin, ToolExecutionMixin,
|
|
|
470
473
|
assistant_msg["reasoning_content"] = response["reasoning_content"]
|
|
471
474
|
self._append_message(assistant_msg)
|
|
472
475
|
for tc, result in zip(tool_calls, batch_results, strict=True):
|
|
473
|
-
self._store_tool_result(result, tc["id"])
|
|
476
|
+
self._store_tool_result(result, tc["id"], tool_name=tc["function"]["name"])
|
|
474
477
|
|
|
475
478
|
# Clawd: one PostToolUse for the serial batch
|
|
476
479
|
all_ok = all(r.success for r in batch_results)
|
|
@@ -584,6 +587,7 @@ class CoderAgent(CompactionMixin, ToolExecutionMixin,
|
|
|
584
587
|
Mirrors the main run() loop: skill tool filtering, token compaction,
|
|
585
588
|
consecutive-failure detection, and circuit breaker.
|
|
586
589
|
"""
|
|
590
|
+
self._state.phase = AgentPhase.THINKING
|
|
587
591
|
self._append_message({"role": "user", "content": message})
|
|
588
592
|
|
|
589
593
|
SAFETY_LIMIT = 999 # circuit breaker
|
|
@@ -626,9 +630,11 @@ class CoderAgent(CompactionMixin, ToolExecutionMixin,
|
|
|
626
630
|
text = response.get("content", "")
|
|
627
631
|
|
|
628
632
|
if not tool_calls:
|
|
633
|
+
self._state.phase = AgentPhase.COMPLETED
|
|
629
634
|
return text or "Done."
|
|
630
635
|
|
|
631
636
|
# Execute tool calls (serial for safety in follow-up context)
|
|
637
|
+
self._state.phase = AgentPhase.TOOL_EXECUTING
|
|
632
638
|
batch_results: list[ToolResult] = []
|
|
633
639
|
for tc in tool_calls:
|
|
634
640
|
self._state.tool_call_count += 1
|
|
@@ -642,12 +648,17 @@ class CoderAgent(CompactionMixin, ToolExecutionMixin,
|
|
|
642
648
|
batch_results.append(result)
|
|
643
649
|
self._warn_if_large_result(result, tool_name)
|
|
644
650
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
+
# Append ONE assistant message with ALL tool_calls (API protocol)
|
|
652
|
+
assistant_msg: dict[str, Any] = {
|
|
653
|
+
"role": "assistant", "content": text or None, "tool_calls": tool_calls,
|
|
654
|
+
}
|
|
655
|
+
if response.get("reasoning_content"):
|
|
656
|
+
assistant_msg["reasoning_content"] = response["reasoning_content"]
|
|
657
|
+
self._append_message(assistant_msg)
|
|
658
|
+
for tc, result in zip(tool_calls, batch_results, strict=True):
|
|
659
|
+
self._store_tool_result(result, tc["id"], tool_name=tc["function"]["name"])
|
|
660
|
+
|
|
661
|
+
self._state.phase = AgentPhase.THINKING # ready for next LLM turn
|
|
651
662
|
|
|
652
663
|
# ── Consecutive failure detection ───────────────────────────
|
|
653
664
|
if batch_results and not any(r.success for r in batch_results):
|
|
@@ -663,6 +674,7 @@ class CoderAgent(CompactionMixin, ToolExecutionMixin,
|
|
|
663
674
|
else:
|
|
664
675
|
_consecutive_failures = 0
|
|
665
676
|
|
|
677
|
+
self._state.phase = AgentPhase.COMPLETED
|
|
666
678
|
return text or "Done."
|
|
667
679
|
|
|
668
680
|
# ── Tool filtering → agent_tools.py (ToolExecutionMixin)
|
|
@@ -681,7 +693,10 @@ class CoderAgent(CompactionMixin, ToolExecutionMixin,
|
|
|
681
693
|
"""
|
|
682
694
|
# Refresh model name on each build (may have changed via /model)
|
|
683
695
|
self._prompt_builder.model = self.config.llm.model
|
|
684
|
-
|
|
696
|
+
prompt = self._prompt_builder.build(TOOL_DEFINITIONS, user_input=user_input)
|
|
697
|
+
# Trigger extension point: on_system_prompt_build
|
|
698
|
+
self._ep_on_system_prompt.trigger(prompt=prompt, task=user_input)
|
|
699
|
+
return prompt
|
|
685
700
|
|
|
686
701
|
# ── Memory commands ───────────────────────────────────────────────────
|
|
687
702
|
|
|
@@ -73,7 +73,7 @@ class CompactionMixin:
|
|
|
73
73
|
|
|
74
74
|
cm.replace_all(truncated)
|
|
75
75
|
self._cached_system_prompt = None # system msg may have shifted
|
|
76
|
-
self._state.messages = cm.messages # sync for backward compat
|
|
76
|
+
self._state.messages = list(cm.messages) # sync for backward compat (copy — avoid shared ref)
|
|
77
77
|
|
|
78
78
|
new_tokens = cm.token_total
|
|
79
79
|
logger.info("Compacted: %d→%d msgs, ~%d→%d tokens (files: %d, tools: %d)",
|
|
@@ -95,7 +95,7 @@ class CompactionMixin:
|
|
|
95
95
|
truncated, result = cm.build_truncated_list()
|
|
96
96
|
cm.replace_all(truncated)
|
|
97
97
|
self._cached_system_prompt = None
|
|
98
|
-
self._state.messages = cm.messages # sync
|
|
98
|
+
self._state.messages = list(cm.messages) # sync (copy — avoid shared ref)
|
|
99
99
|
logger.warning("Force-truncated: %d → %d messages (~%d tokens kept)",
|
|
100
100
|
result.old_count, result.new_count, result.new_tokens)
|
|
101
101
|
|
|
@@ -163,8 +163,10 @@ class AgentController:
|
|
|
163
163
|
raise
|
|
164
164
|
except Exception as e:
|
|
165
165
|
logger.exception("Agent task failed")
|
|
166
|
+
# Sanitize — full details are in the log; never leak exception
|
|
167
|
+
# messages to the event stream.
|
|
166
168
|
await self.event_queue.put(
|
|
167
|
-
ErrorEvent(
|
|
169
|
+
ErrorEvent("An unexpected error occurred. Check logs for details.")
|
|
168
170
|
)
|
|
169
171
|
await self.event_queue.put(
|
|
170
172
|
CompleteEvent(
|
|
@@ -75,7 +75,7 @@ class ModelRoutingMixin:
|
|
|
75
75
|
s = get_settings()
|
|
76
76
|
simple_max = s.get("complexity", "simple_max_chars", default=60)
|
|
77
77
|
complex_min = s.get("complexity", "complex_min_chars", default=500)
|
|
78
|
-
except
|
|
78
|
+
except (ImportError, AttributeError, KeyError):
|
|
79
79
|
simple_max, complex_min = 60, 500 # fallback defaults
|
|
80
80
|
|
|
81
81
|
if task_len <= simple_max:
|
|
@@ -123,7 +123,6 @@ class ToolExecutionMixin:
|
|
|
123
123
|
if diagnosis and diagnosis.retry_strategy == "auto_fix":
|
|
124
124
|
fixed_args = self.self_correct.suggest_fix(tool_name, arguments, diagnosis, error_message=result.error)
|
|
125
125
|
if fixed_args and fixed_args != arguments:
|
|
126
|
-
self._emit(ToolResultEvent(tool_name, result, source="builtin", arguments=arguments))
|
|
127
126
|
logger.info("Auto-correcting: %s (was: %s)", diagnosis.fix_suggestion[:80], result.error[:80])
|
|
128
127
|
# Retry with fixed args THROUGH the full safety pipeline
|
|
129
128
|
self._self_correct_depth += 1
|
|
@@ -157,6 +156,8 @@ class ToolExecutionMixin:
|
|
|
157
156
|
name = tc["function"]["name"]
|
|
158
157
|
if name == "run_shell":
|
|
159
158
|
return False # Shell commands have side effects, serialize
|
|
159
|
+
if name.startswith("mcp__"):
|
|
160
|
+
return False # MCP tools may have arbitrary side effects
|
|
160
161
|
if name in ("write_file", "edit_file"):
|
|
161
162
|
if pre_parsed and i in pre_parsed:
|
|
162
163
|
fp = pre_parsed[i].get("file_path", "")
|
|
@@ -248,12 +249,16 @@ class ToolExecutionMixin:
|
|
|
248
249
|
return json.dumps(mcp_result)
|
|
249
250
|
return str(mcp_result)
|
|
250
251
|
|
|
251
|
-
def _store_tool_result(self, result: ToolResult, tool_call_id: str
|
|
252
|
+
def _store_tool_result(self, result: ToolResult, tool_call_id: str,
|
|
253
|
+
tool_name: str = "") -> None:
|
|
252
254
|
"""Truncate tool output and append to message history.
|
|
253
255
|
|
|
254
256
|
Full output is available during execution, but only a capped version
|
|
255
257
|
is stored for future LLM turns to prevent context bloat.
|
|
256
258
|
"""
|
|
259
|
+
# Trigger extension point: on_tool_result
|
|
260
|
+
if tool_name:
|
|
261
|
+
self._ep_on_tool_result.trigger(tool_name=tool_name, result=result)
|
|
257
262
|
cap = self.config.agent.max_message_output_chars
|
|
258
263
|
content = result.to_message()
|
|
259
264
|
if len(content) > cap:
|
|
@@ -262,11 +267,13 @@ class ToolExecutionMixin:
|
|
|
262
267
|
+ f"\n\n... [truncated {len(content) - cap:,} chars "
|
|
263
268
|
+ f"from {result.output.count(chr(10)) + 1} lines]"
|
|
264
269
|
)
|
|
265
|
-
|
|
270
|
+
tool_msg = {
|
|
266
271
|
"role": "tool",
|
|
267
272
|
"tool_call_id": tool_call_id,
|
|
268
273
|
"content": content,
|
|
269
|
-
}
|
|
274
|
+
}
|
|
275
|
+
self._state.messages.append(tool_msg)
|
|
276
|
+
self._context_manager.append(tool_msg) # keep CM token tracking in sync
|
|
270
277
|
|
|
271
278
|
@staticmethod
|
|
272
279
|
def _warn_if_large_result(result: ToolResult, tool_name: str) -> None:
|
|
@@ -297,10 +304,13 @@ class ToolExecutionMixin:
|
|
|
297
304
|
# Check file cache first (populated by _tool_read_file)
|
|
298
305
|
# Cache format: (mtime, cached_at, content) — 3-tuple with LRU timestamp
|
|
299
306
|
cache_key = str(p.resolve())
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
307
|
+
try:
|
|
308
|
+
if cache_key in self.tools._file_cache:
|
|
309
|
+
cached_mtime, _, cached_content = self.tools._file_cache[cache_key]
|
|
310
|
+
if cached_mtime == p.stat().st_mtime:
|
|
311
|
+
return cached_content
|
|
312
|
+
except (ValueError, KeyError):
|
|
313
|
+
pass # cache format changed — fall through to disk read
|
|
304
314
|
|
|
305
315
|
try:
|
|
306
316
|
# Safety: skip files > 50MB to avoid OOM
|
|
@@ -72,9 +72,9 @@ class AnthropicClient(BaseLLMClient):
|
|
|
72
72
|
"x-api-key": self.config.api_key,
|
|
73
73
|
"Content-Type": "application/json",
|
|
74
74
|
}
|
|
75
|
-
# Native Anthropic requires this header
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
# Native Anthropic requires this header (default: 2023-06-01).
|
|
76
|
+
# Proxies may ignore it; override via ANTHROPIC_VERSION env var.
|
|
77
|
+
self._headers["anthropic-version"] = os.getenv("ANTHROPIC_VERSION", "2023-06-01")
|
|
78
78
|
|
|
79
79
|
self._client = httpx.AsyncClient(
|
|
80
80
|
timeout=httpx.Timeout(300.0, connect=30.0),
|
|
@@ -263,6 +263,8 @@ class AnthropicClient(BaseLLMClient):
|
|
|
263
263
|
)
|
|
264
264
|
|
|
265
265
|
tool_buf: dict[int, dict] = {}
|
|
266
|
+
prompt_tokens = 0
|
|
267
|
+
completion_tokens = 0
|
|
266
268
|
async for line in resp.aiter_lines():
|
|
267
269
|
if not line or not line.startswith("data: "):
|
|
268
270
|
continue
|
|
@@ -278,6 +280,17 @@ class AnthropicClient(BaseLLMClient):
|
|
|
278
280
|
delta = event.get("delta", {})
|
|
279
281
|
idx = event.get("index", 0)
|
|
280
282
|
|
|
283
|
+
# Track usage from streaming events (Anthropic protocol)
|
|
284
|
+
if evt_type == "message_start":
|
|
285
|
+
msg = event.get("message", {})
|
|
286
|
+
usage = msg.get("usage", {})
|
|
287
|
+
if usage.get("input_tokens"):
|
|
288
|
+
prompt_tokens = usage["input_tokens"]
|
|
289
|
+
elif evt_type == "message_delta":
|
|
290
|
+
usage = delta.get("usage", {})
|
|
291
|
+
if usage.get("output_tokens"):
|
|
292
|
+
completion_tokens = usage["output_tokens"]
|
|
293
|
+
|
|
281
294
|
if evt_type == "content_block_delta":
|
|
282
295
|
dt = delta.get("type", "")
|
|
283
296
|
if dt == "text_delta":
|
|
@@ -297,6 +310,18 @@ class AnthropicClient(BaseLLMClient):
|
|
|
297
310
|
elif evt_type == "message_stop":
|
|
298
311
|
yield ("finish", "end_turn")
|
|
299
312
|
|
|
313
|
+
# Update token counters with streamed usage data
|
|
314
|
+
if prompt_tokens:
|
|
315
|
+
self._total_prompt_tokens += prompt_tokens
|
|
316
|
+
self.last_exact_prompt_tokens = prompt_tokens
|
|
317
|
+
if completion_tokens:
|
|
318
|
+
self._total_completion_tokens += completion_tokens
|
|
319
|
+
if self._usage_callback and (prompt_tokens or completion_tokens):
|
|
320
|
+
self._usage_callback(
|
|
321
|
+
prompt_tokens=prompt_tokens,
|
|
322
|
+
completion_tokens=completion_tokens,
|
|
323
|
+
)
|
|
324
|
+
|
|
300
325
|
# Yield tool calls
|
|
301
326
|
for idx in sorted(tool_buf.keys()):
|
|
302
327
|
buf = tool_buf[idx]
|
|
@@ -345,7 +370,11 @@ class AnthropicClient(BaseLLMClient):
|
|
|
345
370
|
elif ch in (']', '}'):
|
|
346
371
|
if stack and stack[-1] == ch:
|
|
347
372
|
stack.pop()
|
|
348
|
-
|
|
373
|
+
# Close any unterminated string before balancing brackets
|
|
374
|
+
result = text
|
|
375
|
+
if in_string and not escape:
|
|
376
|
+
result += '"'
|
|
377
|
+
return result + ''.join(reversed(stack))
|
|
349
378
|
|
|
350
379
|
def _apply_thinking(self, body: dict) -> None:
|
|
351
380
|
"""Apply thinking/reasoning_effort — provider-agnostic.
|
|
@@ -431,18 +460,18 @@ class AnthropicClient(BaseLLMClient):
|
|
|
431
460
|
out = usage.get("output_tokens", 0)
|
|
432
461
|
if inp:
|
|
433
462
|
self.last_exact_prompt_tokens = inp
|
|
434
|
-
|
|
435
|
-
#
|
|
463
|
+
if out:
|
|
464
|
+
# Only estimate output tokens when API doesn't provide them;
|
|
465
|
+
# never estimate prompt tokens from output text.
|
|
466
|
+
pass
|
|
467
|
+
elif texts:
|
|
436
468
|
from .token_counter import _cjk_estimate
|
|
437
469
|
out_text = "\n".join(texts)
|
|
438
|
-
|
|
439
|
-
[{"role": "user", "content": out_text}]
|
|
440
|
-
))
|
|
441
|
-
out = max(1, _cjk_estimate(out_text) or out_text and len(out_text) // 4 or 1)
|
|
470
|
+
out = max(1, _cjk_estimate(out_text))
|
|
442
471
|
self._total_prompt_tokens += inp
|
|
443
|
-
self._total_completion_tokens += out
|
|
472
|
+
self._total_completion_tokens += out
|
|
444
473
|
if self._usage_callback:
|
|
445
|
-
self._usage_callback(inp, out
|
|
474
|
+
self._usage_callback(inp, out)
|
|
446
475
|
|
|
447
476
|
return result
|
|
448
477
|
|
|
@@ -9,7 +9,6 @@ agent_extension.py
|
|
|
9
9
|
agent_routing.py
|
|
10
10
|
agent_subsystems.py
|
|
11
11
|
agent_tools.py
|
|
12
|
-
agent_undo.py
|
|
13
12
|
anthropic_client.py
|
|
14
13
|
change_tracker.py
|
|
15
14
|
clawd_integration.py
|
|
@@ -62,7 +61,6 @@ utils.py
|
|
|
62
61
|
./agent_routing.py
|
|
63
62
|
./agent_subsystems.py
|
|
64
63
|
./agent_tools.py
|
|
65
|
-
./agent_undo.py
|
|
66
64
|
./anthropic_client.py
|
|
67
65
|
./change_tracker.py
|
|
68
66
|
./clawd_integration.py
|
|
@@ -156,7 +154,6 @@ utils.py
|
|
|
156
154
|
./tools/__init__.py
|
|
157
155
|
./tools/definitions.py
|
|
158
156
|
./tools/executor.py
|
|
159
|
-
./tools/file_ops.py
|
|
160
157
|
./tools/result.py
|
|
161
158
|
./tools/strategy.py
|
|
162
159
|
./tools/subagent.py
|
|
@@ -241,7 +238,6 @@ tests/test_tools.py
|
|
|
241
238
|
tools/__init__.py
|
|
242
239
|
tools/definitions.py
|
|
243
240
|
tools/executor.py
|
|
244
|
-
tools/file_ops.py
|
|
245
241
|
tools/result.py
|
|
246
242
|
tools/strategy.py
|
|
247
243
|
tools/subagent.py
|
|
@@ -109,6 +109,7 @@ class ChangeTracker:
|
|
|
109
109
|
self._backups: dict[str, str] = {}
|
|
110
110
|
self._dry_run = False
|
|
111
111
|
self._last_active: int = -1
|
|
112
|
+
self.workspace: Path | None = None # set by agent for workspace boundary checks
|
|
112
113
|
|
|
113
114
|
# ── Dry run toggle ───────────────────────────────────────────────────
|
|
114
115
|
|
|
@@ -245,6 +246,13 @@ class ChangeTracker:
|
|
|
245
246
|
def _apply_revert(self, c: FileChange) -> None:
|
|
246
247
|
"""Apply revert for a single change."""
|
|
247
248
|
path = Path(c.file_path)
|
|
249
|
+
# Safety: skip paths outside the workspace (defense in depth)
|
|
250
|
+
if self.workspace is not None:
|
|
251
|
+
try:
|
|
252
|
+
path.resolve().relative_to(self.workspace.resolve())
|
|
253
|
+
except ValueError:
|
|
254
|
+
logger.warning("Skipping undo outside workspace: %s", c.file_path)
|
|
255
|
+
return
|
|
248
256
|
if c.change_type == ChangeType.WRITE:
|
|
249
257
|
if c.old_content is None:
|
|
250
258
|
if path.exists():
|
|
@@ -273,7 +273,7 @@ def _settings_base_url() -> str:
|
|
|
273
273
|
|
|
274
274
|
|
|
275
275
|
def _settings_default_model() -> str:
|
|
276
|
-
return _from_settings("default_model", "deepseek-
|
|
276
|
+
return _from_settings("default_model", "deepseek-v4-pro")
|
|
277
277
|
|
|
278
278
|
|
|
279
279
|
def _settings_max_output_tokens() -> int:
|
|
@@ -42,7 +42,7 @@ class EventQueue:
|
|
|
42
42
|
async def get(self, timeout: Optional[float] = None) -> Optional[Any]:
|
|
43
43
|
"""Get one event, blocking with optional timeout."""
|
|
44
44
|
try:
|
|
45
|
-
if timeout:
|
|
45
|
+
if timeout is not None:
|
|
46
46
|
event = await asyncio.wait_for(self._queue.get(), timeout=timeout)
|
|
47
47
|
else:
|
|
48
48
|
event = await self._queue.get()
|
|
@@ -262,6 +262,7 @@ class ExtensionManager:
|
|
|
262
262
|
def __init__(self):
|
|
263
263
|
self._extensions: dict[str, Extension] = {}
|
|
264
264
|
self._active: set[str] = set()
|
|
265
|
+
self._activating: set[str] = set() # cycle detection stack
|
|
265
266
|
self._loaded_dirs: list[Path] = []
|
|
266
267
|
self._lock = threading.Lock() # protects _extensions, _active, _loaded_dirs
|
|
267
268
|
|
|
@@ -358,25 +359,38 @@ class ExtensionManager:
|
|
|
358
359
|
return False
|
|
359
360
|
if name in self._active:
|
|
360
361
|
return True # already active
|
|
362
|
+
# Cycle detection — detect circular dependencies
|
|
363
|
+
if name in self._activating:
|
|
364
|
+
logger.error(
|
|
365
|
+
"Circular dependency detected: %s is already being activated. "
|
|
366
|
+
"Active path: %s",
|
|
367
|
+
name, ", ".join(self._activating),
|
|
368
|
+
)
|
|
369
|
+
return False
|
|
370
|
+
self._activating.add(name)
|
|
361
371
|
deps = list(ext.meta.dependencies)
|
|
362
372
|
|
|
363
|
-
# Activate dependencies (try raw name first, then skill: prefix)
|
|
364
|
-
for dep in deps:
|
|
365
|
-
if dep not in self._active:
|
|
366
|
-
if not self.activate(dep):
|
|
367
|
-
self.activate(f"skill:{dep}")
|
|
368
|
-
|
|
369
|
-
# on_activate 在锁外调用,避免死锁
|
|
370
373
|
try:
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
374
|
+
# Activate dependencies (try raw name first, then skill: prefix)
|
|
375
|
+
for dep in deps:
|
|
376
|
+
if dep not in self._active:
|
|
377
|
+
if not self.activate(dep):
|
|
378
|
+
self.activate(f"skill:{dep}")
|
|
375
379
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
+
# on_activate 在锁外调用,避免死锁
|
|
381
|
+
try:
|
|
382
|
+
ext.on_activate()
|
|
383
|
+
except Exception:
|
|
384
|
+
logger.exception("Extension %r on_activate failed", name)
|
|
385
|
+
return False
|
|
386
|
+
|
|
387
|
+
with self._lock:
|
|
388
|
+
self._active.add(name)
|
|
389
|
+
logger.debug("Extension activated: %s", name)
|
|
390
|
+
return True
|
|
391
|
+
finally:
|
|
392
|
+
with self._lock:
|
|
393
|
+
self._activating.discard(name)
|
|
380
394
|
|
|
381
395
|
def deactivate(self, name: str) -> bool:
|
|
382
396
|
"""停用一个扩展(线程安全)。"""
|
|
@@ -126,6 +126,13 @@ class FoolProofEngine:
|
|
|
126
126
|
self._blocks += 1
|
|
127
127
|
return check
|
|
128
128
|
|
|
129
|
+
# 1b. Typing confirmation (safety guard flagged requires_typing)
|
|
130
|
+
if safety.requires_typing:
|
|
131
|
+
check.action = ActionRequired.WARN_CONFIRM
|
|
132
|
+
check.confirm_message = safety.reason or "Type 'YES' to confirm this operation."
|
|
133
|
+
check.requires_typing = True
|
|
134
|
+
return check
|
|
135
|
+
|
|
129
136
|
# 2. Read tools — always safe
|
|
130
137
|
if category == "read":
|
|
131
138
|
check.allowed = True
|
|
@@ -221,10 +221,11 @@ class GitWorkflow:
|
|
|
221
221
|
if code != 0:
|
|
222
222
|
return False, err
|
|
223
223
|
|
|
224
|
-
# Check for secrets in staged changes
|
|
224
|
+
# Check for secrets in staged changes — block the commit
|
|
225
225
|
secret_check = self._check_secrets()
|
|
226
226
|
if secret_check:
|
|
227
|
-
logger.
|
|
227
|
+
logger.error("Potential secrets in commit — blocked: %s", secret_check)
|
|
228
|
+
return False, f"Secret detection blocked commit:\n{secret_check}\n\nUse --force to override."
|
|
228
229
|
|
|
229
230
|
# Generate commit message if not provided
|
|
230
231
|
if not message:
|
|
@@ -424,19 +424,18 @@ class LLMClient(BaseLLMClient):
|
|
|
424
424
|
for attempt in range(self._max_retries + 1):
|
|
425
425
|
try:
|
|
426
426
|
response = await self._client.post(self._api_url, json=body)
|
|
427
|
-
except httpx.ConnectError as e:
|
|
427
|
+
except (httpx.ConnectError, httpx.ReadTimeout) as e:
|
|
428
|
+
last_error = str(e)
|
|
429
|
+
if attempt < self._max_retries:
|
|
430
|
+
delay = self._retry_base_delay * (2 ** attempt) * (0.5 + random.random())
|
|
431
|
+
logger.warning("Transient network error, retrying in %.1fs: %s", delay, e)
|
|
432
|
+
await asyncio.sleep(delay)
|
|
433
|
+
continue
|
|
428
434
|
raise RuntimeError(
|
|
429
|
-
f"Cannot
|
|
430
|
-
f" Check
|
|
431
|
-
f" Current: {self.config.base_url}\n"
|
|
435
|
+
f"Cannot reach {self._api_url} after {self._max_retries + 1} attempts.\n"
|
|
436
|
+
f" Check your connection and the server URL.\n"
|
|
432
437
|
f" Detail: {e}"
|
|
433
438
|
)
|
|
434
|
-
except httpx.ReadTimeout:
|
|
435
|
-
raise RuntimeError(
|
|
436
|
-
"Request timed out after 300s.\n"
|
|
437
|
-
" The model may be overloaded or the prompt too large.\n"
|
|
438
|
-
" Try again or reduce the task complexity."
|
|
439
|
-
)
|
|
440
439
|
except httpx.RemoteProtocolError as e:
|
|
441
440
|
last_error = str(e)
|
|
442
441
|
if attempt < self._max_retries:
|
|
@@ -457,7 +456,9 @@ class LLMClient(BaseLLMClient):
|
|
|
457
456
|
try:
|
|
458
457
|
delay = float(retry_after) if retry_after else self._retry_base_delay * (2 ** attempt)
|
|
459
458
|
except ValueError:
|
|
460
|
-
delay = self._retry_base_delay * (2 ** attempt)
|
|
459
|
+
delay = self._retry_base_delay * (2 ** attempt)
|
|
460
|
+
# Always apply jitter to prevent thundering-herd
|
|
461
|
+
delay *= (0.5 + random.random())
|
|
461
462
|
delay = min(delay, 60.0) # cap at 60s
|
|
462
463
|
|
|
463
464
|
if attempt < self._max_retries:
|
|
@@ -308,11 +308,10 @@ class MCPServerConnection:
|
|
|
308
308
|
class StdioMCPConnection(MCPServerConnection):
|
|
309
309
|
"""MCP connection over stdio (subprocess)."""
|
|
310
310
|
|
|
311
|
-
_next_req_id = 0
|
|
312
|
-
|
|
313
311
|
def __init__(self, name: str, command: str, args: list[str] | None = None,
|
|
314
312
|
env: dict[str, str] | None = None, cwd: str | None = None):
|
|
315
313
|
super().__init__(name)
|
|
314
|
+
self._next_req_id = 0 # per-instance counter (was shared class var)
|
|
316
315
|
self.command = command
|
|
317
316
|
self.args = args or []
|
|
318
317
|
self.env = env
|
|
@@ -325,9 +324,9 @@ class StdioMCPConnection(MCPServerConnection):
|
|
|
325
324
|
self._on_progress: Callable[[int, int, str | None], None] | None = None
|
|
326
325
|
|
|
327
326
|
@classmethod
|
|
328
|
-
def _next_id(
|
|
329
|
-
|
|
330
|
-
return str(
|
|
327
|
+
def _next_id(self) -> str:
|
|
328
|
+
self._next_req_id += 1
|
|
329
|
+
return str(self._next_req_id)
|
|
331
330
|
|
|
332
331
|
def on_progress(self, callback: Callable[[int, int, str | None], None]) -> None:
|
|
333
332
|
"""Register a callback for progress notifications."""
|
|
@@ -815,7 +814,7 @@ class MCPClient:
|
|
|
815
814
|
tool["_mcp_original_name"] = tool_name
|
|
816
815
|
self._all_tools.append(tool)
|
|
817
816
|
|
|
818
|
-
def refresh_tools(self, server_name: str | None = None) -> None:
|
|
817
|
+
async def refresh_tools(self, server_name: str | None = None) -> None:
|
|
819
818
|
"""Re-discover and re-register tools from one or all servers."""
|
|
820
819
|
names = [server_name] if server_name else list(self._connections)
|
|
821
820
|
for name in names:
|
|
@@ -826,7 +825,7 @@ class MCPClient:
|
|
|
826
825
|
self._all_tools = [t for t in self._all_tools if t.get("_mcp_server") != name]
|
|
827
826
|
self._tool_to_server = {k: v for k, v in self._tool_to_server.items() if v != name}
|
|
828
827
|
# Re-discover and register
|
|
829
|
-
conn.discover()
|
|
828
|
+
await conn.discover()
|
|
830
829
|
self._register_server_tools(name, conn)
|
|
831
830
|
|
|
832
831
|
# ── Tool access ─────────────────────────────────────────────────────────
|
|
@@ -945,7 +944,7 @@ class MCPClient:
|
|
|
945
944
|
|
|
946
945
|
# ── Resource cache ──────────────────────────────────────────────────────
|
|
947
946
|
|
|
948
|
-
def cached_read_resource(self, uri: str) -> dict[str, Any]:
|
|
947
|
+
async def cached_read_resource(self, uri: str) -> dict[str, Any]:
|
|
949
948
|
"""Read a resource with LRU+TTL caching."""
|
|
950
949
|
now = time.time()
|
|
951
950
|
if uri in self._resource_cache:
|
|
@@ -959,7 +958,7 @@ class MCPClient:
|
|
|
959
958
|
for conn in self._connections.values():
|
|
960
959
|
for res in conn.resources:
|
|
961
960
|
if res.get("uri") == uri:
|
|
962
|
-
result = conn.read_resource(uri)
|
|
961
|
+
result = await conn.read_resource(uri)
|
|
963
962
|
content = result.get("contents", result)
|
|
964
963
|
if len(self._resource_cache) >= self._resource_cache_max:
|
|
965
964
|
self._resource_cache.popitem(last=False)
|
|
@@ -973,7 +972,7 @@ class MCPClient:
|
|
|
973
972
|
# Simple match: if URI starts with the template prefix
|
|
974
973
|
prefix = tmpl_uri.split("{")[0] if "{" in tmpl_uri else tmpl_uri
|
|
975
974
|
if uri.startswith(prefix):
|
|
976
|
-
result = conn.read_resource(uri)
|
|
975
|
+
result = await conn.read_resource(uri)
|
|
977
976
|
content = result.get("contents", result)
|
|
978
977
|
if len(self._resource_cache) >= self._resource_cache_max:
|
|
979
978
|
self._resource_cache.popitem(last=False)
|