gobby 0.2.5__py3-none-any.whl

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 (383) hide show
  1. gobby/__init__.py +3 -0
  2. gobby/adapters/__init__.py +30 -0
  3. gobby/adapters/base.py +93 -0
  4. gobby/adapters/claude_code.py +276 -0
  5. gobby/adapters/codex.py +1292 -0
  6. gobby/adapters/gemini.py +343 -0
  7. gobby/agents/__init__.py +37 -0
  8. gobby/agents/codex_session.py +120 -0
  9. gobby/agents/constants.py +112 -0
  10. gobby/agents/context.py +362 -0
  11. gobby/agents/definitions.py +133 -0
  12. gobby/agents/gemini_session.py +111 -0
  13. gobby/agents/registry.py +618 -0
  14. gobby/agents/runner.py +968 -0
  15. gobby/agents/session.py +259 -0
  16. gobby/agents/spawn.py +916 -0
  17. gobby/agents/spawners/__init__.py +77 -0
  18. gobby/agents/spawners/base.py +142 -0
  19. gobby/agents/spawners/cross_platform.py +266 -0
  20. gobby/agents/spawners/embedded.py +225 -0
  21. gobby/agents/spawners/headless.py +226 -0
  22. gobby/agents/spawners/linux.py +125 -0
  23. gobby/agents/spawners/macos.py +277 -0
  24. gobby/agents/spawners/windows.py +308 -0
  25. gobby/agents/tty_config.py +319 -0
  26. gobby/autonomous/__init__.py +32 -0
  27. gobby/autonomous/progress_tracker.py +447 -0
  28. gobby/autonomous/stop_registry.py +269 -0
  29. gobby/autonomous/stuck_detector.py +383 -0
  30. gobby/cli/__init__.py +67 -0
  31. gobby/cli/__main__.py +8 -0
  32. gobby/cli/agents.py +529 -0
  33. gobby/cli/artifacts.py +266 -0
  34. gobby/cli/daemon.py +329 -0
  35. gobby/cli/extensions.py +526 -0
  36. gobby/cli/github.py +263 -0
  37. gobby/cli/init.py +53 -0
  38. gobby/cli/install.py +614 -0
  39. gobby/cli/installers/__init__.py +37 -0
  40. gobby/cli/installers/antigravity.py +65 -0
  41. gobby/cli/installers/claude.py +363 -0
  42. gobby/cli/installers/codex.py +192 -0
  43. gobby/cli/installers/gemini.py +294 -0
  44. gobby/cli/installers/git_hooks.py +377 -0
  45. gobby/cli/installers/shared.py +737 -0
  46. gobby/cli/linear.py +250 -0
  47. gobby/cli/mcp.py +30 -0
  48. gobby/cli/mcp_proxy.py +698 -0
  49. gobby/cli/memory.py +304 -0
  50. gobby/cli/merge.py +384 -0
  51. gobby/cli/projects.py +79 -0
  52. gobby/cli/sessions.py +622 -0
  53. gobby/cli/tasks/__init__.py +30 -0
  54. gobby/cli/tasks/_utils.py +658 -0
  55. gobby/cli/tasks/ai.py +1025 -0
  56. gobby/cli/tasks/commits.py +169 -0
  57. gobby/cli/tasks/crud.py +685 -0
  58. gobby/cli/tasks/deps.py +135 -0
  59. gobby/cli/tasks/labels.py +63 -0
  60. gobby/cli/tasks/main.py +273 -0
  61. gobby/cli/tasks/search.py +178 -0
  62. gobby/cli/tui.py +34 -0
  63. gobby/cli/utils.py +513 -0
  64. gobby/cli/workflows.py +927 -0
  65. gobby/cli/worktrees.py +481 -0
  66. gobby/config/__init__.py +129 -0
  67. gobby/config/app.py +551 -0
  68. gobby/config/extensions.py +167 -0
  69. gobby/config/features.py +472 -0
  70. gobby/config/llm_providers.py +98 -0
  71. gobby/config/logging.py +66 -0
  72. gobby/config/mcp.py +346 -0
  73. gobby/config/persistence.py +247 -0
  74. gobby/config/servers.py +141 -0
  75. gobby/config/sessions.py +250 -0
  76. gobby/config/tasks.py +784 -0
  77. gobby/hooks/__init__.py +104 -0
  78. gobby/hooks/artifact_capture.py +213 -0
  79. gobby/hooks/broadcaster.py +243 -0
  80. gobby/hooks/event_handlers.py +723 -0
  81. gobby/hooks/events.py +218 -0
  82. gobby/hooks/git.py +169 -0
  83. gobby/hooks/health_monitor.py +171 -0
  84. gobby/hooks/hook_manager.py +856 -0
  85. gobby/hooks/hook_types.py +575 -0
  86. gobby/hooks/plugins.py +813 -0
  87. gobby/hooks/session_coordinator.py +396 -0
  88. gobby/hooks/verification_runner.py +268 -0
  89. gobby/hooks/webhooks.py +339 -0
  90. gobby/install/claude/commands/gobby/bug.md +51 -0
  91. gobby/install/claude/commands/gobby/chore.md +51 -0
  92. gobby/install/claude/commands/gobby/epic.md +52 -0
  93. gobby/install/claude/commands/gobby/eval.md +235 -0
  94. gobby/install/claude/commands/gobby/feat.md +49 -0
  95. gobby/install/claude/commands/gobby/nit.md +52 -0
  96. gobby/install/claude/commands/gobby/ref.md +52 -0
  97. gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
  98. gobby/install/claude/hooks/hook_dispatcher.py +364 -0
  99. gobby/install/claude/hooks/validate_settings.py +102 -0
  100. gobby/install/claude/hooks-template.json +118 -0
  101. gobby/install/codex/hooks/hook_dispatcher.py +153 -0
  102. gobby/install/codex/prompts/forget.md +7 -0
  103. gobby/install/codex/prompts/memories.md +7 -0
  104. gobby/install/codex/prompts/recall.md +7 -0
  105. gobby/install/codex/prompts/remember.md +13 -0
  106. gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
  107. gobby/install/gemini/hooks-template.json +138 -0
  108. gobby/install/shared/plugins/code_guardian.py +456 -0
  109. gobby/install/shared/plugins/example_notify.py +331 -0
  110. gobby/integrations/__init__.py +10 -0
  111. gobby/integrations/github.py +145 -0
  112. gobby/integrations/linear.py +145 -0
  113. gobby/llm/__init__.py +40 -0
  114. gobby/llm/base.py +120 -0
  115. gobby/llm/claude.py +578 -0
  116. gobby/llm/claude_executor.py +503 -0
  117. gobby/llm/codex.py +322 -0
  118. gobby/llm/codex_executor.py +513 -0
  119. gobby/llm/executor.py +316 -0
  120. gobby/llm/factory.py +34 -0
  121. gobby/llm/gemini.py +258 -0
  122. gobby/llm/gemini_executor.py +339 -0
  123. gobby/llm/litellm.py +287 -0
  124. gobby/llm/litellm_executor.py +303 -0
  125. gobby/llm/resolver.py +499 -0
  126. gobby/llm/service.py +236 -0
  127. gobby/mcp_proxy/__init__.py +29 -0
  128. gobby/mcp_proxy/actions.py +175 -0
  129. gobby/mcp_proxy/daemon_control.py +198 -0
  130. gobby/mcp_proxy/importer.py +436 -0
  131. gobby/mcp_proxy/lazy.py +325 -0
  132. gobby/mcp_proxy/manager.py +798 -0
  133. gobby/mcp_proxy/metrics.py +609 -0
  134. gobby/mcp_proxy/models.py +139 -0
  135. gobby/mcp_proxy/registries.py +215 -0
  136. gobby/mcp_proxy/schema_hash.py +381 -0
  137. gobby/mcp_proxy/semantic_search.py +706 -0
  138. gobby/mcp_proxy/server.py +549 -0
  139. gobby/mcp_proxy/services/__init__.py +0 -0
  140. gobby/mcp_proxy/services/fallback.py +306 -0
  141. gobby/mcp_proxy/services/recommendation.py +224 -0
  142. gobby/mcp_proxy/services/server_mgmt.py +214 -0
  143. gobby/mcp_proxy/services/system.py +72 -0
  144. gobby/mcp_proxy/services/tool_filter.py +231 -0
  145. gobby/mcp_proxy/services/tool_proxy.py +309 -0
  146. gobby/mcp_proxy/stdio.py +565 -0
  147. gobby/mcp_proxy/tools/__init__.py +27 -0
  148. gobby/mcp_proxy/tools/agents.py +1103 -0
  149. gobby/mcp_proxy/tools/artifacts.py +207 -0
  150. gobby/mcp_proxy/tools/hub.py +335 -0
  151. gobby/mcp_proxy/tools/internal.py +337 -0
  152. gobby/mcp_proxy/tools/memory.py +543 -0
  153. gobby/mcp_proxy/tools/merge.py +422 -0
  154. gobby/mcp_proxy/tools/metrics.py +283 -0
  155. gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
  156. gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
  157. gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
  158. gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
  159. gobby/mcp_proxy/tools/orchestration/review.py +736 -0
  160. gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
  161. gobby/mcp_proxy/tools/session_messages.py +1056 -0
  162. gobby/mcp_proxy/tools/task_dependencies.py +219 -0
  163. gobby/mcp_proxy/tools/task_expansion.py +591 -0
  164. gobby/mcp_proxy/tools/task_github.py +393 -0
  165. gobby/mcp_proxy/tools/task_linear.py +379 -0
  166. gobby/mcp_proxy/tools/task_orchestration.py +77 -0
  167. gobby/mcp_proxy/tools/task_readiness.py +522 -0
  168. gobby/mcp_proxy/tools/task_sync.py +351 -0
  169. gobby/mcp_proxy/tools/task_validation.py +843 -0
  170. gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
  171. gobby/mcp_proxy/tools/tasks/_context.py +112 -0
  172. gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
  173. gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
  174. gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
  175. gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
  176. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
  177. gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
  178. gobby/mcp_proxy/tools/tasks/_search.py +215 -0
  179. gobby/mcp_proxy/tools/tasks/_session.py +125 -0
  180. gobby/mcp_proxy/tools/workflows.py +973 -0
  181. gobby/mcp_proxy/tools/worktrees.py +1264 -0
  182. gobby/mcp_proxy/transports/__init__.py +0 -0
  183. gobby/mcp_proxy/transports/base.py +95 -0
  184. gobby/mcp_proxy/transports/factory.py +44 -0
  185. gobby/mcp_proxy/transports/http.py +139 -0
  186. gobby/mcp_proxy/transports/stdio.py +213 -0
  187. gobby/mcp_proxy/transports/websocket.py +136 -0
  188. gobby/memory/backends/__init__.py +116 -0
  189. gobby/memory/backends/mem0.py +408 -0
  190. gobby/memory/backends/memu.py +485 -0
  191. gobby/memory/backends/null.py +111 -0
  192. gobby/memory/backends/openmemory.py +537 -0
  193. gobby/memory/backends/sqlite.py +304 -0
  194. gobby/memory/context.py +87 -0
  195. gobby/memory/manager.py +1001 -0
  196. gobby/memory/protocol.py +451 -0
  197. gobby/memory/search/__init__.py +66 -0
  198. gobby/memory/search/text.py +127 -0
  199. gobby/memory/viz.py +258 -0
  200. gobby/prompts/__init__.py +13 -0
  201. gobby/prompts/defaults/expansion/system.md +119 -0
  202. gobby/prompts/defaults/expansion/user.md +48 -0
  203. gobby/prompts/defaults/external_validation/agent.md +72 -0
  204. gobby/prompts/defaults/external_validation/external.md +63 -0
  205. gobby/prompts/defaults/external_validation/spawn.md +83 -0
  206. gobby/prompts/defaults/external_validation/system.md +6 -0
  207. gobby/prompts/defaults/features/import_mcp.md +22 -0
  208. gobby/prompts/defaults/features/import_mcp_github.md +17 -0
  209. gobby/prompts/defaults/features/import_mcp_search.md +16 -0
  210. gobby/prompts/defaults/features/recommend_tools.md +32 -0
  211. gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
  212. gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
  213. gobby/prompts/defaults/features/server_description.md +20 -0
  214. gobby/prompts/defaults/features/server_description_system.md +6 -0
  215. gobby/prompts/defaults/features/task_description.md +31 -0
  216. gobby/prompts/defaults/features/task_description_system.md +6 -0
  217. gobby/prompts/defaults/features/tool_summary.md +17 -0
  218. gobby/prompts/defaults/features/tool_summary_system.md +6 -0
  219. gobby/prompts/defaults/research/step.md +58 -0
  220. gobby/prompts/defaults/validation/criteria.md +47 -0
  221. gobby/prompts/defaults/validation/validate.md +38 -0
  222. gobby/prompts/loader.py +346 -0
  223. gobby/prompts/models.py +113 -0
  224. gobby/py.typed +0 -0
  225. gobby/runner.py +488 -0
  226. gobby/search/__init__.py +23 -0
  227. gobby/search/protocol.py +104 -0
  228. gobby/search/tfidf.py +232 -0
  229. gobby/servers/__init__.py +7 -0
  230. gobby/servers/http.py +636 -0
  231. gobby/servers/models.py +31 -0
  232. gobby/servers/routes/__init__.py +23 -0
  233. gobby/servers/routes/admin.py +416 -0
  234. gobby/servers/routes/dependencies.py +118 -0
  235. gobby/servers/routes/mcp/__init__.py +24 -0
  236. gobby/servers/routes/mcp/hooks.py +135 -0
  237. gobby/servers/routes/mcp/plugins.py +121 -0
  238. gobby/servers/routes/mcp/tools.py +1337 -0
  239. gobby/servers/routes/mcp/webhooks.py +159 -0
  240. gobby/servers/routes/sessions.py +582 -0
  241. gobby/servers/websocket.py +766 -0
  242. gobby/sessions/__init__.py +13 -0
  243. gobby/sessions/analyzer.py +322 -0
  244. gobby/sessions/lifecycle.py +240 -0
  245. gobby/sessions/manager.py +563 -0
  246. gobby/sessions/processor.py +225 -0
  247. gobby/sessions/summary.py +532 -0
  248. gobby/sessions/transcripts/__init__.py +41 -0
  249. gobby/sessions/transcripts/base.py +125 -0
  250. gobby/sessions/transcripts/claude.py +386 -0
  251. gobby/sessions/transcripts/codex.py +143 -0
  252. gobby/sessions/transcripts/gemini.py +195 -0
  253. gobby/storage/__init__.py +21 -0
  254. gobby/storage/agents.py +409 -0
  255. gobby/storage/artifact_classifier.py +341 -0
  256. gobby/storage/artifacts.py +285 -0
  257. gobby/storage/compaction.py +67 -0
  258. gobby/storage/database.py +357 -0
  259. gobby/storage/inter_session_messages.py +194 -0
  260. gobby/storage/mcp.py +680 -0
  261. gobby/storage/memories.py +562 -0
  262. gobby/storage/merge_resolutions.py +550 -0
  263. gobby/storage/migrations.py +860 -0
  264. gobby/storage/migrations_legacy.py +1359 -0
  265. gobby/storage/projects.py +166 -0
  266. gobby/storage/session_messages.py +251 -0
  267. gobby/storage/session_tasks.py +97 -0
  268. gobby/storage/sessions.py +817 -0
  269. gobby/storage/task_dependencies.py +223 -0
  270. gobby/storage/tasks/__init__.py +42 -0
  271. gobby/storage/tasks/_aggregates.py +180 -0
  272. gobby/storage/tasks/_crud.py +449 -0
  273. gobby/storage/tasks/_id.py +104 -0
  274. gobby/storage/tasks/_lifecycle.py +311 -0
  275. gobby/storage/tasks/_manager.py +889 -0
  276. gobby/storage/tasks/_models.py +300 -0
  277. gobby/storage/tasks/_ordering.py +119 -0
  278. gobby/storage/tasks/_path_cache.py +110 -0
  279. gobby/storage/tasks/_queries.py +343 -0
  280. gobby/storage/tasks/_search.py +143 -0
  281. gobby/storage/workflow_audit.py +393 -0
  282. gobby/storage/worktrees.py +547 -0
  283. gobby/sync/__init__.py +29 -0
  284. gobby/sync/github.py +333 -0
  285. gobby/sync/linear.py +304 -0
  286. gobby/sync/memories.py +284 -0
  287. gobby/sync/tasks.py +641 -0
  288. gobby/tasks/__init__.py +8 -0
  289. gobby/tasks/build_verification.py +193 -0
  290. gobby/tasks/commits.py +633 -0
  291. gobby/tasks/context.py +747 -0
  292. gobby/tasks/criteria.py +342 -0
  293. gobby/tasks/enhanced_validator.py +226 -0
  294. gobby/tasks/escalation.py +263 -0
  295. gobby/tasks/expansion.py +626 -0
  296. gobby/tasks/external_validator.py +764 -0
  297. gobby/tasks/issue_extraction.py +171 -0
  298. gobby/tasks/prompts/expand.py +327 -0
  299. gobby/tasks/research.py +421 -0
  300. gobby/tasks/tdd.py +352 -0
  301. gobby/tasks/tree_builder.py +263 -0
  302. gobby/tasks/validation.py +712 -0
  303. gobby/tasks/validation_history.py +357 -0
  304. gobby/tasks/validation_models.py +89 -0
  305. gobby/tools/__init__.py +0 -0
  306. gobby/tools/summarizer.py +170 -0
  307. gobby/tui/__init__.py +5 -0
  308. gobby/tui/api_client.py +281 -0
  309. gobby/tui/app.py +327 -0
  310. gobby/tui/screens/__init__.py +25 -0
  311. gobby/tui/screens/agents.py +333 -0
  312. gobby/tui/screens/chat.py +450 -0
  313. gobby/tui/screens/dashboard.py +377 -0
  314. gobby/tui/screens/memory.py +305 -0
  315. gobby/tui/screens/metrics.py +231 -0
  316. gobby/tui/screens/orchestrator.py +904 -0
  317. gobby/tui/screens/sessions.py +412 -0
  318. gobby/tui/screens/tasks.py +442 -0
  319. gobby/tui/screens/workflows.py +289 -0
  320. gobby/tui/screens/worktrees.py +174 -0
  321. gobby/tui/widgets/__init__.py +21 -0
  322. gobby/tui/widgets/chat.py +210 -0
  323. gobby/tui/widgets/conductor.py +104 -0
  324. gobby/tui/widgets/menu.py +132 -0
  325. gobby/tui/widgets/message_panel.py +160 -0
  326. gobby/tui/widgets/review_gate.py +224 -0
  327. gobby/tui/widgets/task_tree.py +99 -0
  328. gobby/tui/widgets/token_budget.py +166 -0
  329. gobby/tui/ws_client.py +258 -0
  330. gobby/utils/__init__.py +3 -0
  331. gobby/utils/daemon_client.py +235 -0
  332. gobby/utils/git.py +222 -0
  333. gobby/utils/id.py +38 -0
  334. gobby/utils/json_helpers.py +161 -0
  335. gobby/utils/logging.py +376 -0
  336. gobby/utils/machine_id.py +135 -0
  337. gobby/utils/metrics.py +589 -0
  338. gobby/utils/project_context.py +182 -0
  339. gobby/utils/project_init.py +263 -0
  340. gobby/utils/status.py +256 -0
  341. gobby/utils/validation.py +80 -0
  342. gobby/utils/version.py +23 -0
  343. gobby/workflows/__init__.py +4 -0
  344. gobby/workflows/actions.py +1310 -0
  345. gobby/workflows/approval_flow.py +138 -0
  346. gobby/workflows/artifact_actions.py +103 -0
  347. gobby/workflows/audit_helpers.py +110 -0
  348. gobby/workflows/autonomous_actions.py +286 -0
  349. gobby/workflows/context_actions.py +394 -0
  350. gobby/workflows/definitions.py +130 -0
  351. gobby/workflows/detection_helpers.py +208 -0
  352. gobby/workflows/engine.py +485 -0
  353. gobby/workflows/evaluator.py +669 -0
  354. gobby/workflows/git_utils.py +96 -0
  355. gobby/workflows/hooks.py +169 -0
  356. gobby/workflows/lifecycle_evaluator.py +613 -0
  357. gobby/workflows/llm_actions.py +70 -0
  358. gobby/workflows/loader.py +333 -0
  359. gobby/workflows/mcp_actions.py +60 -0
  360. gobby/workflows/memory_actions.py +272 -0
  361. gobby/workflows/premature_stop.py +164 -0
  362. gobby/workflows/session_actions.py +139 -0
  363. gobby/workflows/state_actions.py +123 -0
  364. gobby/workflows/state_manager.py +104 -0
  365. gobby/workflows/stop_signal_actions.py +163 -0
  366. gobby/workflows/summary_actions.py +344 -0
  367. gobby/workflows/task_actions.py +249 -0
  368. gobby/workflows/task_enforcement_actions.py +901 -0
  369. gobby/workflows/templates.py +52 -0
  370. gobby/workflows/todo_actions.py +84 -0
  371. gobby/workflows/webhook.py +223 -0
  372. gobby/workflows/webhook_executor.py +399 -0
  373. gobby/worktrees/__init__.py +5 -0
  374. gobby/worktrees/git.py +690 -0
  375. gobby/worktrees/merge/__init__.py +20 -0
  376. gobby/worktrees/merge/conflict_parser.py +177 -0
  377. gobby/worktrees/merge/resolver.py +485 -0
  378. gobby-0.2.5.dist-info/METADATA +351 -0
  379. gobby-0.2.5.dist-info/RECORD +383 -0
  380. gobby-0.2.5.dist-info/WHEEL +5 -0
  381. gobby-0.2.5.dist-info/entry_points.txt +2 -0
  382. gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
  383. gobby-0.2.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,503 @@
1
+ """
2
+ Claude implementation of AgentExecutor.
3
+
4
+ Supports multiple auth modes:
5
+ - api_key: Direct Anthropic API with API key
6
+ - subscription: Claude Agent SDK with CLI (Pro/Team subscriptions)
7
+ """
8
+
9
+ import asyncio
10
+ import concurrent.futures
11
+ import json
12
+ import logging
13
+ import os
14
+ import shutil
15
+ from collections.abc import Callable
16
+ from typing import Any, Literal
17
+
18
+ import anthropic
19
+
20
+ from gobby.llm.executor import (
21
+ AgentExecutor,
22
+ AgentResult,
23
+ ToolCallRecord,
24
+ ToolHandler,
25
+ ToolResult,
26
+ ToolSchema,
27
+ )
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ # Auth mode type
32
+ ClaudeAuthMode = Literal["api_key", "subscription"]
33
+
34
+
35
+ class ClaudeExecutor(AgentExecutor):
36
+ """
37
+ Claude implementation of AgentExecutor.
38
+
39
+ Supports two authentication modes:
40
+ - api_key: Uses the Anthropic API directly with an API key
41
+ - subscription: Uses Claude Agent SDK with CLI for Pro/Team subscriptions
42
+
43
+ The executor implements a proper agentic loop:
44
+ 1. Send prompt to Claude with tool schemas
45
+ 2. When Claude requests a tool, call tool_handler
46
+ 3. Send tool result back to Claude
47
+ 4. Repeat until Claude stops requesting tools or limits are reached
48
+
49
+ Example:
50
+ >>> executor = ClaudeExecutor(auth_mode="api_key", api_key="sk-ant-...")
51
+ >>> result = await executor.run(
52
+ ... prompt="Create a task",
53
+ ... tools=[ToolSchema(name="create_task", ...)],
54
+ ... tool_handler=my_handler,
55
+ ... )
56
+ """
57
+
58
+ _client: anthropic.AsyncAnthropic | None
59
+ _cli_path: str
60
+
61
+ def __init__(
62
+ self,
63
+ auth_mode: ClaudeAuthMode = "api_key",
64
+ api_key: str | None = None,
65
+ default_model: str = "claude-sonnet-4-20250514",
66
+ ):
67
+ """
68
+ Initialize ClaudeExecutor.
69
+
70
+ Args:
71
+ auth_mode: Authentication mode ("api_key" or "subscription").
72
+ api_key: Anthropic API key (required for api_key mode).
73
+ default_model: Default model to use if not specified in run().
74
+ """
75
+ self.auth_mode = auth_mode
76
+ self.default_model = default_model
77
+ self.logger = logger
78
+ self._client = None
79
+ self._cli_path = ""
80
+
81
+ if auth_mode == "api_key":
82
+ # Use provided key or fall back to environment variable
83
+ key = api_key or os.environ.get("ANTHROPIC_API_KEY")
84
+ if not key:
85
+ raise ValueError(
86
+ "API key required for api_key mode. "
87
+ "Provide api_key parameter or set ANTHROPIC_API_KEY env var."
88
+ )
89
+ self._client = anthropic.AsyncAnthropic(api_key=key)
90
+ elif auth_mode == "subscription":
91
+ # Verify Claude CLI is available for subscription mode
92
+ cli_path = shutil.which("claude")
93
+ if not cli_path:
94
+ raise ValueError(
95
+ "Claude CLI not found in PATH. Install Claude Code for subscription mode."
96
+ )
97
+ self._cli_path = cli_path
98
+ else:
99
+ raise ValueError(f"Unknown auth_mode: {auth_mode}")
100
+
101
+ @property
102
+ def provider_name(self) -> str:
103
+ """Return the provider name."""
104
+ return "claude"
105
+
106
+ def _convert_tools_to_anthropic_format(
107
+ self, tools: list[ToolSchema]
108
+ ) -> list[anthropic.types.ToolParam]:
109
+ """Convert ToolSchema list to Anthropic API format."""
110
+ anthropic_tools: list[anthropic.types.ToolParam] = []
111
+ for tool in tools:
112
+ # input_schema must have "type": "object" at minimum
113
+ input_schema: dict[str, Any] = {"type": "object", **tool.input_schema}
114
+ anthropic_tools.append(
115
+ {
116
+ "name": tool.name,
117
+ "description": tool.description,
118
+ "input_schema": input_schema,
119
+ }
120
+ )
121
+ return anthropic_tools
122
+
123
+ async def run(
124
+ self,
125
+ prompt: str,
126
+ tools: list[ToolSchema],
127
+ tool_handler: ToolHandler,
128
+ system_prompt: str | None = None,
129
+ model: str | None = None,
130
+ max_turns: int = 10,
131
+ timeout: float = 120.0,
132
+ ) -> AgentResult:
133
+ """
134
+ Execute an agentic loop with tool calling.
135
+
136
+ Runs Claude with the given prompt, calling tools via tool_handler
137
+ until completion, max_turns, or timeout.
138
+
139
+ Args:
140
+ prompt: The user prompt to process.
141
+ tools: List of available tools with their schemas.
142
+ tool_handler: Callback to execute tool calls.
143
+ system_prompt: Optional system prompt.
144
+ model: Optional model override.
145
+ max_turns: Maximum turns before stopping (default: 10).
146
+ timeout: Maximum execution time in seconds (default: 120.0).
147
+
148
+ Returns:
149
+ AgentResult with output, status, and tool call records.
150
+ """
151
+ if self.auth_mode == "api_key":
152
+ return await self._run_with_api(
153
+ prompt=prompt,
154
+ tools=tools,
155
+ tool_handler=tool_handler,
156
+ system_prompt=system_prompt,
157
+ model=model or self.default_model,
158
+ max_turns=max_turns,
159
+ timeout=timeout,
160
+ )
161
+ else:
162
+ return await self._run_with_sdk(
163
+ prompt=prompt,
164
+ tools=tools,
165
+ tool_handler=tool_handler,
166
+ system_prompt=system_prompt,
167
+ model=model or self.default_model,
168
+ max_turns=max_turns,
169
+ timeout=timeout,
170
+ )
171
+
172
+ async def _run_with_api(
173
+ self,
174
+ prompt: str,
175
+ tools: list[ToolSchema],
176
+ tool_handler: ToolHandler,
177
+ system_prompt: str | None,
178
+ model: str,
179
+ max_turns: int,
180
+ timeout: float,
181
+ ) -> AgentResult:
182
+ """Run using direct Anthropic API."""
183
+ if self._client is None:
184
+ return AgentResult(
185
+ output="",
186
+ status="error",
187
+ error="Anthropic client not initialized",
188
+ turns_used=0,
189
+ )
190
+
191
+ tool_calls: list[ToolCallRecord] = []
192
+ anthropic_tools = self._convert_tools_to_anthropic_format(tools)
193
+
194
+ # Build initial messages
195
+ messages: list[anthropic.types.MessageParam] = [{"role": "user", "content": prompt}]
196
+
197
+ # Track turns in outer scope so timeout handler can access the count
198
+ turns_counter = [0]
199
+
200
+ async def _run_loop() -> AgentResult:
201
+ nonlocal messages
202
+ turns_used = 0
203
+ final_output = ""
204
+ client = self._client
205
+ if client is None:
206
+ raise RuntimeError("ClaudeExecutor client not initialized")
207
+
208
+ while turns_used < max_turns:
209
+ turns_used += 1
210
+ turns_counter[0] = turns_used
211
+
212
+ # Call Claude
213
+ try:
214
+ response = await client.messages.create(
215
+ model=model,
216
+ max_tokens=8192,
217
+ system=system_prompt or "You are a helpful assistant.",
218
+ messages=messages,
219
+ tools=anthropic_tools if anthropic_tools else [],
220
+ )
221
+ except anthropic.APIError as e:
222
+ return AgentResult(
223
+ output="",
224
+ status="error",
225
+ tool_calls=tool_calls,
226
+ error=f"Anthropic API error: {e}",
227
+ turns_used=turns_used,
228
+ )
229
+
230
+ # Process response
231
+ assistant_content: list[anthropic.types.ContentBlockParam] = []
232
+ tool_use_blocks: list[dict[str, Any]] = []
233
+
234
+ for block in response.content:
235
+ if block.type == "text":
236
+ final_output = block.text
237
+ assistant_content.append({"type": "text", "text": block.text})
238
+ elif block.type == "tool_use":
239
+ tool_use_blocks.append(
240
+ {
241
+ "id": block.id,
242
+ "name": block.name,
243
+ "input": block.input,
244
+ }
245
+ )
246
+ assistant_content.append(
247
+ {
248
+ "type": "tool_use",
249
+ "id": block.id,
250
+ "name": block.name,
251
+ "input": dict(block.input) if block.input else {},
252
+ }
253
+ )
254
+
255
+ # Add assistant message to history
256
+ messages.append({"role": "assistant", "content": assistant_content})
257
+
258
+ # If no tool use, we're done
259
+ if not tool_use_blocks:
260
+ return AgentResult(
261
+ output=final_output,
262
+ status="success",
263
+ tool_calls=tool_calls,
264
+ turns_used=turns_used,
265
+ )
266
+
267
+ # Handle tool calls
268
+ tool_results: list[anthropic.types.ToolResultBlockParam] = []
269
+
270
+ for tool_use in tool_use_blocks:
271
+ tool_name = tool_use["name"]
272
+ arguments = tool_use["input"] if isinstance(tool_use["input"], dict) else {}
273
+
274
+ # Record the tool call
275
+ record = ToolCallRecord(
276
+ tool_name=tool_name,
277
+ arguments=arguments,
278
+ )
279
+ tool_calls.append(record)
280
+
281
+ # Execute via handler
282
+ try:
283
+ result = await tool_handler(tool_name, arguments)
284
+ record.result = result
285
+
286
+ # Format result for Claude
287
+ if result.success:
288
+ content = json.dumps(result.result) if result.result else "Success"
289
+ else:
290
+ content = f"Error: {result.error}"
291
+
292
+ tool_results.append(
293
+ {
294
+ "type": "tool_result",
295
+ "tool_use_id": tool_use["id"],
296
+ "content": content,
297
+ }
298
+ )
299
+ except Exception as e:
300
+ self.logger.error(f"Tool handler error for {tool_name}: {e}")
301
+ record.result = ToolResult(
302
+ tool_name=tool_name,
303
+ success=False,
304
+ error=str(e),
305
+ )
306
+ tool_results.append(
307
+ {
308
+ "type": "tool_result",
309
+ "tool_use_id": tool_use["id"],
310
+ "content": f"Error: {e}",
311
+ "is_error": True,
312
+ }
313
+ )
314
+
315
+ # Add tool results to messages
316
+ messages.append({"role": "user", "content": tool_results})
317
+
318
+ # Check stop reason
319
+ if response.stop_reason == "end_turn":
320
+ return AgentResult(
321
+ output=final_output,
322
+ status="success",
323
+ tool_calls=tool_calls,
324
+ turns_used=turns_used,
325
+ )
326
+
327
+ # Max turns reached
328
+ return AgentResult(
329
+ output=final_output,
330
+ status="partial",
331
+ tool_calls=tool_calls,
332
+ turns_used=turns_used,
333
+ )
334
+
335
+ # Run with timeout
336
+ try:
337
+ return await asyncio.wait_for(_run_loop(), timeout=timeout)
338
+ except TimeoutError:
339
+ return AgentResult(
340
+ output="",
341
+ status="timeout",
342
+ tool_calls=tool_calls,
343
+ error=f"Execution timed out after {timeout}s",
344
+ turns_used=turns_counter[0],
345
+ )
346
+
347
+ async def _run_with_sdk(
348
+ self,
349
+ prompt: str,
350
+ tools: list[ToolSchema],
351
+ tool_handler: ToolHandler,
352
+ system_prompt: str | None,
353
+ model: str,
354
+ max_turns: int,
355
+ timeout: float,
356
+ ) -> AgentResult:
357
+ """
358
+ Run using Claude Agent SDK with subscription auth.
359
+
360
+ This mode uses the claude-agent-sdk which handles subscription
361
+ authentication through the Claude CLI.
362
+ """
363
+ from claude_agent_sdk import (
364
+ AssistantMessage,
365
+ ClaudeAgentOptions,
366
+ ResultMessage,
367
+ TextBlock,
368
+ ToolResultBlock,
369
+ ToolUseBlock,
370
+ UserMessage,
371
+ create_sdk_mcp_server,
372
+ query,
373
+ )
374
+
375
+ tool_calls: list[ToolCallRecord] = []
376
+
377
+ # Create in-process tool functions that call our handler
378
+ # The SDK expects sync functions, so we'll use a wrapper
379
+ def make_tool_func(tool_schema: ToolSchema) -> Callable[..., str]:
380
+ """Create a tool function that calls our async handler."""
381
+
382
+ def tool_func(**kwargs: Any) -> str:
383
+ # Run the async handler - need to handle already-running loop
384
+ try:
385
+ loop = asyncio.get_running_loop()
386
+ except RuntimeError:
387
+ loop = None
388
+
389
+ if loop is not None:
390
+ # We're in an async context, use run_coroutine_threadsafe
391
+ coro = tool_handler(tool_schema.name, kwargs)
392
+ future: concurrent.futures.Future[ToolResult] = (
393
+ asyncio.run_coroutine_threadsafe(coro, loop) # type: ignore[arg-type]
394
+ )
395
+ try:
396
+ result = future.result(timeout=30)
397
+ except concurrent.futures.TimeoutError:
398
+ return json.dumps({"error": "Tool execution timed out"})
399
+ except Exception as e:
400
+ return json.dumps({"error": str(e)})
401
+ else:
402
+ # No running loop, use asyncio.run
403
+ coro = tool_handler(tool_schema.name, kwargs)
404
+ result = asyncio.run(coro) # type: ignore[arg-type]
405
+
406
+ # Record the call
407
+ record = ToolCallRecord(
408
+ tool_name=tool_schema.name,
409
+ arguments=kwargs,
410
+ result=result,
411
+ )
412
+ tool_calls.append(record)
413
+
414
+ if result.success:
415
+ return json.dumps(result.result) if result.result else "Success"
416
+ else:
417
+ return json.dumps({"error": result.error})
418
+
419
+ # Set function metadata for the SDK
420
+ tool_func.__name__ = tool_schema.name
421
+ tool_func.__doc__ = tool_schema.description
422
+ return tool_func
423
+
424
+ # Build tool functions
425
+ tool_functions = [make_tool_func(t) for t in tools]
426
+
427
+ # Create MCP server config with our tools
428
+ mcp_server = create_sdk_mcp_server(
429
+ name="gobby-executor",
430
+ tools=tool_functions, # type: ignore[arg-type]
431
+ )
432
+ mcp_servers: dict[str, Any] = {"gobby-executor": mcp_server}
433
+
434
+ # Build allowed tools list
435
+ allowed_tools = [f"mcp__gobby-executor__{t.name}" for t in tools]
436
+
437
+ # Configure SDK options
438
+ options = ClaudeAgentOptions(
439
+ system_prompt=system_prompt or "You are a helpful assistant.",
440
+ max_turns=max_turns,
441
+ model=model,
442
+ allowed_tools=allowed_tools,
443
+ permission_mode="bypassPermissions",
444
+ cli_path=self._cli_path,
445
+ mcp_servers=mcp_servers,
446
+ )
447
+
448
+ # Track turns in outer scope so timeout handler can access the count
449
+ turns_counter = [0]
450
+
451
+ async def _run_query() -> AgentResult:
452
+ result_text = ""
453
+ turns_used = 0
454
+
455
+ try:
456
+ async for message in query(prompt=prompt, options=options):
457
+ if isinstance(message, ResultMessage):
458
+ if message.result:
459
+ result_text = message.result
460
+ elif isinstance(message, AssistantMessage):
461
+ turns_used += 1
462
+ turns_counter[0] = turns_used
463
+ for block in message.content:
464
+ if isinstance(block, TextBlock):
465
+ result_text = block.text
466
+ elif isinstance(block, ToolUseBlock):
467
+ self.logger.debug(
468
+ f"ToolUseBlock: {block.name}, input={block.input}"
469
+ )
470
+ elif isinstance(message, UserMessage):
471
+ if isinstance(message.content, list):
472
+ for block in message.content:
473
+ if isinstance(block, ToolResultBlock):
474
+ self.logger.debug(f"ToolResultBlock: {block.tool_use_id}")
475
+
476
+ return AgentResult(
477
+ output=result_text,
478
+ status="success",
479
+ tool_calls=tool_calls,
480
+ turns_used=turns_used,
481
+ )
482
+
483
+ except Exception as e:
484
+ self.logger.error(f"SDK execution failed: {e}", exc_info=True)
485
+ return AgentResult(
486
+ output="",
487
+ status="error",
488
+ tool_calls=tool_calls,
489
+ error=str(e),
490
+ turns_used=0,
491
+ )
492
+
493
+ # Run with timeout
494
+ try:
495
+ return await asyncio.wait_for(_run_query(), timeout=timeout)
496
+ except TimeoutError:
497
+ return AgentResult(
498
+ output="",
499
+ status="timeout",
500
+ tool_calls=tool_calls,
501
+ error=f"Execution timed out after {timeout}s",
502
+ turns_used=turns_counter[0],
503
+ )