@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.
- package/cdh/__pycache__/cli.cpython-314.pyc +0 -0
- package/cdh/__pycache__/scaffold.cpython-314.pyc +0 -0
- package/cdh/cli.py +473 -87
- package/cdh/scaffold.py +386 -121
- package/onecode/__pycache__/cli.cpython-314.pyc +0 -0
- package/onecode/__pycache__/commands.cpython-314.pyc +0 -0
- package/onecode/__pycache__/config.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/cdh_loader.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/engine.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/onecode_agent_acp.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/session.cpython-314.pyc +0 -0
- package/onecode/agent/agents/__pycache__/types.cpython-314.pyc +0 -0
- package/onecode/agent/agents/types.py +123 -79
- package/onecode/agent/cdh_loader.py +26 -13
- package/onecode/agent/engine.py +426 -235
- package/onecode/agent/onecode_agent_acp.py +202 -75
- package/onecode/agent/session.py +8 -13
- package/onecode/agent/tools/__pycache__/agent_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/todo_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/agent_tools.py +4 -4
- package/onecode/agent/tools/todo_tools.py +188 -0
- package/onecode/cli.py +3 -3
- package/onecode/commands.py +439 -194
- package/onecode/config.py +3 -3
- package/onecode/config_screen.py +4 -2
- package/onecode/models/__pycache__/errors.cpython-314.pyc +0 -0
- package/onecode/models/__pycache__/messages.cpython-314.pyc +0 -0
- package/onecode/models/__pycache__/provider.cpython-314.pyc +0 -0
- package/onecode/models/errors.py +11 -1
- package/onecode/models/messages.py +35 -16
- package/onecode/models/provider.py +243 -12
- package/onecode/models/providers/__pycache__/anthropic_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/deepseek_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/glm_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/minimax_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/minimaxi_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/ollama_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/openai_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/anthropic_provider.py +4 -2
- package/onecode/models/providers/deepseek_provider.py +2 -0
- package/onecode/models/providers/glm_provider.py +2 -1
- package/onecode/models/providers/minimax_provider.py +2 -1
- package/onecode/models/providers/minimaxi_provider.py +76 -25
- package/onecode/models/providers/ollama_provider.py +2 -1
- package/onecode/models/providers/openai_provider.py +4 -0
- package/onecode/tasks/manager.py +1 -1
- package/package.json +1 -1
- package/pyproject.toml +10 -0
- package/run.js +24 -7
- package/tui/__pycache__/agents.cpython-314.pyc +0 -0
- package/tui/__pycache__/app.cpython-314.pyc +0 -0
- package/tui/__pycache__/db.cpython-314.pyc +0 -0
- package/tui/__pycache__/directory_watcher.cpython-314.pyc +0 -0
- package/tui/__pycache__/messages.cpython-314.pyc +0 -0
- package/tui/__pycache__/paths.cpython-314.pyc +0 -0
- package/tui/__pycache__/settings_schema.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/agent.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/api.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/messages.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/prompt.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/protocol.cpython-314.pyc +0 -0
- package/tui/acp/agent.py +60 -11
- package/tui/acp/api.py +6 -0
- package/tui/acp/messages.py +11 -0
- package/tui/acp/prompt.py +1 -1
- package/tui/acp/protocol.py +31 -0
- package/tui/agents.py +8 -7
- package/tui/app.py +60 -8
- package/tui/data/agents/ampcode.com.toml +1 -0
- package/tui/data/agents/augmentcode.com.toml +1 -0
- package/tui/data/agents/cdh.cloud-dev-harness.toml +3 -2
- package/tui/data/agents/crowai.dev.toml +1 -0
- package/tui/data/agents/deepagents.langchain.com +1 -0
- package/tui/data/agents/docker.com.toml +1 -0
- package/tui/data/agents/goose.ai.toml +1 -0
- package/tui/data/agents/inference.huggingface.co.toml +1 -1
- package/tui/data/agents/kiro.dev.toml +1 -0
- package/tui/data/agents/onecode.dev.toml +22 -0
- package/tui/data/agents/openclaw.ai.toml +1 -1
- package/tui/data/agents/openhands.dev.toml +1 -0
- package/tui/data/agents/stakpak.dev.toml +1 -0
- package/tui/data/agents/vibe.mistral.ai.toml +1 -0
- package/tui/data/agents/vtcode.dev.toml +1 -0
- package/tui/db.py +23 -0
- package/tui/directory_watcher.py +35 -1
- package/tui/messages.py +4 -1
- package/tui/paths.py +3 -3
- package/tui/screens/__pycache__/main.cpython-314.pyc +0 -0
- package/tui/screens/component_picker.py +148 -0
- package/tui/screens/main.py +47 -95
- package/tui/screens/projects.tcss +10 -0
- package/tui/screens/projects_app.py +33 -5
- package/tui/screens/projects_screen.py +171 -9
- package/tui/screens/store.py +1 -1
- package/tui/settings_schema.py +1 -1
- package/tui/widgets/__pycache__/ask_user.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/conversation.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/modified_files.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/plan.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/project_directory_tree.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/question.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/subagent.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/tool_call.cpython-314.pyc +0 -0
- package/tui/widgets/ask_user.py +1 -1
- package/tui/widgets/conversation.py +99 -42
- package/tui/widgets/modified_files.py +191 -65
- package/tui/widgets/plan.py +22 -0
- package/tui/widgets/project_directory_tree.py +22 -10
- package/tui/widgets/project_grid_select.py +23 -1
- package/tui/widgets/question.py +1 -1
- package/tui/widgets/subagent.py +202 -36
- package/tui/widgets/subagent_screen.py +186 -0
- package/tui/widgets/tool_call.py +5 -8
- package/cdh/__pycache__/__init__.cpython-313.pyc +0 -0
- package/cdh/__pycache__/scaffold.cpython-313.pyc +0 -0
- package/onecode/__pycache__/config_screen.cpython-314.pyc +0 -0
- package/onecode/__pycache__/mock_data.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/cdh_agent_acp.cpython-313.pyc +0 -0
- package/onecode/agent/__pycache__/cdh_agent_acp.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/harness_skill.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/task_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/task_tools.py +0 -249
- package/onecode/codebase/__pycache__/config.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/mock_provider.cpython-314.pyc +0 -0
- package/onecode/skills/__pycache__/manager.cpython-314.pyc +0 -0
- package/tui/__pycache__/danger.cpython-314.pyc +0 -0
- package/tui/__pycache__/directory_suggester.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/log.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/projects_screen.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/sessions.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/settings.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/store.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/acp_content.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/conversation.cpython-313.pyc +0 -0
- package/tui/widgets/__pycache__/diff_view.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/directory_input.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/future_text.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/grid_select.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/mandelbrot.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/markdown_note.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/project_grid_select.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/project_summary.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/session_grid_select.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/session_summary.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/shell_result.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/terminal.cpython-313.pyc +0 -0
- 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() / ".
|
|
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 = "~/.
|
|
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": "~/.
|
|
204
|
+
"trace_dir": "~/.cdh/traces",
|
|
205
205
|
},
|
|
206
206
|
"attachments": {
|
|
207
207
|
"max_size_mb": 10,
|
package/onecode/config_screen.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/onecode/models/errors.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
"
|
|
98
|
+
"Spawn": ToolCategory.TASK,
|
|
99
99
|
"SendMessage": ToolCategory.INTERACTION,
|
|
100
100
|
"Agent": ToolCategory.TASK,
|
|
101
|
-
"ToolSearch": ToolCategory.
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
"
|
|
105
|
-
"
|
|
106
|
-
"
|
|
107
|
-
"
|
|
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
|
|
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
|
|
430
|
-
return cls(
|
|
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
|
-
|
|
322
|
-
|
|
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
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
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
|
-
|
|
664
|
+
msg, status_code=status_code, body=snippet,
|
|
434
665
|
)
|
|
435
666
|
|
|
436
667
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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 ""
|