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,513 @@
1
+ """
2
+ Codex (OpenAI) implementation of AgentExecutor.
3
+
4
+ Supports two authentication modes with different capabilities:
5
+
6
+ 1. api_key mode (OPENAI_API_KEY):
7
+ - Uses OpenAI API with function calling
8
+ - Full tool injection support
9
+ - Requires OPENAI_API_KEY environment variable
10
+
11
+ 2. subscription mode (ChatGPT Plus/Pro/Team/Enterprise):
12
+ - Spawns `codex exec --json` CLI and parses JSONL events
13
+ - Uses Codex's built-in tools (bash, file operations, etc.)
14
+ - NO custom tool injection - tools parameter is IGNORED
15
+ - Good for delegating complete autonomous tasks
16
+
17
+ IMPORTANT: These modes have fundamentally different capabilities.
18
+ Use api_key mode if you need custom MCP tool injection.
19
+ Use subscription mode for delegating complete tasks to Codex.
20
+ """
21
+
22
+ import asyncio
23
+ import json
24
+ import logging
25
+ import os
26
+ import shutil
27
+ from typing import Any, Literal
28
+
29
+ from gobby.llm.executor import (
30
+ AgentExecutor,
31
+ AgentResult,
32
+ ToolCallRecord,
33
+ ToolHandler,
34
+ ToolSchema,
35
+ )
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+ # Auth mode type
40
+ CodexAuthMode = Literal["api_key", "subscription"]
41
+
42
+
43
+ class CodexExecutor(AgentExecutor):
44
+ """
45
+ Codex (OpenAI) implementation of AgentExecutor.
46
+
47
+ Supports two authentication modes with DIFFERENT CAPABILITIES:
48
+
49
+ api_key mode:
50
+ - Uses OpenAI API function calling (like GPT-4)
51
+ - Full tool injection support via tools parameter
52
+ - Requires OPENAI_API_KEY environment variable
53
+ - Standard agentic loop with custom tools
54
+
55
+ subscription mode:
56
+ - Spawns `codex exec --json` CLI process
57
+ - Parses JSONL events (thread.started, item.completed, turn.completed)
58
+ - Uses Codex's built-in tools ONLY (bash, file ops, web search, etc.)
59
+ - The `tools` parameter is IGNORED in this mode
60
+ - Cannot inject custom MCP tools
61
+ - Best for delegating complete autonomous tasks
62
+
63
+ Example (api_key mode):
64
+ >>> executor = CodexExecutor(auth_mode="api_key")
65
+ >>> result = await executor.run(
66
+ ... prompt="Create a task",
67
+ ... tools=[ToolSchema(name="create_task", ...)],
68
+ ... tool_handler=my_handler,
69
+ ... )
70
+
71
+ Example (subscription mode):
72
+ >>> executor = CodexExecutor(auth_mode="subscription")
73
+ >>> result = await executor.run(
74
+ ... prompt="Fix the bug in main.py and run the tests",
75
+ ... tools=[], # Ignored - Codex uses its own tools
76
+ ... tool_handler=lambda *args: None, # Not called
77
+ ... )
78
+ """
79
+
80
+ def __init__(
81
+ self,
82
+ auth_mode: CodexAuthMode = "api_key",
83
+ api_key: str | None = None,
84
+ default_model: str = "gpt-4o",
85
+ ):
86
+ """
87
+ Initialize CodexExecutor.
88
+
89
+ Args:
90
+ auth_mode: Authentication mode.
91
+ - "api_key": Use OpenAI API with function calling (requires OPENAI_API_KEY)
92
+ - "subscription": Use Codex CLI with ChatGPT subscription (requires `codex` in PATH)
93
+ api_key: OpenAI API key (optional for api_key mode, uses OPENAI_API_KEY env var).
94
+ default_model: Default model for api_key mode (default: gpt-4o).
95
+ """
96
+ self.auth_mode = auth_mode
97
+ self.default_model = default_model
98
+ self.logger = logger
99
+ self._client: Any = None
100
+ self._cli_path: str = ""
101
+
102
+ if auth_mode == "api_key":
103
+ # Use provided key or fall back to environment variable
104
+ key = api_key or os.environ.get("OPENAI_API_KEY")
105
+ if not key:
106
+ raise ValueError(
107
+ "API key required for api_key mode. "
108
+ "Provide api_key parameter or set OPENAI_API_KEY env var."
109
+ )
110
+ try:
111
+ from openai import AsyncOpenAI
112
+
113
+ self._client = AsyncOpenAI(api_key=key)
114
+ self.logger.debug("CodexExecutor initialized with API key")
115
+ except ImportError as e:
116
+ raise ImportError(
117
+ "openai package not found. Please install with `pip install openai`."
118
+ ) from e
119
+
120
+ elif auth_mode == "subscription":
121
+ # Verify Codex CLI is available
122
+ cli_path = shutil.which("codex")
123
+ if not cli_path:
124
+ raise ValueError(
125
+ "Codex CLI not found in PATH. "
126
+ "Install Codex CLI and run `codex login` for subscription mode."
127
+ )
128
+ self._cli_path = cli_path
129
+ self.logger.debug(f"CodexExecutor initialized with CLI at {cli_path}")
130
+
131
+ else:
132
+ raise ValueError(f"Unknown auth_mode: {auth_mode}")
133
+
134
+ @property
135
+ def provider_name(self) -> str:
136
+ """Return the provider name."""
137
+ return "codex"
138
+
139
+ def _convert_tools_to_openai_format(self, tools: list[ToolSchema]) -> list[dict[str, Any]]:
140
+ """Convert ToolSchema list to OpenAI function calling format."""
141
+ openai_tools = []
142
+ for tool in tools:
143
+ # Ensure input_schema has "type": "object"
144
+ params = {"type": "object", **tool.input_schema}
145
+ openai_tools.append(
146
+ {
147
+ "type": "function",
148
+ "function": {
149
+ "name": tool.name,
150
+ "description": tool.description,
151
+ "parameters": params,
152
+ },
153
+ }
154
+ )
155
+ return openai_tools
156
+
157
+ async def run(
158
+ self,
159
+ prompt: str,
160
+ tools: list[ToolSchema],
161
+ tool_handler: ToolHandler,
162
+ system_prompt: str | None = None,
163
+ model: str | None = None,
164
+ max_turns: int = 10,
165
+ timeout: float = 120.0,
166
+ ) -> AgentResult:
167
+ """
168
+ Execute an agentic loop.
169
+
170
+ For api_key mode: Uses OpenAI function calling with custom tools.
171
+ For subscription mode: Spawns Codex CLI (tools parameter is IGNORED).
172
+
173
+ Args:
174
+ prompt: The user prompt to process.
175
+ tools: List of available tools (IGNORED in subscription mode).
176
+ tool_handler: Callback for tool calls (NOT CALLED in subscription mode).
177
+ system_prompt: Optional system prompt (api_key mode only).
178
+ model: Optional model override (api_key mode only).
179
+ max_turns: Maximum turns before stopping (api_key mode only).
180
+ timeout: Maximum execution time in seconds.
181
+
182
+ Returns:
183
+ AgentResult with output, status, and tool call records.
184
+ """
185
+ if self.auth_mode == "api_key":
186
+ return await self._run_with_api(
187
+ prompt=prompt,
188
+ tools=tools,
189
+ tool_handler=tool_handler,
190
+ system_prompt=system_prompt,
191
+ model=model or self.default_model,
192
+ max_turns=max_turns,
193
+ timeout=timeout,
194
+ )
195
+ else:
196
+ return await self._run_with_cli(
197
+ prompt=prompt,
198
+ timeout=timeout,
199
+ )
200
+
201
+ async def _run_with_api(
202
+ self,
203
+ prompt: str,
204
+ tools: list[ToolSchema],
205
+ tool_handler: ToolHandler,
206
+ system_prompt: str | None,
207
+ model: str,
208
+ max_turns: int,
209
+ timeout: float,
210
+ ) -> AgentResult:
211
+ """Run using OpenAI API with function calling."""
212
+ if self._client is None:
213
+ return AgentResult(
214
+ output="",
215
+ status="error",
216
+ error="OpenAI client not initialized",
217
+ turns_used=0,
218
+ )
219
+
220
+ tool_calls_list: list[ToolCallRecord] = []
221
+ openai_tools = self._convert_tools_to_openai_format(tools)
222
+
223
+ # Build initial messages
224
+ messages: list[dict[str, Any]] = []
225
+ if system_prompt:
226
+ messages.append({"role": "system", "content": system_prompt})
227
+ messages.append({"role": "user", "content": prompt})
228
+
229
+ # Track turns in outer scope so timeout handler can access the count
230
+ turns_counter = [0]
231
+
232
+ async def _run_loop() -> AgentResult:
233
+ nonlocal messages
234
+ turns_used = 0
235
+ final_output = ""
236
+ client = self._client
237
+ if client is None:
238
+ raise RuntimeError("CodexExecutor client not initialized")
239
+
240
+ while turns_used < max_turns:
241
+ turns_used += 1
242
+ turns_counter[0] = turns_used
243
+
244
+ # Call OpenAI
245
+ try:
246
+ response = await client.chat.completions.create(
247
+ model=model,
248
+ messages=messages,
249
+ tools=openai_tools if openai_tools else None,
250
+ max_tokens=8192,
251
+ )
252
+ except Exception as e:
253
+ self.logger.error(f"OpenAI API error: {e}")
254
+ return AgentResult(
255
+ output="",
256
+ status="error",
257
+ tool_calls=tool_calls_list,
258
+ error=f"OpenAI API error: {e}",
259
+ turns_used=turns_used,
260
+ )
261
+
262
+ # Get the assistant's message
263
+ choice = response.choices[0]
264
+ message = choice.message
265
+
266
+ # Extract text content
267
+ if message.content:
268
+ final_output = message.content
269
+
270
+ # Add assistant message to history
271
+ messages.append(message.model_dump())
272
+
273
+ # Check if there are tool calls
274
+ if not message.tool_calls:
275
+ # No tool calls - we're done
276
+ return AgentResult(
277
+ output=final_output,
278
+ status="success",
279
+ tool_calls=tool_calls_list,
280
+ turns_used=turns_used,
281
+ )
282
+
283
+ # Handle tool calls
284
+ for tool_call in message.tool_calls:
285
+ tool_name = tool_call.function.name
286
+ try:
287
+ arguments = json.loads(tool_call.function.arguments)
288
+ except json.JSONDecodeError as e:
289
+ self.logger.warning(
290
+ f"Failed to parse tool call arguments for '{tool_name}' "
291
+ f"(id={getattr(tool_call, 'id', 'unknown')}): {e}. "
292
+ f"Arguments: {tool_call.function.arguments!r}"
293
+ )
294
+ arguments = {}
295
+
296
+ # Record the tool call
297
+ record = ToolCallRecord(
298
+ tool_name=tool_name,
299
+ arguments=arguments,
300
+ )
301
+ tool_calls_list.append(record)
302
+
303
+ # Execute via handler
304
+ try:
305
+ result = await tool_handler(tool_name, arguments)
306
+ record.result = result
307
+
308
+ # Format result for OpenAI
309
+ if result.success:
310
+ content = json.dumps(result.result) if result.result else "Success"
311
+ else:
312
+ content = f"Error: {result.error}"
313
+
314
+ except Exception as e:
315
+ self.logger.error(f"Tool handler error for {tool_name}: {e}")
316
+ from gobby.llm.executor import ToolResult as TR
317
+
318
+ record.result = TR(
319
+ tool_name=tool_name,
320
+ success=False,
321
+ error=str(e),
322
+ )
323
+ content = f"Error: {e}"
324
+
325
+ # Add tool result to messages
326
+ messages.append(
327
+ {
328
+ "role": "tool",
329
+ "tool_call_id": tool_call.id,
330
+ "content": content,
331
+ }
332
+ )
333
+
334
+ # Check finish reason
335
+ if choice.finish_reason == "stop":
336
+ return AgentResult(
337
+ output=final_output,
338
+ status="success",
339
+ tool_calls=tool_calls_list,
340
+ turns_used=turns_used,
341
+ )
342
+
343
+ # Max turns reached
344
+ return AgentResult(
345
+ output=final_output,
346
+ status="partial",
347
+ tool_calls=tool_calls_list,
348
+ turns_used=turns_used,
349
+ )
350
+
351
+ # Run with timeout
352
+ try:
353
+ return await asyncio.wait_for(_run_loop(), timeout=timeout)
354
+ except TimeoutError:
355
+ return AgentResult(
356
+ output="",
357
+ status="timeout",
358
+ tool_calls=tool_calls_list,
359
+ error=f"Execution timed out after {timeout}s",
360
+ turns_used=turns_counter[0],
361
+ )
362
+
363
+ async def _run_with_cli(
364
+ self,
365
+ prompt: str,
366
+ timeout: float,
367
+ ) -> AgentResult:
368
+ """
369
+ Run using Codex CLI in subscription mode.
370
+
371
+ This mode spawns `codex exec --json` and parses JSONL events.
372
+ Custom tools are NOT supported - Codex uses its built-in tools.
373
+
374
+ JSONL events include:
375
+ - thread.started: Session begins
376
+ - turn.started/completed: Turn lifecycle
377
+ - item.started/completed: Individual items (reasoning, commands, messages)
378
+ - item types: reasoning, command_execution, agent_message, file_change, etc.
379
+ """
380
+ tool_calls_list: list[ToolCallRecord] = []
381
+ final_output = ""
382
+ turns_used = 0
383
+
384
+ try:
385
+ # Spawn codex exec with JSON output
386
+ process = await asyncio.create_subprocess_exec(
387
+ self._cli_path,
388
+ "exec",
389
+ "--json",
390
+ prompt,
391
+ stdout=asyncio.subprocess.PIPE,
392
+ stderr=asyncio.subprocess.PIPE,
393
+ )
394
+
395
+ # Read JSONL events with timeout
396
+ try:
397
+ stdout_data, stderr_data = await asyncio.wait_for(
398
+ process.communicate(), timeout=timeout
399
+ )
400
+ except TimeoutError:
401
+ process.kill()
402
+ await process.wait()
403
+ return AgentResult(
404
+ output="",
405
+ status="timeout",
406
+ tool_calls=tool_calls_list,
407
+ error=f"Codex CLI timed out after {timeout}s",
408
+ turns_used=turns_used,
409
+ )
410
+
411
+ # Parse JSONL output
412
+ if stdout_data:
413
+ for line in stdout_data.decode("utf-8").splitlines():
414
+ if not line.strip():
415
+ continue
416
+ try:
417
+ event = json.loads(line)
418
+ event_type = event.get("type", "")
419
+
420
+ if event_type == "turn.started":
421
+ turns_used += 1
422
+
423
+ elif event_type == "turn.completed":
424
+ # Extract usage stats if available
425
+ pass
426
+
427
+ elif event_type == "item.completed":
428
+ item = event.get("item", {})
429
+ item_type = item.get("type", "")
430
+
431
+ if item_type == "agent_message":
432
+ # Final message from the agent
433
+ final_output = item.get("text", "")
434
+
435
+ elif item_type == "command_execution":
436
+ # Record as a tool call
437
+ command = item.get("command", "")
438
+ output = item.get("aggregated_output", "")
439
+ exit_code = item.get("exit_code", 0)
440
+
441
+ from gobby.llm.executor import ToolResult
442
+
443
+ record = ToolCallRecord(
444
+ tool_name="bash",
445
+ arguments={"command": command},
446
+ result=ToolResult(
447
+ tool_name="bash",
448
+ success=exit_code == 0,
449
+ result=output if exit_code == 0 else None,
450
+ error=output if exit_code != 0 else None,
451
+ ),
452
+ )
453
+ tool_calls_list.append(record)
454
+
455
+ elif item_type == "file_change":
456
+ # Record file changes
457
+ file_path = item.get("path", "")
458
+ change_type = item.get("change_type", "")
459
+
460
+ from gobby.llm.executor import ToolResult
461
+
462
+ record = ToolCallRecord(
463
+ tool_name="file_change",
464
+ arguments={
465
+ "path": file_path,
466
+ "type": change_type,
467
+ },
468
+ result=ToolResult(
469
+ tool_name="file_change",
470
+ success=True,
471
+ result={"path": file_path, "type": change_type},
472
+ ),
473
+ )
474
+ tool_calls_list.append(record)
475
+
476
+ except json.JSONDecodeError:
477
+ # Skip non-JSON lines
478
+ continue
479
+
480
+ # Check process exit code
481
+ if process.returncode != 0:
482
+ stderr_text = stderr_data.decode("utf-8") if stderr_data else ""
483
+ return AgentResult(
484
+ output=final_output,
485
+ status="error",
486
+ tool_calls=tool_calls_list,
487
+ error=f"Codex CLI exited with code {process.returncode}: {stderr_text}",
488
+ turns_used=turns_used,
489
+ )
490
+
491
+ return AgentResult(
492
+ output=final_output,
493
+ status="success",
494
+ tool_calls=tool_calls_list,
495
+ turns_used=turns_used,
496
+ )
497
+
498
+ except FileNotFoundError:
499
+ return AgentResult(
500
+ output="",
501
+ status="error",
502
+ error="Codex CLI not found. Install with: npm install -g @openai/codex",
503
+ turns_used=0,
504
+ )
505
+ except Exception as e:
506
+ self.logger.error(f"Codex CLI execution failed: {e}")
507
+ return AgentResult(
508
+ output="",
509
+ status="error",
510
+ tool_calls=tool_calls_list,
511
+ error=str(e),
512
+ turns_used=turns_used,
513
+ )