@scemoon/cdh 1.0.3 → 1.0.5

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 (294) hide show
  1. package/ai-dlc-skill/SKILL.md +4 -2
  2. package/ai-dlc-skill/agents/master.md +6 -0
  3. package/ai-dlc-skill/agents/plan-agent.md +2 -2
  4. package/ai-dlc-skill/agents/understand-agent.md +4 -4
  5. package/ai-dlc-skill/agents/verify-agent.md +1 -1
  6. package/ai-dlc-skill/architecture/project-structure.md +10 -10
  7. package/ai-dlc-skill/brownfield/README.md +2 -2
  8. package/ai-dlc-skill/brownfield/generate-context.sh +3 -3
  9. package/ai-dlc-skill/brownfield/scripts/extract-api.sh +5 -5
  10. package/ai-dlc-skill/components/backend.md +1 -1
  11. package/ai-dlc-skill/components/web.md +2 -2
  12. package/ai-dlc-skill/contracts/README.md +3 -3
  13. package/ai-dlc-skill/cross-tool/cline/export.sh +1 -1
  14. package/ai-dlc-skill/cross-tool/copilot/export.sh +1 -1
  15. package/ai-dlc-skill/cross-tool/onecode/export-skills.sh +7 -3
  16. package/ai-dlc-skill/cross-tool/onecode/export.sh +14 -8
  17. package/ai-dlc-skill/phases/deliver/lifecycle.md +6 -6
  18. package/ai-dlc-skill/phases/deliver/prompt.md +2 -2
  19. package/ai-dlc-skill/phases/deliver/rules.md +1 -1
  20. package/ai-dlc-skill/phases/plan/lifecycle.md +11 -11
  21. package/ai-dlc-skill/phases/plan/prompt.md +6 -6
  22. package/ai-dlc-skill/phases/plan/rules.md +1 -1
  23. package/ai-dlc-skill/phases/understand/entry.md +1 -1
  24. package/ai-dlc-skill/phases/understand/lifecycle.md +7 -7
  25. package/ai-dlc-skill/phases/understand/prompt.md +8 -8
  26. package/ai-dlc-skill/phases/understand/rules.md +5 -5
  27. package/ai-dlc-skill/phases/verify/lifecycle.md +24 -24
  28. package/ai-dlc-skill/phases/verify/prompt.md +8 -8
  29. package/ai-dlc-skill/phases/verify/rules.md +9 -9
  30. package/ai-dlc-skill/providers/README.md +2 -2
  31. package/ai-dlc-skill/providers/aliyun/preview.yaml +2 -2
  32. package/ai-dlc-skill/providers/tcb/preview.yaml +2 -2
  33. package/ai-dlc-skill/requirements.md +7 -7
  34. package/ai-dlc-skill/skill.yaml +4 -4
  35. package/ai-dlc-skill/templates/integration/CHANGELOG.md +2 -2
  36. package/ai-dlc-skill/templates/integration/contract-diff.md +7 -7
  37. package/ai-dlc-skill/templates/integration/contract-spec.md +7 -7
  38. package/ai-dlc-skill/templates/integration/runtime-contract.yaml +1 -1
  39. package/ai-dlc-skill/templates/project/README.md +18 -19
  40. package/ai-dlc-skill/templates/project/template.md +21 -21
  41. package/ai-dlc-skill/walkthrough/collect.sh +1 -1
  42. package/ai-dlc-skill/walkthrough/template.md +2 -2
  43. package/ai-dlc-skill/workflows/ai-dlc.yaml +12 -12
  44. package/ai-dlc-skill/workflows/deployment.yaml +4 -4
  45. package/cdh/__pycache__/cdh_mcp_injector.cpython-314.pyc +0 -0
  46. package/cdh/__pycache__/cdh_mcp_loader.cpython-314.pyc +0 -0
  47. package/cdh/__pycache__/cdh_mcp_manager.cpython-314.pyc +0 -0
  48. package/cdh/__pycache__/cdh_session_aggregator.cpython-314.pyc +0 -0
  49. package/cdh/__pycache__/cdh_skill_loader.cpython-314.pyc +0 -0
  50. package/cdh/__pycache__/cdh_skill_manager.cpython-314.pyc +0 -0
  51. package/cdh/__pycache__/cli.cpython-313.pyc +0 -0
  52. package/cdh/__pycache__/cli.cpython-314.pyc +0 -0
  53. package/cdh/__pycache__/scaffold.cpython-314.pyc +0 -0
  54. package/cdh/cdh_mcp_injector.py +115 -0
  55. package/cdh/cdh_mcp_loader.py +67 -0
  56. package/cdh/cdh_mcp_manager.py +73 -0
  57. package/cdh/cdh_session_aggregator.py +258 -0
  58. package/cdh/cdh_skill_manager.py +105 -0
  59. package/cdh/cli.py +14 -3
  60. package/cdh/scaffold.py +192 -41
  61. package/onecode/__init__.py +1 -1
  62. package/onecode/__pycache__/cli.cpython-314.pyc +0 -0
  63. package/onecode/__pycache__/commands.cpython-314.pyc +0 -0
  64. package/onecode/__pycache__/config.cpython-314.pyc +0 -0
  65. package/onecode/__pycache__/config_screen.cpython-314.pyc +0 -0
  66. package/onecode/__pycache__/migrate.cpython-314.pyc +0 -0
  67. package/onecode/agent/__pycache__/cdh_loader.cpython-314.pyc +0 -0
  68. package/onecode/agent/__pycache__/context.cpython-314.pyc +0 -0
  69. package/onecode/agent/__pycache__/engine.cpython-314.pyc +0 -0
  70. package/onecode/agent/__pycache__/hooks.cpython-314.pyc +0 -0
  71. package/onecode/agent/__pycache__/onecode_agent_acp.cpython-314.pyc +0 -0
  72. package/onecode/agent/__pycache__/permissions.cpython-314.pyc +0 -0
  73. package/onecode/agent/__pycache__/project_doc.cpython-314.pyc +0 -0
  74. package/onecode/agent/__pycache__/session.cpython-314.pyc +0 -0
  75. package/onecode/agent/agents/__pycache__/types.cpython-314.pyc +0 -0
  76. package/onecode/agent/agents/types.py +47 -164
  77. package/onecode/agent/attachments.py +0 -1
  78. package/onecode/agent/cdh_loader.py +17 -25
  79. package/onecode/agent/context.py +8 -3
  80. package/onecode/agent/engine.py +835 -220
  81. package/onecode/agent/hooks.py +1 -1
  82. package/onecode/agent/onecode_agent_acp.py +462 -49
  83. package/onecode/agent/onecode_agent_acp.py.bak +2447 -0
  84. package/onecode/agent/permissions.py +0 -1
  85. package/onecode/agent/project_doc.py +19 -0
  86. package/onecode/agent/session.py +17 -3
  87. package/onecode/agent/snapshot.py +3 -4
  88. package/onecode/agent/tools/__pycache__/agent_tools.cpython-314.pyc +0 -0
  89. package/onecode/agent/tools/__pycache__/apply_patch_tool.cpython-314.pyc +0 -0
  90. package/onecode/agent/tools/__pycache__/bash_tool.cpython-314.pyc +0 -0
  91. package/onecode/agent/tools/__pycache__/communication_tools.cpython-314.pyc +0 -0
  92. package/onecode/agent/tools/__pycache__/config_tool.cpython-314.pyc +0 -0
  93. package/onecode/agent/tools/__pycache__/cron_tools.cpython-314.pyc +0 -0
  94. package/onecode/agent/tools/__pycache__/file_ops.cpython-314.pyc +0 -0
  95. package/onecode/agent/tools/__pycache__/file_tools.cpython-314.pyc +0 -0
  96. package/onecode/agent/tools/__pycache__/git_tools.cpython-314.pyc +0 -0
  97. package/onecode/agent/tools/__pycache__/lsp_tools.cpython-314.pyc +0 -0
  98. package/onecode/agent/tools/__pycache__/mcp_tools.cpython-314.pyc +0 -0
  99. package/onecode/agent/tools/__pycache__/permission_handler.cpython-314.pyc +0 -0
  100. package/onecode/agent/tools/__pycache__/protocol.cpython-314.pyc +0 -0
  101. package/onecode/agent/tools/__pycache__/registry.cpython-314.pyc +0 -0
  102. package/onecode/agent/tools/__pycache__/sandbox.cpython-314.pyc +0 -0
  103. package/onecode/agent/tools/__pycache__/skill_tools.cpython-314.pyc +0 -0
  104. package/onecode/agent/tools/__pycache__/todo_tools.cpython-314.pyc +0 -0
  105. package/onecode/agent/tools/__pycache__/web_tools.cpython-314.pyc +0 -0
  106. package/onecode/agent/tools/agent_tools.py +4 -6
  107. package/onecode/agent/tools/apply_patch_tool.py +2 -8
  108. package/onecode/agent/tools/bash_tool.py +32 -12
  109. package/onecode/agent/tools/communication_tools.py +4 -5
  110. package/onecode/agent/tools/config_tool.py +3 -3
  111. package/onecode/agent/tools/cron_tools.py +4 -5
  112. package/onecode/agent/tools/file_ops.py +37 -3
  113. package/onecode/agent/tools/file_tools.py +10 -11
  114. package/onecode/agent/tools/git_tools.py +2 -2
  115. package/onecode/agent/tools/lsp_tools.py +2 -2
  116. package/onecode/agent/tools/mcp_tools.py +3 -5
  117. package/onecode/agent/tools/permission_handler.py +1 -1
  118. package/onecode/agent/tools/protocol.py +1 -1
  119. package/onecode/agent/tools/registry.py +69 -2
  120. package/onecode/agent/tools/sandbox.py +110 -16
  121. package/onecode/agent/tools/skill_tools.py +2 -2
  122. package/onecode/agent/tools/todo_tools.py +38 -19
  123. package/onecode/agent/tools/web_tools.py +6 -7
  124. package/onecode/builtin_skills/agent-browser/SKILL.md +212 -0
  125. package/onecode/builtin_skills/agent-browser/skill.yaml +8 -0
  126. package/onecode/builtin_skills/skill-creator/SKILL.md +106 -0
  127. package/onecode/builtin_skills/skill-creator/skill.yaml +8 -0
  128. package/onecode/cli.py +186 -6
  129. package/onecode/codebase/__pycache__/chunker.cpython-314.pyc +0 -0
  130. package/onecode/codebase/__pycache__/indexer.cpython-314.pyc +0 -0
  131. package/onecode/codebase/__pycache__/retriever.cpython-314.pyc +0 -0
  132. package/onecode/codebase/__pycache__/storage.cpython-314.pyc +0 -0
  133. package/onecode/codebase/chunker.py +0 -5
  134. package/onecode/codebase/indexer.py +14 -1
  135. package/onecode/codebase/retriever.py +13 -2
  136. package/onecode/codebase/storage.py +5 -4
  137. package/onecode/commands.py +81 -6
  138. package/onecode/config.py +41 -12
  139. package/onecode/config_screen.py +7 -3
  140. package/onecode/mcp/__pycache__/manager.cpython-314.pyc +0 -0
  141. package/onecode/mcp/manager.py +2 -3
  142. package/onecode/memory/__init__.py +7 -53
  143. package/onecode/memory/__pycache__/__init__.cpython-314.pyc +0 -0
  144. package/onecode/memory/__pycache__/backend.cpython-314.pyc +0 -0
  145. package/onecode/memory/__pycache__/offload.cpython-314.pyc +0 -0
  146. package/onecode/memory/__pycache__/pyramid.cpython-314.pyc +0 -0
  147. package/onecode/memory/__pycache__/recall.cpython-314.pyc +0 -0
  148. package/onecode/memory/backend.py +13 -39
  149. package/onecode/memory/pyramid.py +2 -71
  150. package/onecode/memory/recall.py +38 -3
  151. package/onecode/migrate.py +122 -0
  152. package/onecode/models/__pycache__/messages.cpython-314.pyc +0 -0
  153. package/onecode/models/__pycache__/provider.cpython-314.pyc +0 -0
  154. package/onecode/models/messages.py +58 -0
  155. package/onecode/models/provider.py +20 -2
  156. package/onecode/models/providers/__pycache__/anthropic_provider.cpython-314.pyc +0 -0
  157. package/onecode/models/providers/__pycache__/glm_provider.cpython-314.pyc +0 -0
  158. package/onecode/models/providers/__pycache__/minimax_provider.cpython-314.pyc +0 -0
  159. package/onecode/models/providers/__pycache__/minimaxi_provider.cpython-314.pyc +0 -0
  160. package/onecode/models/providers/__pycache__/ollama_provider.cpython-314.pyc +0 -0
  161. package/onecode/models/providers/anthropic_provider.py +59 -12
  162. package/onecode/models/providers/glm_provider.py +40 -0
  163. package/onecode/models/providers/minimax_provider.py +3 -0
  164. package/onecode/models/providers/minimaxi_provider.py +3 -0
  165. package/onecode/models/providers/ollama_provider.py +3 -0
  166. package/onecode/server/app.py +1 -1
  167. package/onecode/skills/__pycache__/argument_substitution.cpython-314.pyc +0 -0
  168. package/onecode/skills/__pycache__/bootstrap.cpython-314.pyc +0 -0
  169. package/onecode/skills/__pycache__/loader.cpython-314.pyc +0 -0
  170. package/onecode/skills/__pycache__/manager.cpython-314.pyc +0 -0
  171. package/onecode/skills/argument_substitution.py +0 -1
  172. package/onecode/skills/bootstrap.py +124 -0
  173. package/onecode/skills/loader.py +35 -53
  174. package/onecode/skills/manager.py +2 -2
  175. package/onecode/storage/activity.py +3 -4
  176. package/onecode/storage/session.py +3 -4
  177. package/onecode/tasks/manager.py +23 -10
  178. package/onecode/trace/tracer.py +2 -3
  179. package/package.json +1 -1
  180. package/pyproject.toml +5 -1
  181. package/tui/__pycache__/about.cpython-314.pyc +0 -0
  182. package/tui/__pycache__/agents.cpython-314.pyc +0 -0
  183. package/tui/__pycache__/app.cpython-313.pyc +0 -0
  184. package/tui/__pycache__/app.cpython-314.pyc +0 -0
  185. package/tui/__pycache__/danger.cpython-314.pyc +0 -0
  186. package/tui/__pycache__/messages.cpython-314.pyc +0 -0
  187. package/tui/__pycache__/path_complete.cpython-314.pyc +0 -0
  188. package/tui/__pycache__/paths.cpython-314.pyc +0 -0
  189. package/tui/__pycache__/session_tracker.cpython-314.pyc +0 -0
  190. package/tui/__pycache__/settings_schema.cpython-314.pyc +0 -0
  191. package/tui/acp/__pycache__/agent.cpython-314.pyc +0 -0
  192. package/tui/acp/__pycache__/api.cpython-314.pyc +0 -0
  193. package/tui/acp/__pycache__/messages.cpython-314.pyc +0 -0
  194. package/tui/acp/__pycache__/prompt.cpython-314.pyc +0 -0
  195. package/tui/acp/__pycache__/protocol.cpython-314.pyc +0 -0
  196. package/tui/acp/agent.py +125 -58
  197. package/tui/acp/api.py +12 -0
  198. package/tui/acp/messages.py +3 -1
  199. package/tui/acp/prompt.py +0 -57
  200. package/tui/acp/protocol.py +6 -0
  201. package/tui/agents.py +8 -7
  202. package/tui/app.py +11 -33
  203. package/tui/data/agents/ampcode.com.toml +1 -0
  204. package/tui/data/agents/augmentcode.com.toml +1 -0
  205. package/tui/data/agents/cdh.cloud-dev-harness.toml +3 -2
  206. package/tui/data/agents/crowai.dev.toml +1 -0
  207. package/tui/data/agents/deepagents.langchain.com +1 -0
  208. package/tui/data/agents/docker.com.toml +1 -0
  209. package/tui/data/agents/goose.ai.toml +1 -0
  210. package/tui/data/agents/inference.huggingface.co.toml +1 -1
  211. package/tui/data/agents/kiro.dev.toml +1 -0
  212. package/tui/data/agents/onecode.dev.toml +22 -0
  213. package/tui/data/agents/openclaw.ai.toml +1 -1
  214. package/tui/data/agents/openhands.dev.toml +1 -0
  215. package/tui/data/agents/stakpak.dev.toml +1 -0
  216. package/tui/data/agents/vibe.mistral.ai.toml +1 -0
  217. package/tui/data/agents/vtcode.dev.toml +1 -0
  218. package/tui/messages.py +2 -0
  219. package/tui/path_complete.py +1 -1
  220. package/tui/paths.py +16 -0
  221. package/tui/screens/__pycache__/component_picker.cpython-314.pyc +0 -0
  222. package/tui/screens/__pycache__/diff_screen.cpython-314.pyc +0 -0
  223. package/tui/screens/__pycache__/log.cpython-314.pyc +0 -0
  224. package/tui/screens/__pycache__/main.cpython-313.pyc +0 -0
  225. package/tui/screens/__pycache__/main.cpython-314.pyc +0 -0
  226. package/tui/screens/__pycache__/permissions.cpython-314.pyc +0 -0
  227. package/tui/screens/__pycache__/projects_app.cpython-314.pyc +0 -0
  228. package/tui/screens/__pycache__/projects_screen.cpython-314.pyc +0 -0
  229. package/tui/screens/__pycache__/sessions.cpython-314.pyc +0 -0
  230. package/tui/screens/__pycache__/store.cpython-314.pyc +0 -0
  231. package/tui/screens/__pycache__/tool_content_screen.cpython-314.pyc +0 -0
  232. package/tui/screens/component_picker.py +35 -15
  233. package/tui/screens/diff_screen.py +84 -0
  234. package/tui/screens/log.py +25 -25
  235. package/tui/screens/main.py +48 -26
  236. package/tui/screens/projects.tcss +5 -1
  237. package/tui/screens/projects_app.py +6 -6
  238. package/tui/screens/projects_screen.py +19 -39
  239. package/tui/screens/sessions.py +3 -5
  240. package/tui/screens/store.py +1 -1
  241. package/tui/session_tracker.py +3 -0
  242. package/tui/settings_schema.py +2 -2
  243. package/tui/visuals/__pycache__/columns.cpython-314.pyc +0 -0
  244. package/tui/visuals/columns.py +1 -2
  245. package/tui/widgets/__pycache__/agent_response.cpython-314.pyc +0 -0
  246. package/tui/widgets/__pycache__/agent_thought.cpython-314.pyc +0 -0
  247. package/tui/widgets/__pycache__/conversation.cpython-314.pyc +0 -0
  248. package/tui/widgets/__pycache__/diff_view.cpython-314.pyc +0 -0
  249. package/tui/widgets/__pycache__/load_more.cpython-314.pyc +0 -0
  250. package/tui/widgets/__pycache__/modified_files.cpython-314.pyc +0 -0
  251. package/tui/widgets/__pycache__/plan.cpython-314.pyc +0 -0
  252. package/tui/widgets/__pycache__/project_grid_select.cpython-314.pyc +0 -0
  253. package/tui/widgets/__pycache__/session_grid_select.cpython-314.pyc +0 -0
  254. package/tui/widgets/__pycache__/shell_result.cpython-314.pyc +0 -0
  255. package/tui/widgets/__pycache__/side_bar.cpython-314.pyc +0 -0
  256. package/tui/widgets/__pycache__/subagent.cpython-314.pyc +0 -0
  257. package/tui/widgets/__pycache__/subagent_screen.cpython-314.pyc +0 -0
  258. package/tui/widgets/__pycache__/terminal.cpython-314.pyc +0 -0
  259. package/tui/widgets/__pycache__/tool_call.cpython-314.pyc +0 -0
  260. package/tui/widgets/agent_response.py +0 -1
  261. package/tui/widgets/agent_thought.py +0 -1
  262. package/tui/widgets/conversation.py +157 -14
  263. package/tui/widgets/diff_view.py +10 -0
  264. package/tui/widgets/load_more.py +24 -0
  265. package/tui/widgets/modified_files.py +173 -105
  266. package/tui/widgets/plan.py +1 -1
  267. package/tui/widgets/project_grid_select.py +9 -2
  268. package/tui/widgets/session_grid_select.py +6 -0
  269. package/tui/widgets/side_bar.py +10 -0
  270. package/tui/widgets/subagent.py +92 -58
  271. package/tui/widgets/subagent_screen.py +638 -115
  272. package/tui/widgets/terminal.py +1 -2
  273. package/tui/widgets/terminal_tool.py +11 -2
  274. package/tui/widgets/tool_call.py +35 -2
  275. package/cdh/__pycache__/__init__.cpython-313.pyc +0 -0
  276. package/cdh/__pycache__/__main__.cpython-314.pyc +0 -0
  277. package/cdh/__pycache__/scaffold.cpython-313.pyc +0 -0
  278. package/onecode/__pycache__/mock_data.cpython-314.pyc +0 -0
  279. package/onecode/agent/__pycache__/cdh_agent_acp.cpython-313.pyc +0 -0
  280. package/onecode/agent/__pycache__/cdh_agent_acp.cpython-314.pyc +0 -0
  281. package/onecode/agent/__pycache__/harness_skill.cpython-314.pyc +0 -0
  282. package/onecode/agent/tools/__pycache__/task_tools.cpython-314.pyc +0 -0
  283. package/onecode/codebase/__pycache__/config.cpython-314.pyc +0 -0
  284. package/onecode/memory/offload.py +0 -114
  285. package/onecode/memory/symbolic.py +0 -164
  286. package/onecode/models/providers/__pycache__/mock_provider.cpython-314.pyc +0 -0
  287. package/onecode/tasks/__pycache__/__init__.cpython-314.pyc +0 -0
  288. package/onecode/tasks/__pycache__/manager.cpython-314.pyc +0 -0
  289. package/onecode/tasks/__pycache__/models.cpython-314.pyc +0 -0
  290. package/tui/__pycache__/cli.cpython-314.pyc +0 -0
  291. package/tui/screens/__pycache__/settings.cpython-314.pyc +0 -0
  292. package/tui/widgets/__pycache__/conversation.cpython-313.pyc +0 -0
  293. package/tui/widgets/__pycache__/terminal.cpython-313.pyc +0 -0
  294. package/tui/widgets/__pycache__/tool_call.cpython-313.pyc +0 -0
@@ -1,7 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from abc import ABC, abstractmethod
3
4
  from dataclasses import dataclass
4
- from typing import Any, Iterable, Mapping, Optional, Protocol
5
+ from typing import Any, Callable, Iterable, Mapping
5
6
 
6
7
  from onecode.agent.tools.errors import ToolInputError, ToolPermissionError
7
8
  from onecode.agent.tools.protocol import ToolCall, ToolResult
@@ -19,11 +20,19 @@ class ToolSpec:
19
20
  max_result_size_chars: int = 20_000
20
21
 
21
22
 
22
- class Tool(Protocol):
23
+ class Tool(ABC):
24
+ @abstractmethod
23
25
  def spec(self) -> ToolSpec: ...
24
26
 
27
+ @abstractmethod
25
28
  def run(self, tool_input: dict[str, Any]) -> ToolResult: ...
26
29
 
30
+ async def run_async(
31
+ self, tool_input: dict[str, Any],
32
+ cancel_check: Callable[[], bool] | None = None,
33
+ ) -> ToolResult:
34
+ return self.run(tool_input)
35
+
27
36
 
28
37
  class ToolRegistry:
29
38
  def __init__(
@@ -68,6 +77,64 @@ class ToolRegistry:
68
77
  })
69
78
  return schemas
70
79
 
80
+ async def dispatch_async(
81
+ self,
82
+ call: ToolCall,
83
+ cancel_check: Callable[[], bool] | None = None,
84
+ ) -> ToolResult:
85
+ tool = self.get(call.name)
86
+ if tool is None:
87
+ return ToolResult(
88
+ name=call.name,
89
+ output={"error": f"unknown tool: {call.name}"},
90
+ is_error=True,
91
+ tool_use_id=call.tool_use_id,
92
+ )
93
+ try:
94
+ spec = tool.spec()
95
+ validate_json_schema(call.input, spec.input_schema, tool_name=call.name)
96
+ result = await tool.run_async(call.input, cancel_check=cancel_check)
97
+ max_chars = spec.max_result_size_chars
98
+ if max_chars > 0 and isinstance(result.output, str) and len(result.output) > max_chars:
99
+ result = ToolResult(
100
+ name=result.name,
101
+ output=result.output[:max_chars]
102
+ + f"\n\n[Output truncated to {max_chars} characters]",
103
+ is_error=result.is_error,
104
+ tool_use_id=result.tool_use_id or call.tool_use_id,
105
+ content_type=result.content_type,
106
+ )
107
+ if result.tool_use_id is None:
108
+ return ToolResult(
109
+ name=result.name,
110
+ output=result.output,
111
+ is_error=result.is_error,
112
+ tool_use_id=call.tool_use_id,
113
+ content_type=result.content_type,
114
+ )
115
+ return result
116
+ except ToolInputError as e:
117
+ return ToolResult(
118
+ name=call.name,
119
+ output={"error": str(e)},
120
+ is_error=True,
121
+ tool_use_id=call.tool_use_id,
122
+ )
123
+ except ToolPermissionError as e:
124
+ return ToolResult(
125
+ name=call.name,
126
+ output={"error": str(e)},
127
+ is_error=True,
128
+ tool_use_id=call.tool_use_id,
129
+ )
130
+ except Exception as e:
131
+ return ToolResult(
132
+ name=call.name,
133
+ output={"error": str(e)},
134
+ is_error=True,
135
+ tool_use_id=call.tool_use_id,
136
+ )
137
+
71
138
  def dispatch(
72
139
  self,
73
140
  call: ToolCall,
@@ -1,12 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import asyncio
3
4
  import enum
4
5
  import os
5
6
  import resource
6
7
  import subprocess
7
8
  from dataclasses import dataclass, field
8
9
  from pathlib import Path
9
- from typing import Optional
10
+ from typing import Callable, Optional
10
11
 
11
12
 
12
13
  class SandboxMode(enum.Enum):
@@ -77,7 +78,6 @@ class Sandbox:
77
78
  bwrap_args.extend(["--uid", str(os.getuid())])
78
79
  bwrap_args.extend(["--gid", str(os.getgid())])
79
80
 
80
- env = " ".join(f"{k}={v}" for k, v in os.environ.items() if k in self.config.env_whitelist or k.startswith("LC_"))
81
81
  bwrap_args.extend(["--setenv", "HOME", "/workspace"])
82
82
  bwrap_args.extend(["--setenv", "PATH", "/usr/local/sbin:/usr/local/bin:/usr/bin:/bin"])
83
83
 
@@ -109,6 +109,24 @@ class Sandbox:
109
109
  return self._exec_docker(cmd, timeout)
110
110
  return self._exec_direct(cmd, timeout)
111
111
 
112
+ async def exec_async(
113
+ self, cmd: str, timeout: int = 60,
114
+ cancel_check: Callable[[], bool] | None = None,
115
+ ) -> dict:
116
+ """Async public entry point — supports cancellation mid-execution."""
117
+ mode = self.config.mode
118
+
119
+ if mode == SandboxMode.NONE:
120
+ return await self._exec_direct_async(cmd, timeout, cancel_check)
121
+ if mode == SandboxMode.BUBBLEWRAP:
122
+ if self._check_bwrap_available():
123
+ return await self._exec_bwrap_async(cmd, timeout, cancel_check)
124
+ return await self._exec_direct_async(cmd, timeout, cancel_check)
125
+ if mode == SandboxMode.DOCKER:
126
+ # Docker exec is async-adaptable via docker-py, but keep sync for now
127
+ return self._exec_docker(cmd, timeout)
128
+ return await self._exec_direct_async(cmd, timeout, cancel_check)
129
+
112
130
  def _exec_direct(self, cmd: str, timeout: int) -> dict:
113
131
  # Apply resource limits in the *child* process only — calling
114
132
  # ``setrlimit`` on the parent (this Python interpreter) shrinks
@@ -130,20 +148,6 @@ class Sandbox:
130
148
  )
131
149
  except (ValueError, OSError, resource.error):
132
150
  pass
133
- try:
134
- resource.setrlimit(
135
- resource.RLIMIT_NPROC, (limits.max_procs, limits.max_procs + 5)
136
- )
137
- except (ValueError, OSError, resource.error):
138
- pass
139
- try:
140
- resource.setrlimit(
141
- resource.RLIMIT_NOFILE,
142
- (limits.max_open_files, limits.max_open_files + 10),
143
- )
144
- except (ValueError, OSError, resource.error):
145
- pass
146
-
147
151
  try:
148
152
  result = subprocess.run(
149
153
  cmd,
@@ -166,6 +170,96 @@ class Sandbox:
166
170
  except Exception as e:
167
171
  return {"success": False, "error": str(e), "sandbox": "none"}
168
172
 
173
+ async def _exec_direct_async(
174
+ self, cmd: str, timeout: int,
175
+ cancel_check: Callable[[], bool] | None = None,
176
+ ) -> dict:
177
+ limits = self.config.resource_limits
178
+
179
+ def _preexec_apply_rlimits() -> None:
180
+ try:
181
+ resource.setrlimit(
182
+ resource.RLIMIT_CPU, (limits.cpu_time, limits.cpu_time + 5)
183
+ )
184
+ except (ValueError, OSError, resource.error):
185
+ pass
186
+ try:
187
+ mem_bytes = limits.memory_mb * 1024 * 1024
188
+ resource.setrlimit(
189
+ resource.RLIMIT_AS, (mem_bytes, mem_bytes)
190
+ )
191
+ except (ValueError, OSError, resource.error):
192
+ pass
193
+ process = await asyncio.create_subprocess_shell(
194
+ cmd,
195
+ stdout=asyncio.subprocess.PIPE,
196
+ stderr=asyncio.subprocess.PIPE,
197
+ cwd=str(self.config.workspace_root),
198
+ preexec_fn=_preexec_apply_rlimits,
199
+ )
200
+ try:
201
+ if cancel_check is not None and cancel_check():
202
+ process.terminate()
203
+ await process.wait()
204
+ return {"success": False, "error": "Cancelled", "sandbox": "none"}
205
+ stdout, stderr = await asyncio.wait_for(
206
+ process.communicate(), timeout=timeout,
207
+ )
208
+ stdout = stdout.decode("utf-8", errors="replace") if isinstance(stdout, bytes) else stdout
209
+ stderr = stderr.decode("utf-8", errors="replace") if isinstance(stderr, bytes) else stderr
210
+ return {
211
+ "success": process.returncode == 0,
212
+ "stdout": stdout,
213
+ "stderr": stderr,
214
+ "returncode": process.returncode,
215
+ "sandbox": "none",
216
+ }
217
+ except asyncio.TimeoutError:
218
+ process.terminate()
219
+ await process.wait()
220
+ return {"success": False, "error": "Command timed out", "sandbox": "none"}
221
+ except Exception as e:
222
+ process.terminate()
223
+ await process.wait()
224
+ return {"success": False, "error": str(e), "sandbox": "none"}
225
+
226
+ async def _exec_bwrap_async(
227
+ self, cmd: str, timeout: int,
228
+ cancel_check: Callable[[], bool] | None = None,
229
+ ) -> dict:
230
+ bwrap_cmd = self._build_bwrap_cmd(cmd)
231
+ process = await asyncio.create_subprocess_exec(
232
+ *bwrap_cmd,
233
+ stdout=asyncio.subprocess.PIPE,
234
+ stderr=asyncio.subprocess.PIPE,
235
+ cwd=str(self.config.workspace_root),
236
+ )
237
+ try:
238
+ if cancel_check is not None and cancel_check():
239
+ process.terminate()
240
+ await process.wait()
241
+ return {"success": False, "error": "Cancelled", "sandbox": "bwrap"}
242
+ stdout, stderr = await asyncio.wait_for(
243
+ process.communicate(), timeout=timeout,
244
+ )
245
+ stdout = stdout.decode("utf-8", errors="replace") if isinstance(stdout, bytes) else stdout
246
+ stderr = stderr.decode("utf-8", errors="replace") if isinstance(stderr, bytes) else stderr
247
+ return {
248
+ "success": process.returncode == 0,
249
+ "stdout": stdout,
250
+ "stderr": stderr,
251
+ "returncode": process.returncode,
252
+ "sandbox": "bwrap",
253
+ }
254
+ except asyncio.TimeoutError:
255
+ process.terminate()
256
+ await process.wait()
257
+ return {"success": False, "error": "Command timed out", "sandbox": "bwrap"}
258
+ except Exception as e:
259
+ process.terminate()
260
+ await process.wait()
261
+ return {"success": False, "error": str(e), "sandbox": "bwrap"}
262
+
169
263
  def _exec_bwrap(self, cmd: str, timeout: int) -> dict:
170
264
  bwrap_cmd = self._build_bwrap_cmd(cmd)
171
265
 
@@ -3,12 +3,12 @@ from __future__ import annotations
3
3
  from typing import Any
4
4
 
5
5
  from onecode.agent.tools.protocol import ToolResult
6
- from onecode.agent.tools.registry import ToolSpec
6
+ from onecode.agent.tools.registry import Tool, ToolSpec
7
7
  from onecode.skills.loader import SkillLoader
8
8
  from onecode.skills.argument_substitution import substitute_arguments
9
9
 
10
10
 
11
- class SkillTool:
11
+ class SkillTool(Tool):
12
12
  """Run a SKILL.md skill (Clawd-Code pattern)."""
13
13
 
14
14
  def __init__(self, loader: SkillLoader):
@@ -3,10 +3,10 @@ from __future__ import annotations
3
3
  from typing import Any
4
4
 
5
5
  from onecode.agent.tools.protocol import ToolResult
6
- from onecode.agent.tools.registry import ToolSpec
6
+ from onecode.agent.tools.registry import Tool, ToolSpec
7
7
 
8
8
 
9
- class TodoCreateTool:
9
+ class TodoCreateTool(Tool):
10
10
  def __init__(self, todo_manager):
11
11
  self._tm = todo_manager
12
12
 
@@ -14,10 +14,12 @@ class TodoCreateTool:
14
14
  return ToolSpec(
15
15
  name="TodoCreate",
16
16
  description=(
17
- "Create a todo (lightweight checklist item) for tracking progress "
18
- "in the sidebar. Use for SIMPLE / single-step work (1-3 tool calls). "
19
- "For complex multi-step work, prefer delegating via `Spawn` subagent "
20
- "and use TodoCreate only to surface the overall plan. Returns todo id."
17
+ "Create a todo (plan item) for tracking progress. ALL tasks — simple "
18
+ "or complex are created via TodoCreate, persisted to .cdh/todos.json, "
19
+ "and mirrored to the sidebar Plan widget. At execution time, route by "
20
+ "complexity: simple todos are executed directly; complex todos are "
21
+ "delegated via `Spawn` (mark with metadata={\"delegate_to\": \"general\"}). "
22
+ "Returns todo id."
21
23
  ),
22
24
  input_schema={
23
25
  "type": "object",
@@ -42,7 +44,7 @@ class TodoCreateTool:
42
44
 
43
45
 
44
46
 
45
- class TodoGetTool:
47
+ class TodoGetTool(Tool):
46
48
  def __init__(self, todo_manager):
47
49
  self._tm = todo_manager
48
50
 
@@ -69,7 +71,7 @@ class TodoGetTool:
69
71
 
70
72
 
71
73
 
72
- class TodoListTool:
74
+ class TodoListTool(Tool):
73
75
  def __init__(self, todo_manager):
74
76
  self._tm = todo_manager
75
77
 
@@ -95,7 +97,7 @@ class TodoListTool:
95
97
 
96
98
 
97
99
 
98
- class TodoUpdateTool:
100
+ class TodoUpdateTool(Tool):
99
101
  def __init__(self, todo_manager):
100
102
  self._tm = todo_manager
101
103
 
@@ -137,7 +139,7 @@ class TodoUpdateTool:
137
139
 
138
140
 
139
141
 
140
- class TodoOutputTool:
142
+ class TodoOutputTool(Tool):
141
143
  def __init__(self, todo_manager):
142
144
  self._tm = todo_manager
143
145
 
@@ -148,20 +150,20 @@ class TodoOutputTool:
148
150
  input_schema={
149
151
  "type": "object",
150
152
  "properties": {
151
- "task_id": {"type": "string", "description": "Todo id to get output for"},
153
+ "taskId": {"type": "string", "description": "Todo id to get output for"},
152
154
  },
153
- "required": ["task_id"],
155
+ "required": ["taskId"],
154
156
  },
155
157
  )
156
158
 
157
159
  def run(self, tool_input: dict[str, Any]) -> ToolResult:
158
- todo_id = tool_input.get("task_id", "")
160
+ todo_id = tool_input.get("taskId", "")
159
161
  result = self._tm.get_todo_output(todo_id)
160
162
  return ToolResult(name="TodoOutput", output=result)
161
163
 
162
164
 
163
165
 
164
- class TodoStopTool:
166
+ class TodoStopTool(Tool):
165
167
  def __init__(self, todo_manager):
166
168
  self._tm = todo_manager
167
169
 
@@ -172,17 +174,34 @@ class TodoStopTool:
172
174
  input_schema={
173
175
  "type": "object",
174
176
  "properties": {
175
- "task_id": {"type": "string", "description": "Todo id to stop"},
177
+ "taskId": {"type": "string", "description": "Todo id to stop"},
176
178
  },
177
- "required": ["task_id"],
179
+ "required": ["taskId"],
178
180
  },
179
181
  )
180
182
 
181
183
  def run(self, tool_input: dict[str, Any]) -> ToolResult:
182
- todo_id = tool_input.get("task_id", "")
184
+ todo_id = tool_input.get("taskId", "")
183
185
  result = self._tm.update_todo(todo_id, status="completed")
184
186
  if result is None:
185
187
  return ToolResult(name="TodoStop", output={"success": False, "stopped": False, "error": "Todo not found"}, is_error=True)
186
188
  if result.get("deleted"):
187
- return ToolResult(name="TodoStop", output={"success": True, "stopped": True, "task_id": todo_id})
188
- return ToolResult(name="TodoStop", output={"success": True, "stopped": True, "task_id": todo_id})
189
+ return ToolResult(name="TodoStop", output={"success": True, "stopped": True, "taskId": todo_id})
190
+ return ToolResult(name="TodoStop", output={"success": True, "stopped": True, "taskId": todo_id})
191
+
192
+
193
+ class TodoClearTool(Tool):
194
+ def __init__(self, todo_manager):
195
+ self._tm = todo_manager
196
+
197
+ def spec(self) -> ToolSpec:
198
+ return ToolSpec(
199
+ name="TodoClear",
200
+ description="Clear all todos. Resets the entire plan — all pending, in-progress, and completed todos are removed and the plan starts fresh.",
201
+ input_schema={"type": "object", "properties": {}, "required": []},
202
+ )
203
+
204
+ def run(self, tool_input: dict[str, Any]) -> ToolResult:
205
+ count = len(self._tm.list_todos())
206
+ self._tm.clear_todos()
207
+ return ToolResult(name="TodoClear", output={"cleared": count})
@@ -8,7 +8,7 @@ from typing import Any, Optional
8
8
  import httpx
9
9
 
10
10
  from onecode.agent.tools.protocol import ToolResult
11
- from onecode.agent.tools.registry import ToolSpec
11
+ from onecode.agent.tools.registry import Tool, ToolSpec
12
12
 
13
13
 
14
14
  @dataclass
@@ -68,7 +68,7 @@ class WebSearch:
68
68
 
69
69
  for url in search_urls:
70
70
  try:
71
- results = self._search_duckduckgo(url, num_results)
71
+ results = self._search_duckduckgo(url, num_results, query)
72
72
  if results:
73
73
  return results
74
74
  except Exception:
@@ -76,7 +76,7 @@ class WebSearch:
76
76
 
77
77
  return self._search_fallback(query, num_results)
78
78
 
79
- def _search_duckduckgo(self, url: str, num_results: int) -> str:
79
+ def _search_duckduckgo(self, url: str, num_results: int, query: str) -> str:
80
80
  with httpx.Client(timeout=self.timeout, follow_redirects=True) as client:
81
81
  resp = client.get(url)
82
82
  resp.raise_for_status()
@@ -84,9 +84,7 @@ class WebSearch:
84
84
  html_content = resp.text
85
85
  results = []
86
86
 
87
- result_pattern = re.compile(r'<a class="result__a" href="([^"]+)">([^<]+)</a>.*?<a class="result__snippet"[^>]*>([^<]+)</a>', re.DOTALL)
88
87
  snippets = re.findall(r'<a class="result__a" href="([^"]+)">([^<]+)</a>', html_content)
89
- snippet_pattern = re.compile(r'<a class="result__a" href="[^"]+">[^<]+</a>.*?(?:<p class="result__snippet">([^<]+)</p>)?', re.DOTALL)
90
88
 
91
89
  for i, (url, title) in enumerate(snippets[:num_results]):
92
90
  snippet_match = re.search(rf'<a class="result__a" href="{re.escape(url)}"[^>]*>[^<]+</a>.*?(?:<p class="result__snippet">([^<]+)</p>|(?:<a class="result__snippet"[^>]*>([^<]+)</a>))?', html_content, re.DOTALL)
@@ -104,6 +102,7 @@ class WebSearch:
104
102
  return f"No results found for '{query}'"
105
103
 
106
104
  def _search_fallback(self, query: str, num_results: int) -> str:
105
+ """Fallback search using a different method."""
107
106
  return f"Web search for '{query}' returned no results. Try using WebFetch to directly access a URL."
108
107
 
109
108
  def _quote(self, query: str) -> str:
@@ -121,7 +120,7 @@ def websearch(query: str, num_results: int = 5) -> str:
121
120
  return searcher.search(query, num_results)
122
121
 
123
122
 
124
- class WebFetchTool:
123
+ class WebFetchTool(Tool):
125
124
  def spec(self) -> ToolSpec:
126
125
  return ToolSpec(
127
126
  name="WebFetch",
@@ -146,7 +145,7 @@ class WebFetchTool:
146
145
 
147
146
 
148
147
 
149
- class WebSearchTool:
148
+ class WebSearchTool(Tool):
150
149
  def spec(self) -> ToolSpec:
151
150
  return ToolSpec(
152
151
  name="WebSearch",
@@ -0,0 +1,212 @@
1
+ ---
2
+ name: agent-browser
3
+ description: Browser automation via Playwright for web interaction and testing
4
+ enabled: true
5
+ triggers:
6
+ - browser
7
+ - playwrith
8
+ - screenshot
9
+ - web page
10
+ - navigate
11
+ - open url
12
+ - click element
13
+ - fill form
14
+ - scrape
15
+ - headless
16
+ allowed_tools:
17
+ - Bash
18
+ - Read
19
+ - Write
20
+ phases:
21
+ - verify
22
+ - deliver
23
+ ---
24
+
25
+ # Agent Browser
26
+
27
+ ## When to Use
28
+
29
+ - Navigating to a web page and interacting with it
30
+ - Taking screenshots of pages
31
+ - Filling and submitting forms
32
+ - Clicking buttons, links, and other elements
33
+ - Extracting dynamic content that requires JS execution
34
+ - End-to-end testing of web applications
35
+ - Scraping SPAs (Single Page Applications)
36
+
37
+ Use **WebFetch** instead for simple GET requests that don't need interaction.
38
+
39
+ ## Setup
40
+
41
+ ```bash
42
+ pip install playwright
43
+ playwright install chromium
44
+ ```
45
+
46
+ Check if already installed:
47
+
48
+ ```bash
49
+ python -c "import playwright; print(playwright.__file__)"
50
+ ```
51
+
52
+ ## Basic Usage
53
+
54
+ ### Navigate and Screenshot
55
+
56
+ ```python
57
+ from playwright.sync_api import sync_playwright
58
+
59
+ with sync_playwright() as p:
60
+ browser = p.chromium.launch(headless=True)
61
+ page = browser.new_page()
62
+ page.goto("https://example.com")
63
+ page.screenshot(path="screenshot.png", full_page=True)
64
+ browser.close()
65
+ ```
66
+
67
+ ### Fill Form and Submit
68
+
69
+ ```python
70
+ page.fill("input[name='username']", "myuser")
71
+ page.fill("input[name='password']", "mypass")
72
+ page.click("button[type='submit']")
73
+ page.wait_for_url("**/dashboard")
74
+ ```
75
+
76
+ ### Extract Content
77
+
78
+ ```python
79
+ # Get text content
80
+ title = page.text_content("h1")
81
+ items = page.query_selector_all(".item")
82
+ for item in items:
83
+ print(item.inner_text())
84
+
85
+ # Get HTML
86
+ html = page.content()
87
+
88
+ # Evaluate JavaScript
89
+ result = page.evaluate("() => document.title")
90
+ ```
91
+
92
+ ### Wait Strategies
93
+
94
+ ```python
95
+ page.wait_for_selector(".results") # element appears
96
+ page.wait_for_load_state("networkidle") # no network activity
97
+ page.wait_for_timeout(2000) # sleep 2s
98
+ page.wait_for_url("**/success") # URL pattern
99
+ ```
100
+
101
+ ## Selectors
102
+
103
+ ```python
104
+ # CSS
105
+ page.click(".btn-primary")
106
+ page.click("button[data-id='submit']")
107
+
108
+ # Text
109
+ page.click("text=Submit")
110
+ page.click("button:has-text('Continue')")
111
+
112
+ # XPath
113
+ page.click("xpath=//button[@name='login']")
114
+
115
+ # Role (recommended)
116
+ page.get_by_role("button", name="Submit").click()
117
+ page.get_by_label("Email").fill("user@example.com")
118
+ page.get_by_placeholder("Search...").fill("query")
119
+ ```
120
+
121
+ ## Common Patterns
122
+
123
+ ### Login Flow
124
+
125
+ ```python
126
+ page.goto("https://example.com/login")
127
+ page.fill("#email", email)
128
+ page.fill("#password", password)
129
+ page.click("button[type='submit']")
130
+ page.wait_for_url("**/dashboard")
131
+ print("Logined:", page.url)
132
+ page.screenshot(path="after_login.png")
133
+ ```
134
+
135
+ ### Paginated Scraping
136
+
137
+ ```python
138
+ results = []
139
+ while True:
140
+ items = page.query_selector_all(".result-item")
141
+ for item in items:
142
+ results.append(item.inner_text())
143
+ next_btn = page.query_selector(".pagination .next")
144
+ if not next_btn or next_btn.is_disabled():
145
+ break
146
+ next_btn.click()
147
+ page.wait_for_load_state("networkidle")
148
+ ```
149
+
150
+ ### Multi-page Navigation
151
+
152
+ ```python
153
+ pages = ["https://example.com/page1", "https://example.com/page2"]
154
+ for url in pages:
155
+ page.goto(url, wait_until="domcontentloaded")
156
+ page.screenshot(path=f"screenshot-{url.split('/')[-1]}.png")
157
+ ```
158
+
159
+ ## Executing via Bash
160
+
161
+ For quick one-off tasks, use `python -c`:
162
+
163
+ ```bash
164
+ python -c "
165
+ from playwright.sync_api import sync_playwright
166
+ with sync_playwright() as p:
167
+ browser = p.chromium.launch(headless=True)
168
+ page = browser.new_page()
169
+ page.goto('https://example.com')
170
+ print(page.title())
171
+ browser.close()
172
+ "
173
+ ```
174
+
175
+ Or save a script and execute:
176
+
177
+ ```bash
178
+ cat > /tmp/browser_script.py << 'EOF'
179
+ from playwright.sync_api import sync_playwright
180
+ with sync_playwright() as p:
181
+ browser = p.chromium.launch(headless=True)
182
+ page = browser.new_page()
183
+ ...
184
+ browser.close()
185
+ EOF
186
+ python /tmp/browser_script.py
187
+ ```
188
+
189
+ ## Error Handling
190
+
191
+ ```python
192
+ try:
193
+ page.goto(url, timeout=30000)
194
+ except Exception as e:
195
+ print(f"Navigation failed: {e}")
196
+ page.screenshot(path="error.png")
197
+
198
+ try:
199
+ page.click(".missing-button", timeout=5000)
200
+ except Exception:
201
+ print("Button not found, trying alternative...")
202
+ page.click(".fallback-button")
203
+ ```
204
+
205
+ ## Best Practices
206
+
207
+ - Always use `headless=True` unless user asks for visible browser
208
+ - Set reasonable timeouts (10-30s for navigation)
209
+ - Close browser after use to free resources
210
+ - Screenshot on errors for debugging
211
+ - Prefer `get_by_role/get_by_label` over fragile CSS selectors
212
+ - Wait for network idle before extracting content
@@ -0,0 +1,8 @@
1
+ name: agent-browser
2
+ description: >-
3
+ Browser automation skill via Playwright. Enables the agent to navigate pages,
4
+ take screenshots, click elements, fill forms, extract content, and evaluate
5
+ JavaScript in a headless or headed browser.
6
+ version: 1.0.0
7
+ enabled: true
8
+ entry: SKILL.md