@scemoon/cdh 1.0.0 → 1.0.4

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 (147) hide show
  1. package/cdh/__pycache__/cli.cpython-314.pyc +0 -0
  2. package/cdh/__pycache__/scaffold.cpython-314.pyc +0 -0
  3. package/cdh/cli.py +473 -87
  4. package/cdh/scaffold.py +386 -121
  5. package/onecode/__pycache__/cli.cpython-314.pyc +0 -0
  6. package/onecode/__pycache__/commands.cpython-314.pyc +0 -0
  7. package/onecode/__pycache__/config.cpython-314.pyc +0 -0
  8. package/onecode/agent/__pycache__/cdh_loader.cpython-314.pyc +0 -0
  9. package/onecode/agent/__pycache__/engine.cpython-314.pyc +0 -0
  10. package/onecode/agent/__pycache__/onecode_agent_acp.cpython-314.pyc +0 -0
  11. package/onecode/agent/__pycache__/session.cpython-314.pyc +0 -0
  12. package/onecode/agent/agents/__pycache__/types.cpython-314.pyc +0 -0
  13. package/onecode/agent/agents/types.py +123 -79
  14. package/onecode/agent/cdh_loader.py +26 -13
  15. package/onecode/agent/engine.py +426 -235
  16. package/onecode/agent/onecode_agent_acp.py +202 -75
  17. package/onecode/agent/session.py +8 -13
  18. package/onecode/agent/tools/__pycache__/agent_tools.cpython-314.pyc +0 -0
  19. package/onecode/agent/tools/__pycache__/todo_tools.cpython-314.pyc +0 -0
  20. package/onecode/agent/tools/agent_tools.py +4 -4
  21. package/onecode/agent/tools/todo_tools.py +188 -0
  22. package/onecode/cli.py +3 -3
  23. package/onecode/commands.py +439 -194
  24. package/onecode/config.py +3 -3
  25. package/onecode/config_screen.py +4 -2
  26. package/onecode/models/__pycache__/errors.cpython-314.pyc +0 -0
  27. package/onecode/models/__pycache__/messages.cpython-314.pyc +0 -0
  28. package/onecode/models/__pycache__/provider.cpython-314.pyc +0 -0
  29. package/onecode/models/errors.py +11 -1
  30. package/onecode/models/messages.py +35 -16
  31. package/onecode/models/provider.py +243 -12
  32. package/onecode/models/providers/__pycache__/anthropic_provider.cpython-314.pyc +0 -0
  33. package/onecode/models/providers/__pycache__/deepseek_provider.cpython-314.pyc +0 -0
  34. package/onecode/models/providers/__pycache__/glm_provider.cpython-314.pyc +0 -0
  35. package/onecode/models/providers/__pycache__/minimax_provider.cpython-314.pyc +0 -0
  36. package/onecode/models/providers/__pycache__/minimaxi_provider.cpython-314.pyc +0 -0
  37. package/onecode/models/providers/__pycache__/ollama_provider.cpython-314.pyc +0 -0
  38. package/onecode/models/providers/__pycache__/openai_provider.cpython-314.pyc +0 -0
  39. package/onecode/models/providers/anthropic_provider.py +4 -2
  40. package/onecode/models/providers/deepseek_provider.py +2 -0
  41. package/onecode/models/providers/glm_provider.py +2 -1
  42. package/onecode/models/providers/minimax_provider.py +2 -1
  43. package/onecode/models/providers/minimaxi_provider.py +76 -25
  44. package/onecode/models/providers/ollama_provider.py +2 -1
  45. package/onecode/models/providers/openai_provider.py +4 -0
  46. package/onecode/tasks/manager.py +1 -1
  47. package/package.json +1 -1
  48. package/pyproject.toml +10 -0
  49. package/run.js +24 -7
  50. package/tui/__pycache__/agents.cpython-314.pyc +0 -0
  51. package/tui/__pycache__/app.cpython-314.pyc +0 -0
  52. package/tui/__pycache__/db.cpython-314.pyc +0 -0
  53. package/tui/__pycache__/directory_watcher.cpython-314.pyc +0 -0
  54. package/tui/__pycache__/messages.cpython-314.pyc +0 -0
  55. package/tui/__pycache__/paths.cpython-314.pyc +0 -0
  56. package/tui/__pycache__/settings_schema.cpython-314.pyc +0 -0
  57. package/tui/acp/__pycache__/agent.cpython-314.pyc +0 -0
  58. package/tui/acp/__pycache__/api.cpython-314.pyc +0 -0
  59. package/tui/acp/__pycache__/messages.cpython-314.pyc +0 -0
  60. package/tui/acp/__pycache__/prompt.cpython-314.pyc +0 -0
  61. package/tui/acp/__pycache__/protocol.cpython-314.pyc +0 -0
  62. package/tui/acp/agent.py +60 -11
  63. package/tui/acp/api.py +6 -0
  64. package/tui/acp/messages.py +11 -0
  65. package/tui/acp/prompt.py +1 -1
  66. package/tui/acp/protocol.py +31 -0
  67. package/tui/agents.py +8 -7
  68. package/tui/app.py +60 -8
  69. package/tui/data/agents/ampcode.com.toml +1 -0
  70. package/tui/data/agents/augmentcode.com.toml +1 -0
  71. package/tui/data/agents/cdh.cloud-dev-harness.toml +3 -2
  72. package/tui/data/agents/crowai.dev.toml +1 -0
  73. package/tui/data/agents/deepagents.langchain.com +1 -0
  74. package/tui/data/agents/docker.com.toml +1 -0
  75. package/tui/data/agents/goose.ai.toml +1 -0
  76. package/tui/data/agents/inference.huggingface.co.toml +1 -1
  77. package/tui/data/agents/kiro.dev.toml +1 -0
  78. package/tui/data/agents/onecode.dev.toml +22 -0
  79. package/tui/data/agents/openclaw.ai.toml +1 -1
  80. package/tui/data/agents/openhands.dev.toml +1 -0
  81. package/tui/data/agents/stakpak.dev.toml +1 -0
  82. package/tui/data/agents/vibe.mistral.ai.toml +1 -0
  83. package/tui/data/agents/vtcode.dev.toml +1 -0
  84. package/tui/db.py +23 -0
  85. package/tui/directory_watcher.py +35 -1
  86. package/tui/messages.py +4 -1
  87. package/tui/paths.py +3 -3
  88. package/tui/screens/__pycache__/main.cpython-314.pyc +0 -0
  89. package/tui/screens/component_picker.py +148 -0
  90. package/tui/screens/main.py +47 -95
  91. package/tui/screens/projects.tcss +10 -0
  92. package/tui/screens/projects_app.py +33 -5
  93. package/tui/screens/projects_screen.py +171 -9
  94. package/tui/screens/store.py +1 -1
  95. package/tui/settings_schema.py +1 -1
  96. package/tui/widgets/__pycache__/ask_user.cpython-314.pyc +0 -0
  97. package/tui/widgets/__pycache__/conversation.cpython-314.pyc +0 -0
  98. package/tui/widgets/__pycache__/modified_files.cpython-314.pyc +0 -0
  99. package/tui/widgets/__pycache__/plan.cpython-314.pyc +0 -0
  100. package/tui/widgets/__pycache__/project_directory_tree.cpython-314.pyc +0 -0
  101. package/tui/widgets/__pycache__/question.cpython-314.pyc +0 -0
  102. package/tui/widgets/__pycache__/subagent.cpython-314.pyc +0 -0
  103. package/tui/widgets/__pycache__/tool_call.cpython-314.pyc +0 -0
  104. package/tui/widgets/ask_user.py +1 -1
  105. package/tui/widgets/conversation.py +99 -42
  106. package/tui/widgets/modified_files.py +191 -65
  107. package/tui/widgets/plan.py +22 -0
  108. package/tui/widgets/project_directory_tree.py +22 -10
  109. package/tui/widgets/project_grid_select.py +23 -1
  110. package/tui/widgets/question.py +1 -1
  111. package/tui/widgets/subagent.py +202 -36
  112. package/tui/widgets/subagent_screen.py +186 -0
  113. package/tui/widgets/tool_call.py +5 -8
  114. package/cdh/__pycache__/__init__.cpython-313.pyc +0 -0
  115. package/cdh/__pycache__/scaffold.cpython-313.pyc +0 -0
  116. package/onecode/__pycache__/config_screen.cpython-314.pyc +0 -0
  117. package/onecode/__pycache__/mock_data.cpython-314.pyc +0 -0
  118. package/onecode/agent/__pycache__/cdh_agent_acp.cpython-313.pyc +0 -0
  119. package/onecode/agent/__pycache__/cdh_agent_acp.cpython-314.pyc +0 -0
  120. package/onecode/agent/__pycache__/harness_skill.cpython-314.pyc +0 -0
  121. package/onecode/agent/tools/__pycache__/task_tools.cpython-314.pyc +0 -0
  122. package/onecode/agent/tools/task_tools.py +0 -249
  123. package/onecode/codebase/__pycache__/config.cpython-314.pyc +0 -0
  124. package/onecode/models/providers/__pycache__/mock_provider.cpython-314.pyc +0 -0
  125. package/onecode/skills/__pycache__/manager.cpython-314.pyc +0 -0
  126. package/tui/__pycache__/danger.cpython-314.pyc +0 -0
  127. package/tui/__pycache__/directory_suggester.cpython-314.pyc +0 -0
  128. package/tui/screens/__pycache__/log.cpython-314.pyc +0 -0
  129. package/tui/screens/__pycache__/projects_screen.cpython-314.pyc +0 -0
  130. package/tui/screens/__pycache__/sessions.cpython-314.pyc +0 -0
  131. package/tui/screens/__pycache__/settings.cpython-314.pyc +0 -0
  132. package/tui/screens/__pycache__/store.cpython-314.pyc +0 -0
  133. package/tui/widgets/__pycache__/acp_content.cpython-314.pyc +0 -0
  134. package/tui/widgets/__pycache__/conversation.cpython-313.pyc +0 -0
  135. package/tui/widgets/__pycache__/diff_view.cpython-314.pyc +0 -0
  136. package/tui/widgets/__pycache__/directory_input.cpython-314.pyc +0 -0
  137. package/tui/widgets/__pycache__/future_text.cpython-314.pyc +0 -0
  138. package/tui/widgets/__pycache__/grid_select.cpython-314.pyc +0 -0
  139. package/tui/widgets/__pycache__/mandelbrot.cpython-314.pyc +0 -0
  140. package/tui/widgets/__pycache__/markdown_note.cpython-314.pyc +0 -0
  141. package/tui/widgets/__pycache__/project_grid_select.cpython-314.pyc +0 -0
  142. package/tui/widgets/__pycache__/project_summary.cpython-314.pyc +0 -0
  143. package/tui/widgets/__pycache__/session_grid_select.cpython-314.pyc +0 -0
  144. package/tui/widgets/__pycache__/session_summary.cpython-314.pyc +0 -0
  145. package/tui/widgets/__pycache__/shell_result.cpython-314.pyc +0 -0
  146. package/tui/widgets/__pycache__/terminal.cpython-313.pyc +0 -0
  147. package/tui/widgets/__pycache__/tool_call.cpython-313.pyc +0 -0
package/onecode/config.py CHANGED
@@ -6,7 +6,7 @@ from typing import Any, Optional
6
6
  import yaml
7
7
 
8
8
 
9
- CLOUD_DEV_HARNESS_DIR = Path.home() / ".onecode"
9
+ CLOUD_DEV_HARNESS_DIR = Path.home() / ".cdh"
10
10
  GLOBAL_CONFIG_PATH = CLOUD_DEV_HARNESS_DIR / "onecode.config.yaml"
11
11
 
12
12
 
@@ -22,7 +22,7 @@ class ObservabilityConfig:
22
22
  trace_enabled: bool = True
23
23
  trace_exporter: str = "file"
24
24
  otlp_endpoint: str = "http://localhost:4317"
25
- trace_dir: str = "~/.onecode/traces"
25
+ trace_dir: str = "~/.cdh/traces"
26
26
 
27
27
 
28
28
  @dataclass
@@ -201,7 +201,7 @@ def _write_default_config():
201
201
  "trace_enabled": True,
202
202
  "trace_exporter": "file",
203
203
  "otlp_endpoint": "http://localhost:4317",
204
- "trace_dir": "~/.onecode/traces",
204
+ "trace_dir": "~/.cdh/traces",
205
205
  },
206
206
  "attachments": {
207
207
  "max_size_mb": 10,
@@ -160,7 +160,8 @@ class EditFieldScreen(ModalScreen[str]):
160
160
 
161
161
  @on(Button.Pressed, "#edit-save")
162
162
  def on_save(self) -> None:
163
- self.dismiss(self.query_one("#edit-input", Input).value)
163
+ value = self.query_one("#edit-input", Input).value.strip()
164
+ self.dismiss(value or None)
164
165
 
165
166
  @on(Button.Pressed, "#edit-cancel")
166
167
  def on_cancel(self) -> None:
@@ -168,7 +169,8 @@ class EditFieldScreen(ModalScreen[str]):
168
169
 
169
170
  @on(Input.Submitted)
170
171
  def on_input_submitted(self) -> None:
171
- self.dismiss(self.query_one("#edit-input", Input).value)
172
+ value = self.query_one("#edit-input", Input).value.strip()
173
+ self.dismiss(value or None)
172
174
 
173
175
  def action_dismiss_modal(self) -> None:
174
176
  self.dismiss(None)
@@ -23,6 +23,16 @@ if TYPE_CHECKING:
23
23
  import httpx
24
24
 
25
25
 
26
+ def safe_error_msg(e: BaseException) -> str:
27
+ """Return a non-empty error message from an exception.
28
+
29
+ Python's ``str(Exception())`` returns ``""``, which leads to
30
+ confusing ``"Error: "`` messages in the UI. This helper falls
31
+ back to the exception type name when the message is empty.
32
+ """
33
+ return str(e) or type(e).__name__
34
+
35
+
26
36
  def retry_after_seconds(resp: "httpx.Response") -> Optional[float]:
27
37
  """Parse the ``Retry-After`` header from an HTTP response.
28
38
 
@@ -70,7 +80,7 @@ class ProviderError(Exception):
70
80
 
71
81
  def to_user_message(self) -> str:
72
82
  """Build a short, user-friendly message for the TUI."""
73
- head = f"Error: {self.args[0]}" if self.args else "Error: provider failure"
83
+ head = f"Error: {safe_error_msg(self)}" if self.args else "Error: provider failure"
74
84
  if self.status_code is not None:
75
85
  head += f" (HTTP {self.status_code})"
76
86
  if self.retry_after is not None:
@@ -78,7 +78,7 @@ class ToolCategory(str, Enum):
78
78
  WEB_FETCH = "web_fetch"
79
79
  WEB_SEARCH = "web_search"
80
80
  TASK = "task" # sub-agent
81
- TASK_MGMT = "task_mgmt" # task/todo CRUD
81
+ TODO_MGMT = "todo_mgmt" # todo CRUD
82
82
  INTERACTION = "interaction" # user approval / AskUser
83
83
  UNKNOWN = "unknown"
84
84
 
@@ -95,19 +95,16 @@ TOOL_CATEGORY_MAP: dict[str, ToolCategory] = {
95
95
  "Bash": ToolCategory.BASH,
96
96
  "WebFetch": ToolCategory.WEB_FETCH,
97
97
  "WebSearch": ToolCategory.WEB_SEARCH,
98
- "Task": ToolCategory.TASK,
98
+ "Spawn": ToolCategory.TASK,
99
99
  "SendMessage": ToolCategory.INTERACTION,
100
100
  "Agent": ToolCategory.TASK,
101
- "ToolSearch": ToolCategory.TASK_MGMT,
102
- "TaskCreate": ToolCategory.TASK_MGMT,
103
- "TaskGet": ToolCategory.TASK_MGMT,
104
- "TaskList": ToolCategory.TASK_MGMT,
105
- "TaskUpdate": ToolCategory.TASK_MGMT,
106
- "TaskOutput": ToolCategory.TASK_MGMT,
107
- "TaskStop": ToolCategory.TASK_MGMT,
108
- "TodoCreate": ToolCategory.TASK_MGMT,
109
- "TodoList": ToolCategory.TASK_MGMT,
110
- "TodoComplete": ToolCategory.TASK_MGMT,
101
+ "ToolSearch": ToolCategory.TODO_MGMT,
102
+ "TodoCreate": ToolCategory.TODO_MGMT,
103
+ "TodoGet": ToolCategory.TODO_MGMT,
104
+ "TodoList": ToolCategory.TODO_MGMT,
105
+ "TodoUpdate": ToolCategory.TODO_MGMT,
106
+ "TodoOutput": ToolCategory.TODO_MGMT,
107
+ "TodoStop": ToolCategory.TODO_MGMT,
111
108
  "AskUser": ToolCategory.INTERACTION,
112
109
  }
113
110
 
@@ -281,6 +278,7 @@ class StreamEventType(str, Enum):
281
278
  PLAN = "plan"
282
279
  SUBAGENT_START = "subagent_start"
283
280
  SUBAGENT_CHUNK = "subagent_chunk"
281
+ SUBAGENT_THINKING = "subagent_thinking"
284
282
  SUBAGENT_END = "subagent_end"
285
283
 
286
284
 
@@ -325,6 +323,10 @@ class StreamEvent:
325
323
  subagent_id: str = ""
326
324
  subagent_type: str = ""
327
325
  subagent_text: str = ""
326
+ subagent_prompt: str = ""
327
+ subagent_thinking_text: str = ""
328
+ subagent_status: str = ""
329
+ subagent_error: str = ""
328
330
 
329
331
  @classmethod
330
332
  def text_delta(cls, text: str) -> "StreamEvent":
@@ -403,18 +405,19 @@ class StreamEvent:
403
405
 
404
406
  Args:
405
407
  entries: List of plan entries, each with:
406
- - content: str - The task description
408
+ - content: str - The todo description
407
409
  - status: str - "pending" | "in_progress" | "completed"
408
410
  - priority: str - "high" | "medium" | "low"
409
411
  """
410
412
  return cls(type=StreamEventType.PLAN, plan_entries=entries)
411
413
 
412
414
  @classmethod
413
- def subagent_start(cls, agent_type: str, call_id: str) -> "StreamEvent":
415
+ def subagent_start(cls, agent_type: str, call_id: str, prompt: str = "") -> "StreamEvent":
414
416
  return cls(
415
417
  type=StreamEventType.SUBAGENT_START,
416
418
  subagent_id=call_id,
417
419
  subagent_type=agent_type,
420
+ subagent_prompt=prompt,
418
421
  )
419
422
 
420
423
  @classmethod
@@ -426,8 +429,24 @@ class StreamEvent:
426
429
  )
427
430
 
428
431
  @classmethod
429
- def subagent_end(cls, call_id: str) -> "StreamEvent":
430
- return cls(type=StreamEventType.SUBAGENT_END, subagent_id=call_id)
432
+ def subagent_thinking(cls, call_id: str, text: str) -> "StreamEvent":
433
+ return cls(
434
+ type=StreamEventType.SUBAGENT_THINKING,
435
+ subagent_id=call_id,
436
+ subagent_thinking_text=text,
437
+ )
438
+
439
+ @classmethod
440
+ def subagent_end(
441
+ cls, call_id: str, agent_type: str = "", status: str = "completed", error: str = ""
442
+ ) -> "StreamEvent":
443
+ return cls(
444
+ type=StreamEventType.SUBAGENT_END,
445
+ subagent_id=call_id,
446
+ subagent_type=agent_type,
447
+ subagent_status=status,
448
+ subagent_error=error,
449
+ )
431
450
 
432
451
  @staticmethod
433
452
  def is_text(event: "StreamEvent") -> bool:
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import logging
5
6
  from abc import ABC, abstractmethod
6
7
  from dataclasses import dataclass, field
7
8
  from enum import Enum
@@ -296,6 +297,8 @@ class ModelResponse:
296
297
  class Provider(ABC):
297
298
  name: str = ""
298
299
 
300
+ _prep_logger = logging.getLogger("onecode.provider.prepare")
301
+
299
302
  @abstractmethod
300
303
  async def chat(
301
304
  self, messages: list[Message], model: str, **kwargs
@@ -317,22 +320,245 @@ class Provider(ABC):
317
320
  return key or ""
318
321
 
319
322
  def prepare_messages(self, messages: list["Message"]) -> list[dict]:
320
- """Prepare messages for API call. Handles tool, tool_use, tool_result blocks properly."""
321
- system_parts = []
322
- non_system = []
323
+ """Prepare messages for API call. Handles tool, tool_use, tool_result blocks properly.
324
+
325
+ Hardening applied before serialization:
326
+ 1. Drop ``tool`` messages with empty content and no ``tool_call_id``
327
+ (the OpenAI-compatible API rejects these with HTTP 400).
328
+ 2. Coalesce runs of consecutive identical ``user`` content into one
329
+ message (some MiniMax gateways reject back-to-back duplicates).
330
+ 3. Cap the combined system-prompt length at 32 KiB, keeping the
331
+ highest-priority blocks (``AGENT_CONFIG`` → ``REACT_PHASE`` →
332
+ newest skills) and dropping older skill bodies first.
333
+ 4. Drop ``tool`` messages whose ``tool_call_id`` is not present in
334
+ the immediately preceding assistant message's ``tool_calls`` —
335
+ the MiniMax gateway returns ``(2013) tool call result does not
336
+ follow tool call`` otherwise.
337
+ 5. When a tool message is dropped (empty or orphan), also strip
338
+ the matching ``tool_call`` entry from the *owning* assistant
339
+ message — i.e. the assistant that originally emitted that
340
+ ``tool_call.id``. Without this, the request still contains
341
+ an ``assistant.tool_calls`` reference with no following
342
+ ``tool`` result, which MiniMax rejects with the same (2013)
343
+ error (``tool call and result not match`` in the stricter
344
+ validator). Tracking is per-id, not per-last-assistant, so
345
+ drops that span multiple turns (e.g. an empty result from
346
+ turn 1 and a legitimate one from turn 2) are each routed
347
+ back to the right assistant.
348
+ """
349
+ _SYSTEM_CAP_BYTES = 32 * 1024
350
+ log = Provider._prep_logger
351
+ dropped_empty_tools = 0
352
+ dropped_orphan_tools = 0
353
+ coalesced_user_msgs = 0
354
+
355
+ system_parts: list[tuple[str, str]] = [] # (marker, body)
356
+ non_system: list[dict] = []
323
357
 
324
358
  for m in messages:
325
359
  if m.role == "system":
326
- system_parts.append(m.to_api_content())
360
+ body = m.to_api_content()
361
+ marker = ""
362
+ if "<!-- AGENT_CONFIG -->" in body:
363
+ marker = "AGENT_CONFIG"
364
+ elif "<!-- REACT_PHASE -->" in body:
365
+ marker = "REACT_PHASE"
366
+ elif "<!-- SKILL:" in body:
367
+ marker = "SKILL"
368
+ elif "<!-- CDH_PROJECT -->" in body:
369
+ marker = "CDH_PROJECT"
370
+ if body:
371
+ system_parts.append((marker, body))
372
+ elif m.role == "tool":
373
+ api = m.to_api_dict()
374
+ has_id = bool(api.get("tool_call_id"))
375
+ has_content = bool((api.get("content") or "").strip())
376
+ if not has_id or not has_content:
377
+ dropped_empty_tools += 1
378
+ log.debug(
379
+ "prepare_messages: dropped empty tool message (id=%r content_len=%d)",
380
+ api.get("tool_call_id"),
381
+ len(api.get("content") or ""),
382
+ )
383
+ continue
384
+ non_system.append(api)
327
385
  else:
328
386
  non_system.append(m.to_api_dict())
329
387
 
330
- if len(system_parts) > 1:
331
- combined_system = "\n\n".join(system_parts)
332
- elif system_parts:
333
- combined_system = system_parts[0]
334
- else:
335
- combined_system = ""
388
+ coalesced: list[dict] = []
389
+ for msg in non_system:
390
+ if (
391
+ msg.get("role") == "user"
392
+ and coalesced
393
+ and coalesced[-1].get("role") == "user"
394
+ and (msg.get("content") or "") == (coalesced[-1].get("content") or "")
395
+ and not msg.get("tool_calls")
396
+ and not coalesced[-1].get("tool_calls")
397
+ ):
398
+ coalesced_user_msgs += 1
399
+ continue
400
+ coalesced.append(msg)
401
+ non_system = coalesced
402
+
403
+ # First pass: walk messages, drop orphan tool messages, and build
404
+ # a per-id map of ``tc_id -> owning assistant index`` so we know
405
+ # exactly which assistant emitted each ``tool_call`` entry.
406
+ linked: list[dict] = []
407
+ last_assistant_tc_ids: set[str] = set()
408
+ # Maps tool_call.id -> index in ``linked`` of the assistant that
409
+ # currently owns it. Rebuilt whenever a new assistant message
410
+ # is appended (matches the same reset semantics as
411
+ # ``last_assistant_tc_ids``).
412
+ tc_owner: dict[str, int] = {}
413
+ for msg in non_system:
414
+ if msg.get("role") == "tool":
415
+ tcid = msg.get("tool_call_id") or ""
416
+ if tcid not in last_assistant_tc_ids:
417
+ dropped_orphan_tools += 1
418
+ log.debug(
419
+ "prepare_messages: dropped orphan tool message "
420
+ "(tool_call_id=%r not in preceding assistant.tool_calls)",
421
+ tcid,
422
+ )
423
+ continue
424
+ linked.append(msg)
425
+ continue
426
+ new_index = len(linked)
427
+ linked.append(msg)
428
+ if msg.get("role") == "assistant":
429
+ last_assistant_tc_ids = {
430
+ tc.get("id") for tc in (msg.get("tool_calls") or []) if tc.get("id")
431
+ }
432
+ for tc in msg.get("tool_calls") or []:
433
+ tcid = tc.get("id")
434
+ if tcid:
435
+ tc_owner[tcid] = new_index
436
+ else:
437
+ last_assistant_tc_ids = set()
438
+ non_system = linked
439
+
440
+ # Second pass: any tool_call whose matching tool message was
441
+ # dropped (empty content caught above, or otherwise missing)
442
+ # must be removed from the *owning* assistant message — not just
443
+ # the last assistant. We do this in two steps:
444
+ # 1. Walk the full list and collect every tool_call_id that
445
+ # appears in any tool message — these are the "seen" ids.
446
+ # 2. Walk again and, for each assistant message, find any
447
+ # tool_call.id that is NOT in the seen set. Those are
448
+ # dangling entries that must be stripped.
449
+ seen_tool_ids: set[str] = set()
450
+ for msg in non_system:
451
+ if msg.get("role") == "tool":
452
+ tcid = msg.get("tool_call_id") or ""
453
+ if tcid:
454
+ seen_tool_ids.add(tcid)
455
+
456
+ dangling: dict[int, set[str]] = {}
457
+ for msg in non_system:
458
+ if msg.get("role") != "assistant":
459
+ continue
460
+ tcs = msg.get("tool_calls") or []
461
+ if not tcs:
462
+ continue
463
+ for tc in tcs:
464
+ tcid = tc.get("id") or ""
465
+ if tcid and tcid not in seen_tool_ids:
466
+ owner_idx = tc_owner.get(tcid)
467
+ if owner_idx is not None:
468
+ dangling.setdefault(owner_idx, set()).add(tcid)
469
+ # Also strip tool_call entries with an empty id — those can
470
+ # never be matched to a tool result and would be silently
471
+ # dangling. This mirrors the upstream filter in the first-pass
472
+ # tool tracking (``if tc.get("id")`` / ``if tcid``).
473
+ for msg in non_system:
474
+ if msg.get("role") != "assistant":
475
+ continue
476
+ tcs = msg.get("tool_calls") or []
477
+ if not tcs:
478
+ continue
479
+ kept = [tc for tc in tcs if tc.get("id")]
480
+ if len(kept) < len(tcs):
481
+ log.debug(
482
+ "prepare_messages: stripped %d empty-id tool_call entr%s "
483
+ "from assistant message",
484
+ len(tcs) - len(kept),
485
+ "y" if len(tcs) - len(kept) == 1 else "ies",
486
+ )
487
+ if kept:
488
+ msg["tool_calls"] = kept
489
+ else:
490
+ msg.pop("tool_calls", None)
491
+
492
+ # Apply the cleanup: strip dangling tool_call entries. Empty
493
+ # ``tool_calls`` lists are removed entirely.
494
+ if dangling:
495
+ total_stripped = 0
496
+ for idx, ids in dangling.items():
497
+ assistant_msg = non_system[idx]
498
+ tcs = assistant_msg.get("tool_calls") or []
499
+ kept = [tc for tc in tcs if (tc.get("id") or "") not in ids]
500
+ stripped = len(tcs) - len(kept)
501
+ if stripped:
502
+ total_stripped += stripped
503
+ log.debug(
504
+ "prepare_messages: stripped %d dangling tool_call entr%s "
505
+ "from assistant message idx=%d (ids=%r)",
506
+ stripped,
507
+ "y" if stripped == 1 else "ies",
508
+ idx,
509
+ sorted(ids),
510
+ )
511
+ if kept:
512
+ assistant_msg["tool_calls"] = kept
513
+ else:
514
+ assistant_msg.pop("tool_calls", None)
515
+ if total_stripped:
516
+ log.info(
517
+ "prepare_messages(%s): stripped %d dangling tool_call entr%s across %d assistant message(s)",
518
+ self.name or "?",
519
+ total_stripped,
520
+ "y" if total_stripped == 1 else "ies",
521
+ len(dangling),
522
+ )
523
+
524
+ if dropped_empty_tools or dropped_orphan_tools or coalesced_user_msgs:
525
+ log.info(
526
+ "prepare_messages(%s): dropped empty_tools=%d orphan_tools=%d "
527
+ "coalesced_user=%d (input=%d output=%d)",
528
+ self.name or "?",
529
+ dropped_empty_tools,
530
+ dropped_orphan_tools,
531
+ coalesced_user_msgs,
532
+ len(messages),
533
+ len(non_system) + (1 if system_parts else 0),
534
+ )
535
+
536
+ combined_system = ""
537
+ if system_parts:
538
+ combined_parts = [body for _, body in system_parts]
539
+ combined_system = "\n\n".join(combined_parts)
540
+ if len(combined_system.encode("utf-8")) > _SYSTEM_CAP_BYTES:
541
+ priority = {"AGENT_CONFIG": 0, "REACT_PHASE": 1, "CDH_PROJECT": 2, "SKILL": 3}
542
+ ordered = sorted(
543
+ range(len(system_parts)),
544
+ key=lambda i: (priority.get(system_parts[i][0], 9), -i),
545
+ )
546
+ kept_indices: set[int] = set()
547
+ running = 0
548
+ for idx in ordered:
549
+ marker, body = system_parts[idx]
550
+ size = len(body.encode("utf-8")) + 2
551
+ if marker == "AGENT_CONFIG":
552
+ kept_indices.add(idx)
553
+ running = max(running, size)
554
+ continue
555
+ if running + size <= _SYSTEM_CAP_BYTES:
556
+ kept_indices.add(idx)
557
+ running += size
558
+ kept = [body for i, body in enumerate(combined_parts) if i in kept_indices]
559
+ if not any("<!-- AGENT_CONFIG -->" in b for b in kept) and system_parts:
560
+ kept.insert(0, system_parts[0][1])
561
+ combined_system = "\n\n".join(kept).encode("utf-8")[:_SYSTEM_CAP_BYTES].decode("utf-8", errors="ignore")
336
562
 
337
563
  if combined_system:
338
564
  non_system.insert(0, {"role": "system", "content": combined_system})
@@ -351,6 +577,8 @@ class Provider(ABC):
351
577
  args = __import__("json").loads(func.get("arguments", "{}"))
352
578
  except Exception:
353
579
  args = {"raw": func.get("arguments", "")}
580
+ if not isinstance(args, dict):
581
+ args = {"raw": args}
354
582
  result.append({
355
583
  "id": tc.get("id", ""),
356
584
  "name": func.get("name", ""),
@@ -412,6 +640,7 @@ class Provider(ABC):
412
640
  TransientProviderError,
413
641
  )
414
642
  snippet = (body or "").strip()
643
+ body_excerpt = snippet[:200] if snippet else ""
415
644
  if status_code == 429:
416
645
  return RateLimitError(
417
646
  "rate limit exceeded", status_code=status_code,
@@ -426,11 +655,13 @@ class Provider(ABC):
426
655
  "context length exceeded", status_code=status_code, body=snippet,
427
656
  )
428
657
  if 500 <= status_code < 600:
658
+ msg = f"upstream {status_code}: {body_excerpt}" if body_excerpt else f"upstream {status_code}"
429
659
  return TransientProviderError(
430
- f"upstream {status_code}", status_code=status_code, body=snippet,
660
+ msg, status_code=status_code, body=snippet,
431
661
  )
662
+ msg = f"upstream error {status_code}: {body_excerpt}" if body_excerpt else f"upstream error {status_code}"
432
663
  return ProviderError(
433
- f"upstream error {status_code}", status_code=status_code, body=snippet,
664
+ msg, status_code=status_code, body=snippet,
434
665
  )
435
666
 
436
667
 
@@ -118,7 +118,7 @@ class AnthropicProvider(Provider):
118
118
  **kwargs,
119
119
  ) -> ChatResponse:
120
120
  from onecode.models.errors import (
121
- ProviderError, TransientProviderError, retry_after_seconds,
121
+ ProviderError, TransientProviderError, retry_after_seconds, safe_error_msg,
122
122
  )
123
123
  key = self.resolve_api_key(self.api_key)
124
124
  if not key:
@@ -198,6 +198,8 @@ class AnthropicProvider(Provider):
198
198
  current_tool["input"] = json.loads(current_tool["input_json"])
199
199
  except (json.JSONDecodeError, TypeError):
200
200
  current_tool["input"] = {"raw": current_tool["input_json"]}
201
+ if not isinstance(current_tool["input"], dict):
202
+ current_tool["input"] = {"raw": current_tool["input"]}
201
203
  tool_uses.append({
202
204
  "id": current_tool["id"],
203
205
  "name": current_tool["name"],
@@ -212,7 +214,7 @@ class AnthropicProvider(Provider):
212
214
  except httpx.ConnectError as e:
213
215
  raise TransientProviderError(f"Connection error: {e}") from e
214
216
  except Exception as e:
215
- raise TransientProviderError(f"Error: {e}") from e
217
+ raise TransientProviderError(f"Error: {safe_error_msg(e)}") from e
216
218
 
217
219
  return ChatResponse(
218
220
  content="".join(content_parts),
@@ -65,6 +65,8 @@ class DeepSeekProvider(Provider):
65
65
  args = __import__("json").loads(func.get("arguments", "{}"))
66
66
  except Exception:
67
67
  args = {"raw": func.get("arguments", "")}
68
+ if not isinstance(args, dict):
69
+ args = {"raw": args}
68
70
  content_blocks.append({
69
71
  "type": "tool_use",
70
72
  "id": tc.get("id", ""),
@@ -5,6 +5,7 @@ from typing import AsyncIterator, Callable, Optional
5
5
 
6
6
  import httpx
7
7
 
8
+ from onecode.models.errors import safe_error_msg
8
9
  from onecode.models.provider import ChatResponse, Message, ModelResponse, Provider, ProviderRegistry
9
10
 
10
11
 
@@ -132,7 +133,7 @@ class GLMProvider(Provider):
132
133
  if on_tool_call_delta:
133
134
  on_tool_call_delta(slot.get("id", ""), slot.get("name", ""), args_delta)
134
135
  except Exception as e:
135
- return ChatResponse(content=f"Error: {e}")
136
+ return ChatResponse(content=f"Error: {safe_error_msg(e)}")
136
137
  tool_uses = []
137
138
  for slot in stream_tool_calls.values():
138
139
  name = slot.get("name") or ""
@@ -5,6 +5,7 @@ from typing import AsyncIterator, Callable, Optional
5
5
 
6
6
  import httpx
7
7
 
8
+ from onecode.models.errors import safe_error_msg
8
9
  from onecode.models.provider import ChatResponse, Message, ModelResponse, Provider, ProviderRegistry
9
10
 
10
11
 
@@ -132,7 +133,7 @@ class MiniMaxProvider(Provider):
132
133
  if on_tool_call_delta:
133
134
  on_tool_call_delta(slot.get("id", ""), slot.get("name", ""), args_delta)
134
135
  except Exception as e:
135
- return ChatResponse(content=f"Error: {e}")
136
+ return ChatResponse(content=f"Error: {safe_error_msg(e)}")
136
137
  tool_uses = []
137
138
  for slot in stream_tool_calls.values():
138
139
  name = slot.get("name") or ""