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.
- gobby/__init__.py +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- 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
|
+
)
|