@scemoon/cdh 1.0.4 → 1.0.6
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/ai-dlc-skill/SKILL.md +4 -2
- package/ai-dlc-skill/agents/master.md +6 -0
- package/ai-dlc-skill/agents/plan-agent.md +2 -2
- package/ai-dlc-skill/agents/understand-agent.md +4 -4
- package/ai-dlc-skill/agents/verify-agent.md +1 -1
- package/ai-dlc-skill/architecture/project-structure.md +10 -10
- package/ai-dlc-skill/brownfield/README.md +2 -2
- package/ai-dlc-skill/brownfield/generate-context.sh +3 -3
- package/ai-dlc-skill/brownfield/scripts/extract-api.sh +5 -5
- package/ai-dlc-skill/components/backend.md +1 -1
- package/ai-dlc-skill/components/web.md +2 -2
- package/ai-dlc-skill/contracts/README.md +3 -3
- package/ai-dlc-skill/cross-tool/cline/export.sh +1 -1
- package/ai-dlc-skill/cross-tool/copilot/export.sh +1 -1
- package/ai-dlc-skill/cross-tool/onecode/export-skills.sh +7 -3
- package/ai-dlc-skill/cross-tool/onecode/export.sh +14 -8
- package/ai-dlc-skill/phases/deliver/lifecycle.md +6 -6
- package/ai-dlc-skill/phases/deliver/prompt.md +2 -2
- package/ai-dlc-skill/phases/deliver/rules.md +1 -1
- package/ai-dlc-skill/phases/plan/lifecycle.md +11 -11
- package/ai-dlc-skill/phases/plan/prompt.md +6 -6
- package/ai-dlc-skill/phases/plan/rules.md +1 -1
- package/ai-dlc-skill/phases/understand/entry.md +1 -1
- package/ai-dlc-skill/phases/understand/lifecycle.md +7 -7
- package/ai-dlc-skill/phases/understand/prompt.md +8 -8
- package/ai-dlc-skill/phases/understand/rules.md +5 -5
- package/ai-dlc-skill/phases/verify/lifecycle.md +24 -24
- package/ai-dlc-skill/phases/verify/prompt.md +8 -8
- package/ai-dlc-skill/phases/verify/rules.md +9 -9
- package/ai-dlc-skill/providers/README.md +2 -2
- package/ai-dlc-skill/providers/aliyun/preview.yaml +2 -2
- package/ai-dlc-skill/providers/tcb/preview.yaml +2 -2
- package/ai-dlc-skill/requirements.md +7 -7
- package/ai-dlc-skill/skill.yaml +4 -4
- package/ai-dlc-skill/templates/integration/CHANGELOG.md +2 -2
- package/ai-dlc-skill/templates/integration/contract-diff.md +7 -7
- package/ai-dlc-skill/templates/integration/contract-spec.md +7 -7
- package/ai-dlc-skill/templates/integration/runtime-contract.yaml +1 -1
- package/ai-dlc-skill/templates/project/README.md +18 -19
- package/ai-dlc-skill/templates/project/template.md +21 -21
- package/ai-dlc-skill/walkthrough/collect.sh +1 -1
- package/ai-dlc-skill/walkthrough/template.md +2 -2
- package/ai-dlc-skill/workflows/ai-dlc.yaml +12 -12
- package/ai-dlc-skill/workflows/deployment.yaml +4 -4
- package/cdh/__pycache__/cdh_mcp_injector.cpython-314.pyc +0 -0
- package/cdh/__pycache__/cdh_mcp_loader.cpython-314.pyc +0 -0
- package/cdh/__pycache__/cdh_mcp_manager.cpython-314.pyc +0 -0
- package/cdh/__pycache__/cdh_session_aggregator.cpython-314.pyc +0 -0
- package/cdh/__pycache__/cdh_skill_loader.cpython-314.pyc +0 -0
- package/cdh/__pycache__/cdh_skill_manager.cpython-314.pyc +0 -0
- package/cdh/__pycache__/cli.cpython-313.pyc +0 -0
- package/cdh/__pycache__/cli.cpython-314.pyc +0 -0
- package/cdh/__pycache__/scaffold.cpython-314.pyc +0 -0
- package/cdh/cdh_mcp_injector.py +115 -0
- package/cdh/cdh_mcp_loader.py +67 -0
- package/cdh/cdh_mcp_manager.py +73 -0
- package/cdh/cdh_session_aggregator.py +258 -0
- package/cdh/cdh_skill_manager.py +105 -0
- package/cdh/cli.py +64 -4
- package/cdh/scaffold.py +192 -41
- package/onecode/__init__.py +1 -1
- 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/__pycache__/config_screen.cpython-314.pyc +0 -0
- package/onecode/__pycache__/migrate.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/cdh_loader.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/context.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/engine.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/hooks.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/onecode_agent_acp.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/permissions.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/project_doc.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 +47 -164
- package/onecode/agent/attachments.py +0 -1
- package/onecode/agent/cdh_loader.py +17 -25
- package/onecode/agent/context.py +8 -3
- package/onecode/agent/engine.py +835 -220
- package/onecode/agent/hooks.py +1 -1
- package/onecode/agent/onecode_agent_acp.py +460 -47
- package/onecode/agent/onecode_agent_acp.py.bak +2447 -0
- package/onecode/agent/permissions.py +0 -1
- package/onecode/agent/project_doc.py +19 -0
- package/onecode/agent/session.py +17 -3
- package/onecode/agent/snapshot.py +3 -4
- package/onecode/agent/tools/__pycache__/agent_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/apply_patch_tool.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/bash_tool.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/communication_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/config_tool.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/cron_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/file_ops.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/file_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/git_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/lsp_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/mcp_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/permission_handler.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/protocol.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/registry.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/sandbox.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/skill_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/todo_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/web_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/agent_tools.py +4 -6
- package/onecode/agent/tools/apply_patch_tool.py +2 -8
- package/onecode/agent/tools/bash_tool.py +32 -12
- package/onecode/agent/tools/communication_tools.py +4 -5
- package/onecode/agent/tools/config_tool.py +3 -3
- package/onecode/agent/tools/cron_tools.py +4 -5
- package/onecode/agent/tools/file_ops.py +37 -3
- package/onecode/agent/tools/file_tools.py +10 -11
- package/onecode/agent/tools/git_tools.py +2 -2
- package/onecode/agent/tools/lsp_tools.py +2 -2
- package/onecode/agent/tools/mcp_tools.py +3 -5
- package/onecode/agent/tools/permission_handler.py +1 -1
- package/onecode/agent/tools/protocol.py +1 -1
- package/onecode/agent/tools/registry.py +69 -2
- package/onecode/agent/tools/sandbox.py +110 -16
- package/onecode/agent/tools/skill_tools.py +2 -2
- package/onecode/agent/tools/todo_tools.py +38 -19
- package/onecode/agent/tools/web_tools.py +6 -7
- package/onecode/builtin_skills/agent-browser/SKILL.md +212 -0
- package/onecode/builtin_skills/agent-browser/skill.yaml +8 -0
- package/onecode/builtin_skills/skill-creator/SKILL.md +106 -0
- package/onecode/builtin_skills/skill-creator/skill.yaml +8 -0
- package/onecode/cli.py +186 -6
- package/onecode/codebase/__pycache__/chunker.cpython-314.pyc +0 -0
- package/onecode/codebase/__pycache__/indexer.cpython-314.pyc +0 -0
- package/onecode/codebase/__pycache__/retriever.cpython-314.pyc +0 -0
- package/onecode/codebase/__pycache__/storage.cpython-314.pyc +0 -0
- package/onecode/codebase/chunker.py +0 -5
- package/onecode/codebase/indexer.py +14 -1
- package/onecode/codebase/retriever.py +13 -2
- package/onecode/codebase/storage.py +5 -4
- package/onecode/commands.py +81 -6
- package/onecode/config.py +41 -12
- package/onecode/config_screen.py +7 -3
- package/onecode/mcp/__pycache__/manager.cpython-314.pyc +0 -0
- package/onecode/mcp/manager.py +2 -3
- package/onecode/memory/__init__.py +7 -53
- package/onecode/memory/__pycache__/__init__.cpython-314.pyc +0 -0
- package/onecode/memory/__pycache__/backend.cpython-314.pyc +0 -0
- package/onecode/memory/__pycache__/offload.cpython-314.pyc +0 -0
- package/onecode/memory/__pycache__/pyramid.cpython-314.pyc +0 -0
- package/onecode/memory/__pycache__/recall.cpython-314.pyc +0 -0
- package/onecode/memory/backend.py +13 -39
- package/onecode/memory/pyramid.py +2 -71
- package/onecode/memory/recall.py +38 -3
- package/onecode/migrate.py +122 -0
- package/onecode/models/__pycache__/messages.cpython-314.pyc +0 -0
- package/onecode/models/__pycache__/provider.cpython-314.pyc +0 -0
- package/onecode/models/messages.py +58 -0
- package/onecode/models/provider.py +20 -2
- package/onecode/models/providers/__pycache__/anthropic_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/anthropic_provider.py +59 -12
- package/onecode/models/providers/glm_provider.py +40 -0
- package/onecode/models/providers/minimax_provider.py +3 -0
- package/onecode/models/providers/minimaxi_provider.py +3 -0
- package/onecode/models/providers/ollama_provider.py +3 -0
- package/onecode/server/app.py +1 -1
- package/onecode/skills/__pycache__/argument_substitution.cpython-314.pyc +0 -0
- package/onecode/skills/__pycache__/bootstrap.cpython-314.pyc +0 -0
- package/onecode/skills/__pycache__/loader.cpython-314.pyc +0 -0
- package/onecode/skills/__pycache__/manager.cpython-314.pyc +0 -0
- package/onecode/skills/argument_substitution.py +0 -1
- package/onecode/skills/bootstrap.py +124 -0
- package/onecode/skills/loader.py +35 -53
- package/onecode/skills/manager.py +2 -2
- package/onecode/storage/activity.py +3 -4
- package/onecode/storage/session.py +3 -4
- package/onecode/tasks/manager.py +23 -10
- package/onecode/trace/tracer.py +2 -3
- package/package.json +1 -1
- package/pyproject.toml +5 -1
- package/run.js +68 -14
- package/tui/__pycache__/about.cpython-314.pyc +0 -0
- package/tui/__pycache__/app.cpython-313.pyc +0 -0
- package/tui/__pycache__/app.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/__pycache__/messages.cpython-314.pyc +0 -0
- package/tui/__pycache__/path_complete.cpython-314.pyc +0 -0
- package/tui/__pycache__/paths.cpython-314.pyc +0 -0
- package/tui/__pycache__/session_tracker.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 +125 -58
- package/tui/acp/api.py +12 -0
- package/tui/acp/messages.py +3 -1
- package/tui/acp/prompt.py +0 -57
- package/tui/acp/protocol.py +6 -0
- package/tui/app.py +11 -33
- package/tui/messages.py +2 -0
- package/tui/path_complete.py +1 -1
- package/tui/paths.py +16 -0
- package/tui/screens/__pycache__/agent_modal.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/component_picker.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/diff_screen.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/log.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/main.cpython-313.pyc +0 -0
- package/tui/screens/__pycache__/main.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/permissions.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/projects_app.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__/store.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/tool_content_screen.cpython-314.pyc +0 -0
- package/tui/screens/component_picker.py +35 -15
- package/tui/screens/diff_screen.py +84 -0
- package/tui/screens/log.py +25 -25
- package/tui/screens/main.py +48 -26
- package/tui/screens/projects.tcss +5 -1
- package/tui/screens/projects_app.py +6 -6
- package/tui/screens/projects_screen.py +19 -39
- package/tui/screens/sessions.py +3 -5
- package/tui/session_tracker.py +3 -0
- package/tui/settings_schema.py +1 -1
- package/tui/visuals/__pycache__/columns.cpython-314.pyc +0 -0
- package/tui/visuals/columns.py +1 -2
- package/tui/widgets/__pycache__/acp_content.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/agent_response.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/agent_thought.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/conversation.cpython-314.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__/load_more.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__/modified_files.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/plan.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__/side_bar.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/subagent.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/subagent_screen.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/terminal.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/tool_call.cpython-314.pyc +0 -0
- package/tui/widgets/agent_response.py +0 -1
- package/tui/widgets/agent_thought.py +0 -1
- package/tui/widgets/conversation.py +157 -14
- package/tui/widgets/diff_view.py +10 -0
- package/tui/widgets/load_more.py +24 -0
- package/tui/widgets/modified_files.py +86 -12
- package/tui/widgets/plan.py +1 -1
- package/tui/widgets/project_grid_select.py +9 -2
- package/tui/widgets/session_grid_select.py +6 -0
- package/tui/widgets/side_bar.py +10 -0
- package/tui/widgets/subagent.py +92 -58
- package/tui/widgets/subagent_screen.py +638 -115
- package/tui/widgets/terminal.py +1 -2
- package/tui/widgets/terminal_tool.py +11 -2
- package/tui/widgets/tool_call.py +35 -2
- package/onecode/memory/offload.py +0 -114
- package/onecode/memory/symbolic.py +0 -164
|
@@ -43,16 +43,31 @@ def debug_log(*args, **kwargs):
|
|
|
43
43
|
print("[onecode]", *args, file=sys.stderr, flush=True)
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
def info_log(*args, **kwargs):
|
|
47
|
+
"""INFO-level lifecycle log so critical events survive even default log_level=info.
|
|
48
|
+
|
|
49
|
+
``debug_log`` is filtered out at INFO; previously all ACP/subagent
|
|
50
|
+
diagnostic markers were DEBUG, so users running with the default
|
|
51
|
+
``log_level: info`` saw an empty log and could not diagnose the
|
|
52
|
+
subagent hang. Use ``info_log`` for critical lifecycle boundaries
|
|
53
|
+
(adapter init, cancel, subagent dispatch markers).
|
|
54
|
+
"""
|
|
55
|
+
if logger.handlers or logging.getLogger().handlers:
|
|
56
|
+
logger.info(" ".join(str(a) for a in args), **kwargs)
|
|
57
|
+
else:
|
|
58
|
+
print("[onecode]", *args, file=sys.stderr, flush=True)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
_CRASH_LOG_DIR = Path.home() / ".onecode" / "logs"
|
|
47
62
|
|
|
48
63
|
|
|
49
64
|
def _dump_crash(context: str) -> None:
|
|
50
|
-
"""Write traceback to ``~/.
|
|
65
|
+
"""Write traceback to ``~/.onecode/logs/onecode_crash.log`` so it survives subprocess exit."""
|
|
51
66
|
import os
|
|
52
67
|
import traceback
|
|
53
68
|
now = __import__("datetime").datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
|
|
54
69
|
_CRASH_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
55
|
-
path = _CRASH_LOG_DIR / "
|
|
70
|
+
path = _CRASH_LOG_DIR / "onecode_crash.log"
|
|
56
71
|
try:
|
|
57
72
|
lines = [f"=== crash at {now} context={context} ===\n"]
|
|
58
73
|
for tb_line in traceback.format_exc().splitlines(True):
|
|
@@ -73,7 +88,6 @@ from onecode.models.registry import ModelRegistry
|
|
|
73
88
|
from onecode.models.messages import (
|
|
74
89
|
StreamEventType,
|
|
75
90
|
ToolCategory,
|
|
76
|
-
AgentMessage,
|
|
77
91
|
TextBlock,
|
|
78
92
|
ThinkBlock,
|
|
79
93
|
ToolCall as MsgToolCall,
|
|
@@ -82,14 +96,8 @@ from onecode.models.messages import (
|
|
|
82
96
|
LifecycleStatus,
|
|
83
97
|
get_tool_category,
|
|
84
98
|
)
|
|
99
|
+
import onecode.models.providers # noqa: F401 trigger ProviderRegistry.register()
|
|
85
100
|
|
|
86
|
-
from onecode.models.providers.minimaxi_provider import MiniMaxiProvider
|
|
87
|
-
from onecode.models.providers.minimax_provider import MiniMaxProvider
|
|
88
|
-
from onecode.models.providers.anthropic_provider import AnthropicProvider
|
|
89
|
-
from onecode.models.providers.openai_provider import OpenAIProvider
|
|
90
|
-
from onecode.models.providers.deepseek_provider import DeepSeekProvider
|
|
91
|
-
from onecode.models.providers.glm_provider import GLMProvider
|
|
92
|
-
from onecode.models.providers.ollama_provider import OllamaProvider
|
|
93
101
|
|
|
94
102
|
|
|
95
103
|
_CATEGORY_TO_ACP_KIND: dict[str, str] = {
|
|
@@ -325,8 +333,8 @@ def _build_tool_call_content(name: str | None, arguments: dict) -> list:
|
|
|
325
333
|
(Markdown / Static). JSON-fence fallbacks only kick in for tools we
|
|
326
334
|
don't recognise, so known tools render as their intended visual.
|
|
327
335
|
|
|
328
|
-
Write emits a
|
|
329
|
-
|
|
336
|
+
Write emits a code fence so the content reads as proper code
|
|
337
|
+
rather than a noisy all-additions diff view.
|
|
330
338
|
Edit keeps the diff block because there are real before/after changes.
|
|
331
339
|
"""
|
|
332
340
|
if not arguments:
|
|
@@ -336,8 +344,8 @@ def _build_tool_call_content(name: str | None, arguments: dict) -> list:
|
|
|
336
344
|
return []
|
|
337
345
|
|
|
338
346
|
_HEADER_ONLY_TOOLS = frozenset({
|
|
339
|
-
"TodoCreate", "TodoGet", "TodoList", "TodoUpdate", "TodoOutput", "TodoStop",
|
|
340
|
-
"Glob", "Spawn",
|
|
347
|
+
"TodoCreate", "TodoGet", "TodoList", "TodoUpdate", "TodoOutput", "TodoStop", "TodoClear",
|
|
348
|
+
"Glob", "List", "Spawn",
|
|
341
349
|
})
|
|
342
350
|
if name in _HEADER_ONLY_TOOLS:
|
|
343
351
|
return []
|
|
@@ -351,7 +359,7 @@ def _build_tool_call_content(name: str | None, arguments: dict) -> list:
|
|
|
351
359
|
"type": "content",
|
|
352
360
|
"content": {
|
|
353
361
|
"type": "text",
|
|
354
|
-
"text": f"
|
|
362
|
+
"text": f"```{lang}\n{content}\n```",
|
|
355
363
|
},
|
|
356
364
|
}]
|
|
357
365
|
|
|
@@ -378,6 +386,21 @@ def _build_tool_call_content(name: str | None, arguments: dict) -> list:
|
|
|
378
386
|
},
|
|
379
387
|
}]
|
|
380
388
|
|
|
389
|
+
if name == "Grep":
|
|
390
|
+
pattern = str(arguments.get("pattern", ""))
|
|
391
|
+
include = arguments.get("include")
|
|
392
|
+
if pattern:
|
|
393
|
+
text = f"🔍 Pattern: `{pattern}`"
|
|
394
|
+
if include:
|
|
395
|
+
text += f"\n📁 Filter: `{include}`"
|
|
396
|
+
return [{
|
|
397
|
+
"type": "content",
|
|
398
|
+
"content": {"type": "text", "text": text},
|
|
399
|
+
}]
|
|
400
|
+
|
|
401
|
+
if name == "ToolSearch":
|
|
402
|
+
return []
|
|
403
|
+
|
|
381
404
|
if name == "ApplyPatch":
|
|
382
405
|
patch = str(arguments.get("patch", ""))
|
|
383
406
|
if patch:
|
|
@@ -424,11 +447,7 @@ def _build_tool_call_content(name: str | None, arguments: dict) -> list:
|
|
|
424
447
|
"content": {"type": "text", "text": "\n".join(lines)},
|
|
425
448
|
}] if lines else []
|
|
426
449
|
|
|
427
|
-
|
|
428
|
-
return [{
|
|
429
|
-
"type": "content",
|
|
430
|
-
"content": {"type": "text", "text": f"```json\n{args_text}\n```"},
|
|
431
|
-
}]
|
|
450
|
+
return []
|
|
432
451
|
|
|
433
452
|
|
|
434
453
|
def _format_tui_display_text(result_text: str, tool_name: str = "") -> str:
|
|
@@ -456,6 +475,19 @@ def _format_tui_display_text(result_text: str, tool_name: str = "") -> str:
|
|
|
456
475
|
return result_text
|
|
457
476
|
if not isinstance(parsed, dict):
|
|
458
477
|
return result_text
|
|
478
|
+
if tool_name == "ToolSearch":
|
|
479
|
+
matches = parsed.get("matches", [])
|
|
480
|
+
if matches:
|
|
481
|
+
lines = [f"🔍 Found {len(matches)} tool(s):", ""]
|
|
482
|
+
for m in matches:
|
|
483
|
+
name = m.get("name", "")
|
|
484
|
+
desc = m.get("description", "")
|
|
485
|
+
if desc:
|
|
486
|
+
lines.append(f" • **{name}** — {desc}")
|
|
487
|
+
else:
|
|
488
|
+
lines.append(f" • **{name}**")
|
|
489
|
+
return "\n".join(lines)
|
|
490
|
+
return "No matching tools found."
|
|
459
491
|
if "error" in parsed:
|
|
460
492
|
return str(parsed["error"])
|
|
461
493
|
# Read: show actual file content (code fence + language wrapping in _emit_tool_result)
|
|
@@ -464,10 +496,12 @@ def _format_tui_display_text(result_text: str, tool_name: str = "") -> str:
|
|
|
464
496
|
# Todo tools: collapse verbose state echoes to one-liner.
|
|
465
497
|
if tool_name in _STATUS_ONLY_TOOLS:
|
|
466
498
|
return "✓ updated"
|
|
499
|
+
if tool_name == "Write":
|
|
500
|
+
return ""
|
|
467
501
|
if parsed.get("success") is True:
|
|
468
502
|
if path := parsed.get("path"):
|
|
469
503
|
return f"✓ {path}"
|
|
470
|
-
visible = {k: v for k, v in parsed.items() if k != "success"}
|
|
504
|
+
visible = {k: v for k, v in parsed.items() if k != "success" and v not in ("", None)}
|
|
471
505
|
if not visible:
|
|
472
506
|
return "✓ done"
|
|
473
507
|
try:
|
|
@@ -484,6 +518,7 @@ _STATUS_ONLY_TOOLS = frozenset({
|
|
|
484
518
|
"TodoUpdate",
|
|
485
519
|
"TodoOutput",
|
|
486
520
|
"TodoStop",
|
|
521
|
+
"TodoClear",
|
|
487
522
|
})
|
|
488
523
|
|
|
489
524
|
|
|
@@ -661,6 +696,14 @@ class CDHACPAdapter:
|
|
|
661
696
|
self._request_seq = 0
|
|
662
697
|
self._perm_store = PermissionStore()
|
|
663
698
|
self._ask_user_future: asyncio.Future[dict] | None = None
|
|
699
|
+
# Diagnostics: per-subagent ACP forward counters (id → int)
|
|
700
|
+
self._subagent_fwd_count: dict[str, int] = {}
|
|
701
|
+
self._subagent_fwd_bytes: dict[str, int] = {}
|
|
702
|
+
# Display state for tools invoked *inside* a subagent, keyed by a
|
|
703
|
+
# namespaced id ``"sa:{subagent_id}:{inner_tool_id}"`` (kept separate
|
|
704
|
+
# from ``self.tool_calls`` so subagent tool cards never collide with
|
|
705
|
+
# main-agent tool ids nor get persisted into the main session view).
|
|
706
|
+
self._subagent_tool_calls: dict[str, dict] = {}
|
|
664
707
|
|
|
665
708
|
def _perm_store_load(self, cwd: str | None = None) -> None:
|
|
666
709
|
"""Load permission overrides from ``.cdh/permissions.json``."""
|
|
@@ -837,9 +880,23 @@ class CDHACPAdapter:
|
|
|
837
880
|
def cancel_prompt(self):
|
|
838
881
|
"""Synchronously cancel the current prompt (called from main loop)."""
|
|
839
882
|
if self.agent:
|
|
883
|
+
n_children = len(self.agent._child_engines)
|
|
884
|
+
child_ids = [hex(id(c)) for c in self.agent._child_engines]
|
|
885
|
+
info_log(
|
|
886
|
+
"[ACP-CANCEL] cancel_prompt called; marking agent._cancelled=True; "
|
|
887
|
+
"children=%d ids=%s already_cancelled=%s",
|
|
888
|
+
n_children, child_ids, self.agent._cancelled,
|
|
889
|
+
)
|
|
840
890
|
self.agent._cancelled = True
|
|
841
891
|
for child in self.agent._child_engines:
|
|
842
892
|
child._cancelled = True
|
|
893
|
+
info_log(
|
|
894
|
+
"[ACP-CANCEL] after mark: agent._cancelled=%s subagent_fwd=%s",
|
|
895
|
+
self.agent._cancelled,
|
|
896
|
+
dict(self._subagent_fwd_count),
|
|
897
|
+
)
|
|
898
|
+
else:
|
|
899
|
+
info_log("[ACP-CANCEL] cancel_prompt called but self.agent is None")
|
|
843
900
|
|
|
844
901
|
def resolve_ask_user(self, answer: str, cancelled: bool) -> dict:
|
|
845
902
|
"""Resolve the pending ask_user request with the user's answer.
|
|
@@ -857,6 +914,156 @@ class CDHACPAdapter:
|
|
|
857
914
|
"update": update,
|
|
858
915
|
})
|
|
859
916
|
|
|
917
|
+
def _subagent_tool_ns_id(self, subagent_id: str, inner_tool_id: str) -> str:
|
|
918
|
+
"""Namespaced toolCallId so subagent tool cards never collide with
|
|
919
|
+
main-agent tool ids in the TUI or in ``self.tool_calls``."""
|
|
920
|
+
return f"sa:{subagent_id}:{inner_tool_id}"
|
|
921
|
+
|
|
922
|
+
def _handle_subagent_tool_call(self, event) -> None:
|
|
923
|
+
"""Render a tool invoked *inside* a subagent as a real tool_call /
|
|
924
|
+
tool_call_update sessionUpdate tagged with ``subagentId`` so the TUI
|
|
925
|
+
mounts a ToolCall card inside the owning SubAgent widget (same style
|
|
926
|
+
as main-conversation tools) instead of a ``[Tool: X]`` text line."""
|
|
927
|
+
sa_id = event.subagent_id
|
|
928
|
+
ns_id = self._subagent_tool_ns_id(sa_id, event.tool_id)
|
|
929
|
+
tname = event.tool_name or "Tool"
|
|
930
|
+
if tname == "Spawn":
|
|
931
|
+
# Nested spawn is disallowed (depth limit); skip rendering.
|
|
932
|
+
return
|
|
933
|
+
tool_kind = _kind_for_category(event.tool_category)
|
|
934
|
+
|
|
935
|
+
if event.subagent_tool_phase != "complete":
|
|
936
|
+
# start: in_progress card with the tool name as title.
|
|
937
|
+
entry = self._subagent_tool_calls.get(ns_id)
|
|
938
|
+
if entry is None:
|
|
939
|
+
entry = {
|
|
940
|
+
"sessionUpdate": "tool_call",
|
|
941
|
+
"toolCallId": ns_id,
|
|
942
|
+
"subagentId": sa_id,
|
|
943
|
+
"title": tname,
|
|
944
|
+
"kind": tool_kind,
|
|
945
|
+
"status": "in_progress",
|
|
946
|
+
"content": [],
|
|
947
|
+
"_tool_name": tname,
|
|
948
|
+
"_tool_args": {},
|
|
949
|
+
}
|
|
950
|
+
self._subagent_tool_calls[ns_id] = entry
|
|
951
|
+
else:
|
|
952
|
+
entry["subagentId"] = sa_id
|
|
953
|
+
entry["kind"] = tool_kind
|
|
954
|
+
entry["status"] = "in_progress"
|
|
955
|
+
entry["_tool_name"] = tname
|
|
956
|
+
self.send_session_update(entry)
|
|
957
|
+
return
|
|
958
|
+
|
|
959
|
+
# complete: enrich title from args + attach args content.
|
|
960
|
+
args = event.tool_args or {}
|
|
961
|
+
title = tname
|
|
962
|
+
if args:
|
|
963
|
+
if args.get("path", ""):
|
|
964
|
+
title = f"{tname}: {_short_path(args.get('path', ''), self.agent._project_dir)}"
|
|
965
|
+
elif tname == "Bash":
|
|
966
|
+
cmd = str(args.get("command", ""))
|
|
967
|
+
if cmd:
|
|
968
|
+
title = f"Bash: {cmd}"
|
|
969
|
+
elif tname in ("TodoCreate", "TodoUpdate"):
|
|
970
|
+
subject = str(args.get("subject", ""))
|
|
971
|
+
if subject:
|
|
972
|
+
title = f"{tname}: {subject}"
|
|
973
|
+
elif tname in ("TodoGet", "TodoStop", "TodoOutput"):
|
|
974
|
+
tid = str(args.get("taskId", args.get("task_id", "")))
|
|
975
|
+
if tid:
|
|
976
|
+
title = f"{tname}: {tid}"
|
|
977
|
+
elif tname == "Glob":
|
|
978
|
+
pattern = str(args.get("pattern", ""))
|
|
979
|
+
if pattern:
|
|
980
|
+
title = f"Glob: {pattern}"
|
|
981
|
+
elif tname == "ToolSearch":
|
|
982
|
+
query = str(args.get("query", ""))
|
|
983
|
+
if query:
|
|
984
|
+
title = f"ToolSearch: {query}"
|
|
985
|
+
else:
|
|
986
|
+
for key in ("query", "command", "pattern", "name", "subject"):
|
|
987
|
+
val = str(args.get(key, ""))
|
|
988
|
+
if val:
|
|
989
|
+
title = f"{tname}: {val}"
|
|
990
|
+
break
|
|
991
|
+
content = _build_tool_call_content(tname, args)
|
|
992
|
+
entry = self._subagent_tool_calls.get(ns_id)
|
|
993
|
+
if entry is None:
|
|
994
|
+
entry = {
|
|
995
|
+
"sessionUpdate": "tool_call",
|
|
996
|
+
"toolCallId": ns_id,
|
|
997
|
+
"subagentId": sa_id,
|
|
998
|
+
"title": tname,
|
|
999
|
+
"kind": tool_kind,
|
|
1000
|
+
"status": "in_progress",
|
|
1001
|
+
"content": [],
|
|
1002
|
+
"_tool_name": tname,
|
|
1003
|
+
"_tool_args": {},
|
|
1004
|
+
}
|
|
1005
|
+
self._subagent_tool_calls[ns_id] = entry
|
|
1006
|
+
entry.update({
|
|
1007
|
+
"sessionUpdate": "tool_call_update",
|
|
1008
|
+
"toolCallId": ns_id,
|
|
1009
|
+
"subagentId": sa_id,
|
|
1010
|
+
"title": title,
|
|
1011
|
+
"status": "in_progress",
|
|
1012
|
+
"content": content,
|
|
1013
|
+
"_tool_name": tname,
|
|
1014
|
+
"_tool_args": args,
|
|
1015
|
+
})
|
|
1016
|
+
self.send_session_update(entry)
|
|
1017
|
+
|
|
1018
|
+
def _handle_subagent_tool_result(self, event) -> None:
|
|
1019
|
+
"""Attach a subagent tool's result to its card (completed/failed)."""
|
|
1020
|
+
sa_id = event.subagent_id
|
|
1021
|
+
ns_id = self._subagent_tool_ns_id(sa_id, event.tool_id)
|
|
1022
|
+
entry = self._subagent_tool_calls.get(ns_id)
|
|
1023
|
+
tname = (entry or {}).get("_tool_name", "") or event.tool_name or "Tool"
|
|
1024
|
+
targs = (entry or {}).get("_tool_args", {}) or {}
|
|
1025
|
+
if tname == "Spawn":
|
|
1026
|
+
return
|
|
1027
|
+
status = "failed" if event.result_is_error else "completed"
|
|
1028
|
+
display_text = _format_tui_display_text(event.result_content or "", tname)
|
|
1029
|
+
# Fence multi-line results so the TUI renders a code block (mirrors
|
|
1030
|
+
# the main TOOL_RESULT handler).
|
|
1031
|
+
if display_text and "\n" in display_text and "```" not in display_text:
|
|
1032
|
+
lang = ""
|
|
1033
|
+
if tname in ("Bash", "Glob"):
|
|
1034
|
+
lang = "bash"
|
|
1035
|
+
elif tname == "Read" and isinstance(targs, dict):
|
|
1036
|
+
path = str(targs.get("path", ""))
|
|
1037
|
+
if path:
|
|
1038
|
+
lang = _language_for_path(path)
|
|
1039
|
+
display_text = f"```{lang}\n{display_text}\n```" if lang else f"```\n{display_text}\n```"
|
|
1040
|
+
content_block = [{
|
|
1041
|
+
"type": "content",
|
|
1042
|
+
"content": {"type": "text", "text": display_text},
|
|
1043
|
+
}] if display_text else []
|
|
1044
|
+
existing_content = (entry or {}).get("content", [])
|
|
1045
|
+
if not isinstance(existing_content, list):
|
|
1046
|
+
existing_content = []
|
|
1047
|
+
if entry is None:
|
|
1048
|
+
entry = {
|
|
1049
|
+
"sessionUpdate": "tool_call_update",
|
|
1050
|
+
"toolCallId": ns_id,
|
|
1051
|
+
"subagentId": sa_id,
|
|
1052
|
+
"title": tname,
|
|
1053
|
+
"kind": _kind_for_category(get_tool_category(tname)),
|
|
1054
|
+
"status": status,
|
|
1055
|
+
"content": existing_content + content_block,
|
|
1056
|
+
"_tool_name": tname,
|
|
1057
|
+
"_tool_args": targs,
|
|
1058
|
+
}
|
|
1059
|
+
self._subagent_tool_calls[ns_id] = entry
|
|
1060
|
+
else:
|
|
1061
|
+
entry["subagentId"] = sa_id
|
|
1062
|
+
entry["status"] = status
|
|
1063
|
+
entry["content"] = existing_content + content_block
|
|
1064
|
+
entry["sessionUpdate"] = "tool_call_update"
|
|
1065
|
+
self.send_session_update(entry)
|
|
1066
|
+
|
|
860
1067
|
def _emit_tool_result(self, block: ToolResult) -> None:
|
|
861
1068
|
"""Send a tool_result as a tool_call_update, accumulating with existing content."""
|
|
862
1069
|
# Spawn results are rendered as SubAgent widgets via
|
|
@@ -870,6 +1077,11 @@ class CDHACPAdapter:
|
|
|
870
1077
|
tool_name = tool_name.split(":", 1)[0].strip()
|
|
871
1078
|
display_text = _format_tui_display_text(block.content, tool_name)
|
|
872
1079
|
|
|
1080
|
+
# For edit/write tools the diff/code block + header ✔ status is
|
|
1081
|
+
# sufficient — skip the redundant "✓ /path" result text.
|
|
1082
|
+
if display_text and self.tool_calls.get(block.tool_use_id, {}).get("kind") == "edit":
|
|
1083
|
+
display_text = ""
|
|
1084
|
+
|
|
873
1085
|
# Update title with path/subject from result if not already set
|
|
874
1086
|
if block.content and block.tool_use_id in self.tool_calls:
|
|
875
1087
|
try:
|
|
@@ -1015,6 +1227,8 @@ class CDHACPAdapter:
|
|
|
1015
1227
|
|
|
1016
1228
|
async def session_new(self, cwd: str, mcp_servers: list):
|
|
1017
1229
|
"""Create new session."""
|
|
1230
|
+
if self.agent is not None:
|
|
1231
|
+
await self.agent.shutdown()
|
|
1018
1232
|
cfg = load_config()
|
|
1019
1233
|
self.agent = _create_engine(cwd, perm_store=self._perm_store)
|
|
1020
1234
|
self.agent.set_agent(cfg.default_mode)
|
|
@@ -1031,9 +1245,14 @@ class CDHACPAdapter:
|
|
|
1031
1245
|
self.agent.attach_session(session)
|
|
1032
1246
|
self.session_id = session.id
|
|
1033
1247
|
|
|
1034
|
-
#
|
|
1035
|
-
|
|
1036
|
-
|
|
1248
|
+
# New sessions start with a blank plan — inject a hint so the user
|
|
1249
|
+
# knows previous session todos are not restored.
|
|
1250
|
+
self.agent.context.add_system(
|
|
1251
|
+
"<!-- NEW_SESSION_HINT -->\n"
|
|
1252
|
+
"This is a new session with a blank plan. Previous session todos "
|
|
1253
|
+
"are not automatically restored. Use `/resume <session_id>` to "
|
|
1254
|
+
"recover a previous session's plan if needed.\n"
|
|
1255
|
+
)
|
|
1037
1256
|
self._send_available_commands()
|
|
1038
1257
|
|
|
1039
1258
|
return {
|
|
@@ -1046,6 +1265,8 @@ class CDHACPAdapter:
|
|
|
1046
1265
|
|
|
1047
1266
|
async def session_load(self, cwd: str, mcp_servers: list, session_id: str):
|
|
1048
1267
|
"""Load existing session."""
|
|
1268
|
+
if self.agent is not None:
|
|
1269
|
+
await self.agent.shutdown()
|
|
1049
1270
|
self._perm_store_load(cwd)
|
|
1050
1271
|
self.agent = _create_engine(cwd, perm_store=self._perm_store)
|
|
1051
1272
|
self.session_id = session_id
|
|
@@ -1064,13 +1285,44 @@ class CDHACPAdapter:
|
|
|
1064
1285
|
self._emit_plan_update_to_tui()
|
|
1065
1286
|
|
|
1066
1287
|
if not loaded:
|
|
1067
|
-
return {
|
|
1288
|
+
return {
|
|
1289
|
+
"modes": _DEFAULT_MODES,
|
|
1290
|
+
"error": f"Session {session_id} not found at {self.agent._session.storage_path}",
|
|
1291
|
+
}
|
|
1068
1292
|
|
|
1069
1293
|
# Walk the in-memory context (preserves list-of-blocks structure)
|
|
1070
1294
|
# instead of the raw _session.messages dicts, which only carry
|
|
1071
1295
|
# `content: str`. Engine.chat_stream() stores tool_use / tool_result
|
|
1072
1296
|
# blocks in context.Message.content, and we need them at replay time.
|
|
1073
|
-
|
|
1297
|
+
# Only replay the most recent MAX_VISIBLE messages; older ones are
|
|
1298
|
+
# fetched on demand via session/load_earlier.
|
|
1299
|
+
MAX_VISIBLE = 50
|
|
1300
|
+
total_messages = len(self.agent.context.messages)
|
|
1301
|
+
visible_start = max(0, total_messages - MAX_VISIBLE)
|
|
1302
|
+
self._replay_messages(self.agent.context.messages[visible_start:])
|
|
1303
|
+
|
|
1304
|
+
return {
|
|
1305
|
+
"modes": {
|
|
1306
|
+
"currentModeId": cfg.default_mode,
|
|
1307
|
+
"availableModes": _DEFAULT_MODES["availableModes"],
|
|
1308
|
+
},
|
|
1309
|
+
"_total_messages": total_messages,
|
|
1310
|
+
"_visible_start": visible_start,
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
async def session_load_earlier(self, session_id: str, offset: int, limit: int) -> dict:
|
|
1314
|
+
"""Replay a range of earlier messages (used by 'load earlier messages')."""
|
|
1315
|
+
if self.agent is None or self.agent.context is None:
|
|
1316
|
+
return {"count": 0, "remaining": 0}
|
|
1317
|
+
messages = self.agent.context.messages
|
|
1318
|
+
end = min(offset + limit, len(messages))
|
|
1319
|
+
segment = messages[offset:end]
|
|
1320
|
+
self._replay_messages(segment)
|
|
1321
|
+
return {"count": end - offset, "remaining": offset}
|
|
1322
|
+
|
|
1323
|
+
def _replay_messages(self, messages: list) -> None:
|
|
1324
|
+
"""Replay a list of context messages as session/update notifications."""
|
|
1325
|
+
for ctx_msg in messages:
|
|
1074
1326
|
role = ctx_msg.role
|
|
1075
1327
|
content = ctx_msg.content
|
|
1076
1328
|
if role == "user":
|
|
@@ -1146,10 +1398,6 @@ class CDHACPAdapter:
|
|
|
1146
1398
|
elif isinstance(block, ToolResult):
|
|
1147
1399
|
self._emit_tool_result(block)
|
|
1148
1400
|
elif isinstance(block, SubAgentBlock):
|
|
1149
|
-
content_block = [{
|
|
1150
|
-
"type": "content",
|
|
1151
|
-
"content": {"type": "text", "text": block.result},
|
|
1152
|
-
}] if block.result else []
|
|
1153
1401
|
self.send_session_update({
|
|
1154
1402
|
"sessionUpdate": "subagent_start",
|
|
1155
1403
|
"subagentId": block.id,
|
|
@@ -1183,13 +1431,6 @@ class CDHACPAdapter:
|
|
|
1183
1431
|
content.get("is_error", False),
|
|
1184
1432
|
)
|
|
1185
1433
|
|
|
1186
|
-
return {
|
|
1187
|
-
"modes": {
|
|
1188
|
-
"currentModeId": cfg.default_mode,
|
|
1189
|
-
"availableModes": _DEFAULT_MODES["availableModes"],
|
|
1190
|
-
},
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
1434
|
def _make_stream_callback(self):
|
|
1194
1435
|
"""Create a thinking-aware streaming callback for real-time token output.
|
|
1195
1436
|
|
|
@@ -1484,6 +1725,13 @@ class CDHACPAdapter:
|
|
|
1484
1725
|
# Fresh per-turn tracking (tool_calls accumulates across one turn only)
|
|
1485
1726
|
self.tool_calls = {}
|
|
1486
1727
|
self.agent._cancelled = False
|
|
1728
|
+
self._subagent_fwd_count = {}
|
|
1729
|
+
self._subagent_fwd_bytes = {}
|
|
1730
|
+
self._subagent_tool_calls = {}
|
|
1731
|
+
info_log(
|
|
1732
|
+
"[ACP-PROMPT] entering chat_stream loop; agent._cancelled reset to "
|
|
1733
|
+
"False; session=%s", session_id,
|
|
1734
|
+
)
|
|
1487
1735
|
|
|
1488
1736
|
# Create chunkers for agent_message_chunk and agent_thought_chunk
|
|
1489
1737
|
self._text_chunker = TextChunker(
|
|
@@ -1589,6 +1837,20 @@ class CDHACPAdapter:
|
|
|
1589
1837
|
pattern = str(event.tool_args.get("pattern", ""))
|
|
1590
1838
|
if pattern:
|
|
1591
1839
|
title = f"Glob: {pattern}"
|
|
1840
|
+
elif event.tool_name == "Grep":
|
|
1841
|
+
pattern = str(event.tool_args.get("pattern", ""))
|
|
1842
|
+
if pattern:
|
|
1843
|
+
title = f"Grep: {pattern}"
|
|
1844
|
+
elif event.tool_name == "ToolSearch":
|
|
1845
|
+
query = str(event.tool_args.get("query", ""))
|
|
1846
|
+
if query:
|
|
1847
|
+
title = f"ToolSearch: {query}"
|
|
1848
|
+
else:
|
|
1849
|
+
for key in ("query", "command", "pattern", "name", "subject"):
|
|
1850
|
+
val = str(event.tool_args.get(key, ""))
|
|
1851
|
+
if val:
|
|
1852
|
+
title = f"{event.tool_name}: {val}"
|
|
1853
|
+
break
|
|
1592
1854
|
content = _build_tool_call_content(
|
|
1593
1855
|
event.tool_name, event.tool_args
|
|
1594
1856
|
)
|
|
@@ -1633,6 +1895,11 @@ class CDHACPAdapter:
|
|
|
1633
1895
|
event.result_content or "", tname,
|
|
1634
1896
|
)
|
|
1635
1897
|
|
|
1898
|
+
# For edit/write tools the diff/code block + header ✔ status is
|
|
1899
|
+
# sufficient — skip the redundant "✓ /path" result text.
|
|
1900
|
+
if display_text and self.tool_calls.get(event.tool_id, {}).get("kind") == "edit":
|
|
1901
|
+
display_text = ""
|
|
1902
|
+
|
|
1636
1903
|
# Update title with path/subject from result if not already set
|
|
1637
1904
|
if event.result_content and event.tool_id in self.tool_calls:
|
|
1638
1905
|
try:
|
|
@@ -1662,14 +1929,14 @@ class CDHACPAdapter:
|
|
|
1662
1929
|
# Wrap multi-line results in a fenced code block so they
|
|
1663
1930
|
# display as a code block rather than a raw paragraph.
|
|
1664
1931
|
# For Read results, detect language from the file path.
|
|
1665
|
-
# For Bash/Glob, use bash-style fence.
|
|
1932
|
+
# For Bash/Glob/Grep/List, use bash-style fence.
|
|
1666
1933
|
# Skip if already contains a code fence to avoid nesting.
|
|
1667
|
-
if display_text and "\n" in display_text and "```" not in display_text:
|
|
1934
|
+
if display_text and "\n" in display_text and "```" not in display_text and tname != "ToolSearch":
|
|
1668
1935
|
lang = ""
|
|
1669
1936
|
tool_info = self.tool_calls.get(event.tool_id, {})
|
|
1670
1937
|
tname = tool_info.get("_tool_name", "")
|
|
1671
1938
|
targs = tool_info.get("_tool_args", {})
|
|
1672
|
-
if tname in ("Bash", "Glob"):
|
|
1939
|
+
if tname in ("Bash", "Glob", "Grep", "List"):
|
|
1673
1940
|
lang = "bash"
|
|
1674
1941
|
elif tname == "Read" and isinstance(targs, dict):
|
|
1675
1942
|
path = str(targs.get("path", ""))
|
|
@@ -1729,25 +1996,66 @@ class CDHACPAdapter:
|
|
|
1729
1996
|
elif event.type == StreamEventType.SUBAGENT_START:
|
|
1730
1997
|
self._text_chunker.flush()
|
|
1731
1998
|
self._thought_chunker.flush()
|
|
1999
|
+
info_log(
|
|
2000
|
+
"[ACP-SUBagent] SUBAGENT_START id=%s type=%s prompt_len=%d",
|
|
2001
|
+
event.subagent_id, event.subagent_type,
|
|
2002
|
+
len(event.subagent_prompt or ""),
|
|
2003
|
+
)
|
|
1732
2004
|
self.send_session_update({
|
|
1733
2005
|
"sessionUpdate": "subagent_start",
|
|
1734
2006
|
"subagentId": event.subagent_id,
|
|
1735
2007
|
"agentType": event.subagent_type,
|
|
1736
2008
|
"prompt": event.subagent_prompt,
|
|
1737
2009
|
})
|
|
2010
|
+
self._subagent_fwd_count[event.subagent_id] = 0
|
|
2011
|
+
self._subagent_fwd_bytes[event.subagent_id] = 0
|
|
1738
2012
|
elif event.type == StreamEventType.SUBAGENT_CHUNK:
|
|
2013
|
+
_cnt = self._subagent_fwd_count.get(event.subagent_id, 0) + 1
|
|
2014
|
+
self._subagent_fwd_count[event.subagent_id] = _cnt
|
|
2015
|
+
_by = self._subagent_fwd_bytes.get(event.subagent_id, 0) + len(event.subagent_text or "")
|
|
2016
|
+
self._subagent_fwd_bytes[event.subagent_id] = _by
|
|
2017
|
+
if _cnt <= 5 or _cnt % 50 == 0:
|
|
2018
|
+
debug_log(
|
|
2019
|
+
"[ACP-SUBagent] SUBAGENT_CHUNK id=%s chunk#%d bytes=%d total=%d",
|
|
2020
|
+
event.subagent_id, _cnt, len(event.subagent_text or ""),
|
|
2021
|
+
_by,
|
|
2022
|
+
)
|
|
1739
2023
|
self.send_session_update({
|
|
1740
2024
|
"sessionUpdate": "subagent_chunk",
|
|
1741
2025
|
"subagentId": event.subagent_id,
|
|
1742
2026
|
"text": event.subagent_text,
|
|
1743
2027
|
})
|
|
1744
2028
|
elif event.type == StreamEventType.SUBAGENT_THINKING:
|
|
2029
|
+
debug_log(
|
|
2030
|
+
"[ACP-SUBagent] SUBAGENT_THINKING id=%s bytes=%d",
|
|
2031
|
+
event.subagent_id, len(event.subagent_thinking_text or ""),
|
|
2032
|
+
)
|
|
1745
2033
|
self.send_session_update({
|
|
1746
2034
|
"sessionUpdate": "subagent_thinking",
|
|
1747
2035
|
"subagentId": event.subagent_id,
|
|
1748
2036
|
"text": event.subagent_thinking_text,
|
|
1749
2037
|
})
|
|
1750
2038
|
elif event.type == StreamEventType.SUBAGENT_END:
|
|
2039
|
+
chunk_count = self._subagent_fwd_count.pop(event.subagent_id, None)
|
|
2040
|
+
self._subagent_fwd_bytes.pop(event.subagent_id, None)
|
|
2041
|
+
info_log(
|
|
2042
|
+
"[ACP-SUBagent] SUBAGENT_END id=%s type=%s status=%s "
|
|
2043
|
+
"err=%r chunk_count=%s",
|
|
2044
|
+
event.subagent_id, event.subagent_type,
|
|
2045
|
+
event.subagent_status, (event.subagent_error or "")[:100],
|
|
2046
|
+
chunk_count,
|
|
2047
|
+
)
|
|
2048
|
+
# If there's no active subagent for this id, the engine
|
|
2049
|
+
# rejected a nested Spawn (depth exceeded). Drop the
|
|
2050
|
+
# orphan end event so the TUI doesn't get a stale
|
|
2051
|
+
# subagent_end without a matching subagent_start.
|
|
2052
|
+
if chunk_count is None:
|
|
2053
|
+
debug_log(
|
|
2054
|
+
"[ACP-SUBagent] DROP orphan SUBAGENT_END id=%s "
|
|
2055
|
+
"(no matching start — likely nested Spawn rejection)",
|
|
2056
|
+
event.subagent_id,
|
|
2057
|
+
)
|
|
2058
|
+
continue
|
|
1751
2059
|
self.send_session_update({
|
|
1752
2060
|
"sessionUpdate": "subagent_end",
|
|
1753
2061
|
"subagentId": event.subagent_id,
|
|
@@ -1755,6 +2063,10 @@ class CDHACPAdapter:
|
|
|
1755
2063
|
"status": event.subagent_status,
|
|
1756
2064
|
"error": event.subagent_error,
|
|
1757
2065
|
})
|
|
2066
|
+
elif event.type == StreamEventType.SUBAGENT_TOOL_CALL:
|
|
2067
|
+
self._handle_subagent_tool_call(event)
|
|
2068
|
+
elif event.type == StreamEventType.SUBAGENT_TOOL_RESULT:
|
|
2069
|
+
self._handle_subagent_tool_result(event)
|
|
1758
2070
|
elif event.type == StreamEventType.PLAN:
|
|
1759
2071
|
self.send_session_update({
|
|
1760
2072
|
"sessionUpdate": "plan",
|
|
@@ -1947,6 +2259,8 @@ class CDHACPAdapter:
|
|
|
1947
2259
|
"content": result_str, "is_error": is_error}],
|
|
1948
2260
|
name=tid,
|
|
1949
2261
|
)
|
|
2262
|
+
# Sync plan after approval — the tool may have mutated todos
|
|
2263
|
+
self._emit_plan_update_to_tui()
|
|
1950
2264
|
verb = "Approved" if approved else "Rejected"
|
|
1951
2265
|
self.send_session_update({
|
|
1952
2266
|
"sessionUpdate": "agent_message_chunk",
|
|
@@ -1966,7 +2280,7 @@ class CDHACPAdapter:
|
|
|
1966
2280
|
self._thought_chunker.flush()
|
|
1967
2281
|
self.send_session_update({
|
|
1968
2282
|
"sessionUpdate": "agent_message_chunk",
|
|
1969
|
-
"content": {"type": "text", "text":
|
|
2283
|
+
"content": {"type": "text", "text": "Error: internal error"},
|
|
1970
2284
|
})
|
|
1971
2285
|
try:
|
|
1972
2286
|
self.agent.save_session()
|
|
@@ -1992,6 +2306,9 @@ class CDHACPAdapter:
|
|
|
1992
2306
|
except Exception:
|
|
1993
2307
|
debug_log("Failed to save tasks to .cdh at turn end", exc_info=True)
|
|
1994
2308
|
|
|
2309
|
+
# Sync plan to TUI so sidebar shows final todo state
|
|
2310
|
+
self._emit_plan_update_to_tui()
|
|
2311
|
+
|
|
1995
2312
|
# Send context usage stats to TUI sidebar
|
|
1996
2313
|
ctx = self.agent.context
|
|
1997
2314
|
used = ctx._token_count if ctx else 0
|
|
@@ -2080,6 +2397,7 @@ class CDHACPAdapter:
|
|
|
2080
2397
|
"""Cancel current session."""
|
|
2081
2398
|
if self.agent:
|
|
2082
2399
|
await self.agent.cancel()
|
|
2400
|
+
await self.agent.shutdown()
|
|
2083
2401
|
try:
|
|
2084
2402
|
self.agent.save_session()
|
|
2085
2403
|
except Exception:
|
|
@@ -2103,6 +2421,16 @@ class CDHACPAdapter:
|
|
|
2103
2421
|
self._send_available_commands()
|
|
2104
2422
|
return {"modeId": mode_id}
|
|
2105
2423
|
|
|
2424
|
+
async def session_clear_todos(self, session_id: str):
|
|
2425
|
+
"""Clear all todos in the current session."""
|
|
2426
|
+
if self.agent:
|
|
2427
|
+
tm = getattr(self.agent, "_todo_manager", None)
|
|
2428
|
+
if tm:
|
|
2429
|
+
tm.clear_todos()
|
|
2430
|
+
self.agent.save_todos_to_project()
|
|
2431
|
+
self._emit_plan_update_to_tui()
|
|
2432
|
+
return {"cleared": True}
|
|
2433
|
+
|
|
2106
2434
|
# ── Terminal RPC stubs ──────────────────────────────────────────
|
|
2107
2435
|
async def terminal_create(
|
|
2108
2436
|
self, session_id: str, terminal_id: str, command: str, args: list[str] | None = None,
|
|
@@ -2134,6 +2462,8 @@ class JSONRPCServer:
|
|
|
2134
2462
|
"session/cancel": self._handle_session_cancel,
|
|
2135
2463
|
"session/save": self._handle_session_save,
|
|
2136
2464
|
"session/set_mode": self._handle_session_set_mode,
|
|
2465
|
+
"session/clear_todos": self._handle_session_clear_todos,
|
|
2466
|
+
"session/load_earlier": self._handle_session_load_earlier,
|
|
2137
2467
|
"session/ask_user_answer": self._handle_ask_user_answer,
|
|
2138
2468
|
"terminal/create": self._handle_terminal_create,
|
|
2139
2469
|
"terminal/kill": self._handle_terminal_kill,
|
|
@@ -2208,6 +2538,18 @@ class JSONRPCServer:
|
|
|
2208
2538
|
params.get("modeId"),
|
|
2209
2539
|
)
|
|
2210
2540
|
|
|
2541
|
+
async def _handle_session_clear_todos(self, params: dict):
|
|
2542
|
+
return await self.adapter.session_clear_todos(
|
|
2543
|
+
params.get("sessionId"),
|
|
2544
|
+
)
|
|
2545
|
+
|
|
2546
|
+
async def _handle_session_load_earlier(self, params: dict):
|
|
2547
|
+
return await self.adapter.session_load_earlier(
|
|
2548
|
+
params.get("sessionId"),
|
|
2549
|
+
params.get("offset", 0),
|
|
2550
|
+
params.get("limit", 50),
|
|
2551
|
+
)
|
|
2552
|
+
|
|
2211
2553
|
async def _handle_terminal_create(self, params: dict):
|
|
2212
2554
|
return await self.adapter.terminal_create(
|
|
2213
2555
|
params.get("sessionId"),
|
|
@@ -2247,12 +2589,18 @@ class JSONRPCServer:
|
|
|
2247
2589
|
async def _main():
|
|
2248
2590
|
adapter = CDHACPAdapter()
|
|
2249
2591
|
server = JSONRPCServer(adapter)
|
|
2250
|
-
|
|
2592
|
+
|
|
2593
|
+
# Track in-flight prompt tasks by request id so that:
|
|
2594
|
+
# - session/cancel can target the correct task
|
|
2595
|
+
# - EOF can cancel & await all remaining tasks
|
|
2596
|
+
prompt_tasks: dict[str | int, asyncio.Task] = {}
|
|
2251
2597
|
|
|
2252
2598
|
async def _run_prompt(req: dict):
|
|
2253
2599
|
try:
|
|
2254
2600
|
result = await server._handle_session_prompt(req.get("params", {}))
|
|
2255
2601
|
return {"jsonrpc": "2.0", "result": result, "id": req.get("id")}
|
|
2602
|
+
except asyncio.CancelledError:
|
|
2603
|
+
raise
|
|
2256
2604
|
except Exception as e:
|
|
2257
2605
|
import traceback
|
|
2258
2606
|
tb = traceback.format_exc()
|
|
@@ -2264,6 +2612,13 @@ async def _main():
|
|
|
2264
2612
|
while True:
|
|
2265
2613
|
line = await asyncio.to_thread(sys.stdin.readline)
|
|
2266
2614
|
if not line:
|
|
2615
|
+
# EOF: cancel all in-flight prompts and exit
|
|
2616
|
+
for p_req_id, p_task in prompt_tasks.items():
|
|
2617
|
+
if not p_task.done():
|
|
2618
|
+
p_task.cancel()
|
|
2619
|
+
if prompt_tasks:
|
|
2620
|
+
await asyncio.gather(*prompt_tasks.values(), return_exceptions=True)
|
|
2621
|
+
prompt_tasks.clear()
|
|
2267
2622
|
break
|
|
2268
2623
|
|
|
2269
2624
|
line = line.strip()
|
|
@@ -2294,10 +2649,34 @@ async def _main():
|
|
|
2294
2649
|
if method == "session/prompt":
|
|
2295
2650
|
# Run prompt in background so main loop stays responsive
|
|
2296
2651
|
# to cancel notifications on stdin
|
|
2297
|
-
|
|
2298
|
-
|
|
2652
|
+
task = asyncio.create_task(_run_prompt(req))
|
|
2653
|
+
prompt_tasks[req_id] = task
|
|
2654
|
+
|
|
2655
|
+
def _on_prompt_done(t, req=req, rid=req_id):
|
|
2656
|
+
prompt_tasks.pop(rid, None)
|
|
2299
2657
|
try:
|
|
2300
2658
|
resp = t.result()
|
|
2659
|
+
except asyncio.CancelledError:
|
|
2660
|
+
# Prompt was cancelled (session/cancel). Send a
|
|
2661
|
+
# stopReason=cancelled response so the TUI's pending
|
|
2662
|
+
# session/prompt request resolves and the turn ends
|
|
2663
|
+
# cleanly — otherwise the TUI hangs forever waiting
|
|
2664
|
+
# for a response that never comes.
|
|
2665
|
+
info_log(
|
|
2666
|
+
"[ACP-CANCEL] _on_prompt_done CancelledError raised; "
|
|
2667
|
+
"req_id=%s subagent_fwd=%s",
|
|
2668
|
+
req.get("id"), dict(adapter._subagent_fwd_count),
|
|
2669
|
+
)
|
|
2670
|
+
if req.get("id") is not None:
|
|
2671
|
+
try:
|
|
2672
|
+
print(json.dumps({
|
|
2673
|
+
"jsonrpc": "2.0",
|
|
2674
|
+
"result": {"stopReason": "cancelled"},
|
|
2675
|
+
"id": req["id"],
|
|
2676
|
+
}), flush=True)
|
|
2677
|
+
except (TypeError, ValueError, OSError):
|
|
2678
|
+
pass
|
|
2679
|
+
return
|
|
2301
2680
|
except Exception as exc:
|
|
2302
2681
|
_dump_crash("_on_prompt_done")
|
|
2303
2682
|
debug_log(f"Prompt task raised unhandled exception: {exc}", exc_info=True)
|
|
@@ -2308,10 +2687,30 @@ async def _main():
|
|
|
2308
2687
|
except (TypeError, ValueError, OSError) as e:
|
|
2309
2688
|
_dump_crash("_on_prompt_done_send")
|
|
2310
2689
|
debug_log("_on_prompt_done send failed: %s", e)
|
|
2311
|
-
|
|
2690
|
+
task.add_done_callback(_on_prompt_done)
|
|
2312
2691
|
elif method == "session/cancel":
|
|
2313
|
-
|
|
2692
|
+
# session/cancel is a notification (no id), so req_id is None.
|
|
2693
|
+
# prompt_tasks is keyed by request id; find any in-flight task.
|
|
2694
|
+
if req_id is not None:
|
|
2695
|
+
target_task = prompt_tasks.get(req_id)
|
|
2696
|
+
else:
|
|
2697
|
+
target_task = next(
|
|
2698
|
+
(t for t in prompt_tasks.values() if not t.done()),
|
|
2699
|
+
None,
|
|
2700
|
+
)
|
|
2701
|
+
info_log(
|
|
2702
|
+
"[ACP-CANCEL] received session/cancel; "
|
|
2703
|
+
"req_id=%s task_exists=%s task_done=%s",
|
|
2704
|
+
req_id, "yes" if target_task else "no",
|
|
2705
|
+
target_task.done() if target_task else "n/a",
|
|
2706
|
+
)
|
|
2707
|
+
if target_task and not target_task.done():
|
|
2314
2708
|
adapter.cancel_prompt()
|
|
2709
|
+
info_log("[ACP-CANCEL] calling prompt_task.cancel() now")
|
|
2710
|
+
target_task.cancel()
|
|
2711
|
+
info_log("[ACP-CANCEL] prompt_task.cancel() returned")
|
|
2712
|
+
else:
|
|
2713
|
+
info_log("[ACP-CANCEL] no in-flight prompt_task to cancel")
|
|
2315
2714
|
# Notification (no id) — nothing to respond with
|
|
2316
2715
|
else:
|
|
2317
2716
|
response = await server.handle_request(req)
|
|
@@ -2324,6 +2723,20 @@ async def _main():
|
|
|
2324
2723
|
|
|
2325
2724
|
|
|
2326
2725
|
def main():
|
|
2726
|
+
# Initialise file logging ONCE at adapter entry. Previously the ACP
|
|
2727
|
+
# subprocess never called ``setup_logging`` (only the ``cli`` command
|
|
2728
|
+
# path did), so every ``logger.debug(...)`` / ``debug_log(...)`` call
|
|
2729
|
+
# in the engine/provider/ACP layers was silently dropped and
|
|
2730
|
+
# ``~/.onecode/logs/onecode.log`` stayed empty during live reproductions.
|
|
2731
|
+
# Honour the same ``CDH_LOG_LEVEL`` env var as the CLI so users can
|
|
2732
|
+
# turn on DEBUG diagnostics without code changes.
|
|
2733
|
+
import os
|
|
2734
|
+
from onecode.cli import setup_logging
|
|
2735
|
+
setup_logging(os.environ.get("CDH_LOG_LEVEL", "INFO"))
|
|
2736
|
+
info_log(
|
|
2737
|
+
"[ACP-INIT] cdh_acp adapter starting; log_level=%s pid=%d",
|
|
2738
|
+
os.environ.get("CDH_LOG_LEVEL", "INFO"), os.getpid(),
|
|
2739
|
+
)
|
|
2327
2740
|
asyncio.run(_main())
|
|
2328
2741
|
|
|
2329
2742
|
if __name__ == "__main__":
|