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,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
|
+
)
|