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.
Files changed (143) hide show
  1. {ata_coder-2.4.9/ata_coder.egg-info → ata_coder-2.5.0}/PKG-INFO +1 -1
  2. {ata_coder-2.4.9 → ata_coder-2.5.0}/agent.py +28 -13
  3. {ata_coder-2.4.9 → ata_coder-2.5.0}/agent_compact.py +2 -2
  4. {ata_coder-2.4.9 → ata_coder-2.5.0}/agent_controller.py +3 -1
  5. {ata_coder-2.4.9 → ata_coder-2.5.0}/agent_routing.py +1 -1
  6. {ata_coder-2.4.9 → ata_coder-2.5.0}/agent_tools.py +18 -8
  7. {ata_coder-2.4.9 → ata_coder-2.5.0}/anthropic_client.py +41 -12
  8. {ata_coder-2.4.9 → ata_coder-2.5.0/ata_coder.egg-info}/PKG-INFO +1 -1
  9. {ata_coder-2.4.9 → ata_coder-2.5.0}/ata_coder.egg-info/SOURCES.txt +0 -4
  10. {ata_coder-2.4.9 → ata_coder-2.5.0}/change_tracker.py +8 -0
  11. {ata_coder-2.4.9 → ata_coder-2.5.0}/config.py +1 -1
  12. {ata_coder-2.4.9 → ata_coder-2.5.0}/core/queue.py +1 -1
  13. {ata_coder-2.4.9 → ata_coder-2.5.0}/extension.py +29 -15
  14. {ata_coder-2.4.9 → ata_coder-2.5.0}/fool_proof.py +7 -0
  15. {ata_coder-2.4.9 → ata_coder-2.5.0}/git_workflow.py +3 -2
  16. {ata_coder-2.4.9 → ata_coder-2.5.0}/llm_client.py +12 -11
  17. {ata_coder-2.4.9 → ata_coder-2.5.0}/main.py +1 -1
  18. {ata_coder-2.4.9 → ata_coder-2.5.0}/mcp_client.py +9 -10
  19. {ata_coder-2.4.9 → ata_coder-2.5.0}/memory.py +40 -32
  20. {ata_coder-2.4.9 → ata_coder-2.5.0}/permissions.py +6 -4
  21. {ata_coder-2.4.9 → ata_coder-2.5.0}/privilege.py +20 -10
  22. {ata_coder-2.4.9 → ata_coder-2.5.0}/project.py +1 -1
  23. {ata_coder-2.4.9 → ata_coder-2.5.0}/prompt_template.py +60 -19
  24. {ata_coder-2.4.9 → ata_coder-2.5.0}/pyproject.toml +1 -1
  25. {ata_coder-2.4.9 → ata_coder-2.5.0}/repl_theme.py +1 -0
  26. {ata_coder-2.4.9 → ata_coder-2.5.0}/repl_ui.py +6 -0
  27. {ata_coder-2.4.9 → ata_coder-2.5.0}/safety_guard.py +15 -9
  28. {ata_coder-2.4.9 → ata_coder-2.5.0}/self_correct.py +5 -1
  29. {ata_coder-2.4.9 → ata_coder-2.5.0}/server.py +17 -4
  30. {ata_coder-2.4.9 → ata_coder-2.5.0}/server_rate_limit.py +8 -0
  31. {ata_coder-2.4.9 → ata_coder-2.5.0}/server_session.py +3 -0
  32. {ata_coder-2.4.9 → ata_coder-2.5.0}/server_shell.py +8 -6
  33. {ata_coder-2.4.9 → ata_coder-2.5.0}/session.py +4 -0
  34. {ata_coder-2.4.9 → ata_coder-2.5.0}/settings.py +2 -1
  35. {ata_coder-2.4.9 → ata_coder-2.5.0}/setup_wizard.py +6 -1
  36. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills.py +1 -1
  37. {ata_coder-2.4.9 → ata_coder-2.5.0}/sub_agent.py +16 -12
  38. {ata_coder-2.4.9 → ata_coder-2.5.0}/sub_agent_manager.py +12 -4
  39. {ata_coder-2.4.9 → ata_coder-2.5.0}/terminal.py +1 -2
  40. {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_safety_guard.py +3 -3
  41. {ata_coder-2.4.9 → ata_coder-2.5.0}/tools/executor.py +17 -12
  42. {ata_coder-2.4.9 → ata_coder-2.5.0}/tools/subagent.py +16 -5
  43. ata_coder-2.4.9/agent_undo.py +0 -63
  44. ata_coder-2.4.9/tools/file_ops.py +0 -183
  45. {ata_coder-2.4.9 → ata_coder-2.5.0}/LICENSE +0 -0
  46. {ata_coder-2.4.9 → ata_coder-2.5.0}/MANIFEST.in +0 -0
  47. {ata_coder-2.4.9 → ata_coder-2.5.0}/README.md +0 -0
  48. {ata_coder-2.4.9 → ata_coder-2.5.0}/__init__.py +0 -0
  49. {ata_coder-2.4.9 → ata_coder-2.5.0}/agent_extension.py +0 -0
  50. {ata_coder-2.4.9 → ata_coder-2.5.0}/agent_subsystems.py +0 -0
  51. {ata_coder-2.4.9 → ata_coder-2.5.0}/ata_coder.egg-info/dependency_links.txt +0 -0
  52. {ata_coder-2.4.9 → ata_coder-2.5.0}/ata_coder.egg-info/entry_points.txt +0 -0
  53. {ata_coder-2.4.9 → ata_coder-2.5.0}/ata_coder.egg-info/requires.txt +0 -0
  54. {ata_coder-2.4.9 → ata_coder-2.5.0}/ata_coder.egg-info/top_level.txt +0 -0
  55. {ata_coder-2.4.9 → ata_coder-2.5.0}/clawd_integration.py +0 -0
  56. {ata_coder-2.4.9 → ata_coder-2.5.0}/commands/__init__.py +0 -0
  57. {ata_coder-2.4.9 → ata_coder-2.5.0}/commands/_core.py +0 -0
  58. {ata_coder-2.4.9 → ata_coder-2.5.0}/commands/_safety.py +0 -0
  59. {ata_coder-2.4.9 → ata_coder-2.5.0}/commands/_settings.py +0 -0
  60. {ata_coder-2.4.9 → ata_coder-2.5.0}/commands/_workflow.py +0 -0
  61. {ata_coder-2.4.9 → ata_coder-2.5.0}/context_manager.py +0 -0
  62. {ata_coder-2.4.9 → ata_coder-2.5.0}/core/__init__.py +0 -0
  63. {ata_coder-2.4.9 → ata_coder-2.5.0}/core/events.py +0 -0
  64. {ata_coder-2.4.9 → ata_coder-2.5.0}/core/state.py +0 -0
  65. {ata_coder-2.4.9 → ata_coder-2.5.0}/event_queue.py +0 -0
  66. {ata_coder-2.4.9 → ata_coder-2.5.0}/extensions/__init__.py +0 -0
  67. {ata_coder-2.4.9 → ata_coder-2.5.0}/extensions/hello_skill.py +0 -0
  68. {ata_coder-2.4.9 → ata_coder-2.5.0}/gui.py +0 -0
  69. {ata_coder-2.4.9 → ata_coder-2.5.0}/model_registry.py +0 -0
  70. {ata_coder-2.4.9 → ata_coder-2.5.0}/model_router.py +0 -0
  71. {ata_coder-2.4.9 → ata_coder-2.5.0}/prompts/auto-mode.md +0 -0
  72. {ata_coder-2.4.9 → ata_coder-2.5.0}/prompts/coding-rules.md +0 -0
  73. {ata_coder-2.4.9 → ata_coder-2.5.0}/prompts/execution-guardrails.md +0 -0
  74. {ata_coder-2.4.9 → ata_coder-2.5.0}/prompts/memory-system.md +0 -0
  75. {ata_coder-2.4.9 → ata_coder-2.5.0}/prompts/output-style.md +0 -0
  76. {ata_coder-2.4.9 → ata_coder-2.5.0}/prompts/safety.md +0 -0
  77. {ata_coder-2.4.9 → ata_coder-2.5.0}/prompts/slash-commands.md +0 -0
  78. {ata_coder-2.4.9 → ata_coder-2.5.0}/prompts/sub-agents.md +0 -0
  79. {ata_coder-2.4.9 → ata_coder-2.5.0}/prompts/system-reminders.md +0 -0
  80. {ata_coder-2.4.9 → ata_coder-2.5.0}/prompts/system.md +0 -0
  81. {ata_coder-2.4.9 → ata_coder-2.5.0}/prompts/tool-policy.md +0 -0
  82. {ata_coder-2.4.9 → ata_coder-2.5.0}/py.typed +0 -0
  83. {ata_coder-2.4.9 → ata_coder-2.5.0}/repl_tracker.py +0 -0
  84. {ata_coder-2.4.9 → ata_coder-2.5.0}/setup.cfg +0 -0
  85. {ata_coder-2.4.9 → ata_coder-2.5.0}/skill_extension.py +0 -0
  86. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/architect/SKILL.md +0 -0
  87. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/code-reviewer/SKILL.md +0 -0
  88. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/codecraft/SKILL.md +0 -0
  89. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/debugger/SKILL.md +0 -0
  90. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/doc-writer/SKILL.md +0 -0
  91. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/general-coder/SKILL.md +0 -0
  92. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/math-calculator/README.md +0 -0
  93. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/math-calculator/SKILL.md +0 -0
  94. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/math-calculator/handler.py +0 -0
  95. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/math-calculator/prompts/system.md +0 -0
  96. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/math-calculator/requirements.txt +0 -0
  97. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/math-calculator/resources/constants.json +0 -0
  98. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/math-calculator/tests/test_handler.py +0 -0
  99. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/security-auditor/SKILL.md +0 -0
  100. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/test-writer/SKILL.md +0 -0
  101. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/weather-skill/README.md +0 -0
  102. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/weather-skill/handler.py +0 -0
  103. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/weather-skill/manifest.json +0 -0
  104. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/weather-skill/prompts/system_prompt.txt +0 -0
  105. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/weather-skill/prompts/user_prompt_template.txt +0 -0
  106. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/weather-skill/requirements.txt +0 -0
  107. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/weather-skill/resources/city_list.json +0 -0
  108. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/weather-skill/resources/error_messages.json +0 -0
  109. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/weather-skill/tests/test_handler.py +0 -0
  110. {ata_coder-2.4.9 → ata_coder-2.5.0}/skills/weather-skill/weather_utils.py +0 -0
  111. {ata_coder-2.4.9 → ata_coder-2.5.0}/system_prompt_builder.py +0 -0
  112. {ata_coder-2.4.9 → ata_coder-2.5.0}/task_planner.py +0 -0
  113. {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_agent.py +0 -0
  114. {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_change_tracker.py +0 -0
  115. {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_config.py +0 -0
  116. {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_event_queue.py +0 -0
  117. {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_extension.py +0 -0
  118. {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_fibonacci.py +0 -0
  119. {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_fool_proof.py +0 -0
  120. {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_llm_client.py +0 -0
  121. {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_memory.py +0 -0
  122. {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_model_registry.py +0 -0
  123. {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_permissions.py +0 -0
  124. {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_privilege.py +0 -0
  125. {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_prompt_template.py +0 -0
  126. {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_server.py +0 -0
  127. {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_skill_handlers.py +0 -0
  128. {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_sub_agent.py +0 -0
  129. {ata_coder-2.4.9 → ata_coder-2.5.0}/tests/test_tools.py +0 -0
  130. {ata_coder-2.4.9 → ata_coder-2.5.0}/token_counter.py +0 -0
  131. {ata_coder-2.4.9 → ata_coder-2.5.0}/tools/__init__.py +0 -0
  132. {ata_coder-2.4.9 → ata_coder-2.5.0}/tools/definitions.py +0 -0
  133. {ata_coder-2.4.9 → ata_coder-2.5.0}/tools/result.py +0 -0
  134. {ata_coder-2.4.9 → ata_coder-2.5.0}/tools/strategy.py +0 -0
  135. {ata_coder-2.4.9 → ata_coder-2.5.0}/tools/web.py +0 -0
  136. {ata_coder-2.4.9 → ata_coder-2.5.0}/types.py +0 -0
  137. {ata_coder-2.4.9 → ata_coder-2.5.0}/utils.py +0 -0
  138. {ata_coder-2.4.9 → ata_coder-2.5.0}/web/css/style.css +0 -0
  139. {ata_coder-2.4.9 → ata_coder-2.5.0}/web/index.html +0 -0
  140. {ata_coder-2.4.9 → ata_coder-2.5.0}/web/js/app.js +0 -0
  141. {ata_coder-2.4.9 → ata_coder-2.5.0}/web/package-lock.json +0 -0
  142. {ata_coder-2.4.9 → ata_coder-2.5.0}/web/package.json +0 -0
  143. {ata_coder-2.4.9 → ata_coder-2.5.0}/web/tsconfig.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ata-coder
3
- Version: 2.4.9
3
+ Version: 2.5.0
4
4
  Summary: ATA Coder — AI-powered coding assistant
5
5
  Author: ATA Coder Team
6
6
  License-Expression: MIT
@@ -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
- self._emit(ErrorEvent(f"Fatal error: {e}"))
323
- return f"Error: {e}"
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
- # Always deactivate skill after task prevents state leak
329
- if self.skills:
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
- self._append_message({
646
- "role": "assistant",
647
- "content": text or None,
648
- "tool_calls": [tc],
649
- })
650
- self._store_tool_result(result, tc["id"])
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
- return self._prompt_builder.build(TOOL_DEFINITIONS, user_input=user_input)
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(f"Agent error: {e}")
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 Exception:
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) -> None:
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
- self._state.messages.append({
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
- if cache_key in self.tools._file_cache:
301
- cached_mtime, _, cached_content = self.tools._file_cache[cache_key]
302
- if cached_mtime == p.stat().st_mtime:
303
- return cached_content
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; proxies may ignore it
76
- if os.getenv("ANTHROPIC_VERSION"):
77
- self._headers["anthropic-version"] = os.getenv("ANTHROPIC_VERSION")
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
- return text + ''.join(reversed(stack))
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
- else:
435
- # Fallback: use estimated counts from the response text
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
- inp = max(1, self.count_tokens_approx(
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 or 1
472
+ self._total_completion_tokens += out
444
473
  if self._usage_callback:
445
- self._usage_callback(inp, out or 1)
474
+ self._usage_callback(inp, out)
446
475
 
447
476
  return result
448
477
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ata-coder
3
- Version: 2.4.9
3
+ Version: 2.5.0
4
4
  Summary: ATA Coder — AI-powered coding assistant
5
5
  Author: ATA Coder Team
6
6
  License-Expression: MIT
@@ -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-chat")
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
- ext.on_activate()
372
- except Exception:
373
- logger.exception("Extension %r on_activate failed", name)
374
- return False
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
- with self._lock:
377
- self._active.add(name)
378
- logger.debug("Extension activated: %s", name)
379
- return True
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.warning("Potential secrets in commit: %s", secret_check)
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 connect to {self._api_url}\n"
430
- f" Check: is the server running? Is the URL correct?\n"
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) * (0.5 + random.random())
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:
@@ -44,7 +44,7 @@ if sys.platform == 'win32':
44
44
  _patched_init.__ata_patched__ = True
45
45
  _sp.Popen.__init__ = _patched_init
46
46
 
47
- __version__ = "2.4.9"
47
+ __version__ = "2.5.0"
48
48
 
49
49
  import asyncio
50
50
  import logging
@@ -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(cls) -> str:
329
- cls._next_req_id += 1
330
- return str(cls._next_req_id)
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)