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
gobby/agents/runner.py
ADDED
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent runner for orchestrating agent execution.
|
|
3
|
+
|
|
4
|
+
The AgentRunner coordinates:
|
|
5
|
+
- Creating child sessions for agents
|
|
6
|
+
- Tracking agent runs in the database
|
|
7
|
+
- Executing agents via LLM providers
|
|
8
|
+
- Handling tool calls via the MCP proxy
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import threading
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
from gobby.agents.registry import RunningAgent
|
|
19
|
+
from gobby.agents.session import ChildSessionConfig, ChildSessionManager
|
|
20
|
+
from gobby.llm.executor import AgentExecutor, AgentResult, ToolHandler, ToolResult, ToolSchema
|
|
21
|
+
from gobby.storage.agents import AgentRun, LocalAgentRunManager
|
|
22
|
+
from gobby.storage.sessions import Session
|
|
23
|
+
from gobby.workflows.definitions import WorkflowDefinition, WorkflowState
|
|
24
|
+
from gobby.workflows.loader import WorkflowLoader
|
|
25
|
+
from gobby.workflows.state_manager import WorkflowStateManager
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from gobby.storage.database import DatabaseProtocol
|
|
29
|
+
from gobby.storage.sessions import LocalSessionManager
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class AgentConfig:
|
|
36
|
+
"""Configuration for running an agent."""
|
|
37
|
+
|
|
38
|
+
prompt: str
|
|
39
|
+
"""The prompt/task for the agent to perform."""
|
|
40
|
+
|
|
41
|
+
# Required context - can be inferred from get_project_context() or passed explicitly
|
|
42
|
+
parent_session_id: str | None = None
|
|
43
|
+
"""ID of the session spawning this agent. Inferred from context if not provided."""
|
|
44
|
+
|
|
45
|
+
project_id: str | None = None
|
|
46
|
+
"""Project ID for the agent's session. Inferred from context if not provided."""
|
|
47
|
+
|
|
48
|
+
machine_id: str | None = None
|
|
49
|
+
"""Machine identifier. Defaults to hostname if not provided."""
|
|
50
|
+
|
|
51
|
+
source: str = "claude"
|
|
52
|
+
"""CLI source (claude, gemini, codex)."""
|
|
53
|
+
|
|
54
|
+
# New spec-aligned parameters
|
|
55
|
+
workflow: str | None = None
|
|
56
|
+
"""Workflow name or path to execute."""
|
|
57
|
+
|
|
58
|
+
task: str | None = None
|
|
59
|
+
"""Task ID or 'next' for auto-select."""
|
|
60
|
+
|
|
61
|
+
agent: str | None = None
|
|
62
|
+
"""Named agent definition to use."""
|
|
63
|
+
|
|
64
|
+
lifecycle_variables: dict[str, Any] | None = None
|
|
65
|
+
"""Lifecycle variables to override parent settings."""
|
|
66
|
+
|
|
67
|
+
default_variables: dict[str, Any] | None = None
|
|
68
|
+
"""Default variables for the agent."""
|
|
69
|
+
|
|
70
|
+
session_context: str = "summary_markdown"
|
|
71
|
+
"""Context source: summary_markdown, compact_markdown, session_id:<id>, transcript:<n>, file:<path>."""
|
|
72
|
+
|
|
73
|
+
mode: str = "in_process"
|
|
74
|
+
"""Execution mode: in_process, terminal, embedded, headless."""
|
|
75
|
+
|
|
76
|
+
terminal: str = "auto"
|
|
77
|
+
"""Terminal for terminal/embedded modes: auto, ghostty, iterm, etc."""
|
|
78
|
+
|
|
79
|
+
worktree_id: str | None = None
|
|
80
|
+
"""Existing worktree to use for terminal mode."""
|
|
81
|
+
|
|
82
|
+
# Provider settings
|
|
83
|
+
provider: str = "claude"
|
|
84
|
+
"""LLM provider to use."""
|
|
85
|
+
|
|
86
|
+
model: str | None = None
|
|
87
|
+
"""Optional model override."""
|
|
88
|
+
|
|
89
|
+
# Execution limits
|
|
90
|
+
max_turns: int = 10
|
|
91
|
+
"""Maximum number of turns."""
|
|
92
|
+
|
|
93
|
+
timeout: float = 120.0
|
|
94
|
+
"""Execution timeout in seconds."""
|
|
95
|
+
|
|
96
|
+
# Legacy/internal fields (kept for compatibility)
|
|
97
|
+
workflow_name: str | None = None
|
|
98
|
+
"""Deprecated: use 'workflow' instead. Kept for backward compatibility."""
|
|
99
|
+
|
|
100
|
+
system_prompt: str | None = None
|
|
101
|
+
"""Optional system prompt override."""
|
|
102
|
+
|
|
103
|
+
tools: list[ToolSchema] | None = None
|
|
104
|
+
"""Optional list of tools to provide."""
|
|
105
|
+
|
|
106
|
+
git_branch: str | None = None
|
|
107
|
+
"""Git branch for the session."""
|
|
108
|
+
|
|
109
|
+
title: str | None = None
|
|
110
|
+
"""Optional title for the agent session."""
|
|
111
|
+
|
|
112
|
+
project_path: str | None = None
|
|
113
|
+
"""Project path for loading project-specific workflows."""
|
|
114
|
+
|
|
115
|
+
context_injected: bool = False
|
|
116
|
+
"""Whether context was successfully injected into the prompt."""
|
|
117
|
+
|
|
118
|
+
def get_effective_workflow(self) -> str | None:
|
|
119
|
+
"""Get the workflow name, preferring 'workflow' over legacy 'workflow_name'."""
|
|
120
|
+
return self.workflow or self.workflow_name
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class AgentRunContext:
|
|
125
|
+
"""
|
|
126
|
+
Runtime context for an agent execution.
|
|
127
|
+
|
|
128
|
+
Contains all the objects needed to execute an agent, created during
|
|
129
|
+
the prepare phase and used during execution.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
session: Session | None = None
|
|
133
|
+
"""Child session object created for this agent."""
|
|
134
|
+
|
|
135
|
+
run: AgentRun | None = None
|
|
136
|
+
"""Agent run record from the database."""
|
|
137
|
+
|
|
138
|
+
workflow_state: WorkflowState | None = None
|
|
139
|
+
"""Workflow state for the child session, if workflow specified."""
|
|
140
|
+
|
|
141
|
+
workflow_config: WorkflowDefinition | None = None
|
|
142
|
+
"""Loaded workflow definition, if workflow_name was specified."""
|
|
143
|
+
|
|
144
|
+
# Convenience accessors for IDs
|
|
145
|
+
@property
|
|
146
|
+
def session_id(self) -> str | None:
|
|
147
|
+
"""Get the child session ID."""
|
|
148
|
+
return self.session.id if self.session else None
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def run_id(self) -> str | None:
|
|
152
|
+
"""Get the agent run ID."""
|
|
153
|
+
return self.run.id if self.run else None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class AgentRunner:
|
|
157
|
+
"""
|
|
158
|
+
Orchestrates agent execution with session and run tracking.
|
|
159
|
+
|
|
160
|
+
The runner:
|
|
161
|
+
1. Creates a child session for the agent
|
|
162
|
+
2. Records the agent run in the database
|
|
163
|
+
3. Executes the agent via the appropriate LLM provider
|
|
164
|
+
4. Updates run status based on execution result
|
|
165
|
+
|
|
166
|
+
Example:
|
|
167
|
+
>>> runner = AgentRunner(db, session_storage, executor)
|
|
168
|
+
>>> result = await runner.run(AgentConfig(
|
|
169
|
+
... prompt="Create a TODO list",
|
|
170
|
+
... parent_session_id="sess-123",
|
|
171
|
+
... project_id="proj-abc",
|
|
172
|
+
... machine_id="machine-1",
|
|
173
|
+
... source="claude",
|
|
174
|
+
... ))
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
def __init__(
|
|
178
|
+
self,
|
|
179
|
+
db: DatabaseProtocol,
|
|
180
|
+
session_storage: LocalSessionManager,
|
|
181
|
+
executors: dict[str, AgentExecutor],
|
|
182
|
+
max_agent_depth: int = 1,
|
|
183
|
+
workflow_loader: WorkflowLoader | None = None,
|
|
184
|
+
):
|
|
185
|
+
"""
|
|
186
|
+
Initialize AgentRunner.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
db: Database connection.
|
|
190
|
+
session_storage: Session storage manager.
|
|
191
|
+
executors: Map of provider name to executor instance.
|
|
192
|
+
max_agent_depth: Maximum nesting depth for agents.
|
|
193
|
+
workflow_loader: Optional WorkflowLoader for loading workflow definitions.
|
|
194
|
+
"""
|
|
195
|
+
self.db = db
|
|
196
|
+
self._session_storage = session_storage
|
|
197
|
+
self._executors = executors
|
|
198
|
+
self._child_session_manager = ChildSessionManager(
|
|
199
|
+
session_storage,
|
|
200
|
+
max_agent_depth=max_agent_depth,
|
|
201
|
+
)
|
|
202
|
+
self._run_storage = LocalAgentRunManager(db)
|
|
203
|
+
self._workflow_loader = workflow_loader or WorkflowLoader()
|
|
204
|
+
from gobby.agents.definitions import AgentDefinitionLoader
|
|
205
|
+
|
|
206
|
+
self._agent_loader = AgentDefinitionLoader()
|
|
207
|
+
self._workflow_state_manager = WorkflowStateManager(db)
|
|
208
|
+
|
|
209
|
+
self.logger = logger
|
|
210
|
+
|
|
211
|
+
# Thread-safe in-memory tracking of running agents
|
|
212
|
+
self._running_agents: dict[str, RunningAgent] = {}
|
|
213
|
+
self._running_agents_lock = threading.Lock()
|
|
214
|
+
|
|
215
|
+
def get_executor(self, provider: str) -> AgentExecutor | None:
|
|
216
|
+
"""Get executor for a provider."""
|
|
217
|
+
return self._executors.get(provider)
|
|
218
|
+
|
|
219
|
+
def register_executor(self, provider: str, executor: AgentExecutor) -> None:
|
|
220
|
+
"""Register an executor for a provider."""
|
|
221
|
+
self._executors[provider] = executor
|
|
222
|
+
self.logger.info(f"Registered executor for provider: {provider}")
|
|
223
|
+
|
|
224
|
+
def can_spawn(self, parent_session_id: str) -> tuple[bool, str, int]:
|
|
225
|
+
"""
|
|
226
|
+
Check if an agent can be spawned from the given session.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
parent_session_id: The session attempting to spawn.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Tuple of (can_spawn, reason, parent_depth).
|
|
233
|
+
The parent_depth is returned to avoid redundant depth lookups.
|
|
234
|
+
"""
|
|
235
|
+
return self._child_session_manager.can_spawn_child(parent_session_id)
|
|
236
|
+
|
|
237
|
+
def prepare_run(self, config: AgentConfig) -> AgentRunContext | AgentResult:
|
|
238
|
+
"""
|
|
239
|
+
Prepare for agent execution by creating database records.
|
|
240
|
+
|
|
241
|
+
Creates:
|
|
242
|
+
- Child session linked to parent
|
|
243
|
+
- Agent run record in database
|
|
244
|
+
- Workflow state (if workflow specified)
|
|
245
|
+
|
|
246
|
+
This method can be used separately for terminal mode, where we prepare
|
|
247
|
+
the database state, then spawn a terminal process that picks up from
|
|
248
|
+
the session via hooks.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
config: Agent configuration.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
AgentRunContext on success, or AgentResult with error on failure.
|
|
255
|
+
"""
|
|
256
|
+
# Validate required fields
|
|
257
|
+
if not config.parent_session_id:
|
|
258
|
+
return AgentResult(
|
|
259
|
+
output="",
|
|
260
|
+
status="error",
|
|
261
|
+
error="parent_session_id is required",
|
|
262
|
+
turns_used=0,
|
|
263
|
+
)
|
|
264
|
+
if not config.project_id:
|
|
265
|
+
return AgentResult(
|
|
266
|
+
output="",
|
|
267
|
+
status="error",
|
|
268
|
+
error="project_id is required",
|
|
269
|
+
turns_used=0,
|
|
270
|
+
)
|
|
271
|
+
if not config.machine_id:
|
|
272
|
+
return AgentResult(
|
|
273
|
+
output="",
|
|
274
|
+
status="error",
|
|
275
|
+
error="machine_id is required",
|
|
276
|
+
turns_used=0,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Type narrowing for mypy - these are guaranteed non-None after validation above
|
|
280
|
+
parent_session_id: str = config.parent_session_id
|
|
281
|
+
project_id: str = config.project_id
|
|
282
|
+
machine_id: str = config.machine_id
|
|
283
|
+
|
|
284
|
+
# Check if we can spawn (also get parent_depth to avoid redundant lookups)
|
|
285
|
+
can_spawn, reason, _parent_depth = self.can_spawn(parent_session_id)
|
|
286
|
+
if not can_spawn:
|
|
287
|
+
self.logger.warning(f"Cannot spawn agent: {reason}")
|
|
288
|
+
return AgentResult(
|
|
289
|
+
output="",
|
|
290
|
+
status="error",
|
|
291
|
+
error=reason,
|
|
292
|
+
turns_used=0,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Load agent definition if specified
|
|
296
|
+
if config.agent:
|
|
297
|
+
agent_def = self._agent_loader.load(config.agent)
|
|
298
|
+
if agent_def:
|
|
299
|
+
# Merge definition into config (config takes precedence if explicitly set?)
|
|
300
|
+
# Actually, definition provides defaults/overrides.
|
|
301
|
+
# Logic:
|
|
302
|
+
# 1. Use workflow from definition if not in config
|
|
303
|
+
# 2. Use model from definition if not in config
|
|
304
|
+
# 3. Merge lifecycle_variables
|
|
305
|
+
|
|
306
|
+
if not config.workflow:
|
|
307
|
+
config.workflow = agent_def.workflow
|
|
308
|
+
|
|
309
|
+
if not config.model:
|
|
310
|
+
config.model = agent_def.model
|
|
311
|
+
|
|
312
|
+
# Merge lifecycle variables (definition wins? or config? usually definition sets policy)
|
|
313
|
+
def_lifecycle = agent_def.lifecycle_variables or {}
|
|
314
|
+
config_lifecycle = config.lifecycle_variables or {}
|
|
315
|
+
# Config overrides definition? Or vice versa?
|
|
316
|
+
# The Plan says "Child session created with lifecycle_variables merged in"
|
|
317
|
+
# Let's say config overrides definition (standard)
|
|
318
|
+
config.lifecycle_variables = {**def_lifecycle, **config_lifecycle}
|
|
319
|
+
|
|
320
|
+
# Merge default variables
|
|
321
|
+
def_vars = agent_def.default_variables or {}
|
|
322
|
+
config_vars = config.default_variables or {}
|
|
323
|
+
config.default_variables = {**def_vars, **config_vars}
|
|
324
|
+
|
|
325
|
+
self.logger.info(f"Loaded agent definition '{config.agent}'")
|
|
326
|
+
else:
|
|
327
|
+
self.logger.warning(f"Agent definition '{config.agent}' not found")
|
|
328
|
+
|
|
329
|
+
# Get effective workflow name (prefers 'workflow' over legacy 'workflow_name')
|
|
330
|
+
effective_workflow = config.get_effective_workflow()
|
|
331
|
+
|
|
332
|
+
# Validate workflow BEFORE creating child session to avoid orphaned sessions
|
|
333
|
+
workflow_definition = None
|
|
334
|
+
if effective_workflow:
|
|
335
|
+
workflow_definition = self._workflow_loader.load_workflow(
|
|
336
|
+
effective_workflow,
|
|
337
|
+
project_path=config.project_path,
|
|
338
|
+
)
|
|
339
|
+
if workflow_definition:
|
|
340
|
+
# Reject lifecycle workflows - they run automatically via hooks
|
|
341
|
+
if workflow_definition.type == "lifecycle":
|
|
342
|
+
self.logger.error(
|
|
343
|
+
f"Cannot use lifecycle workflow '{effective_workflow}' for agent spawning"
|
|
344
|
+
)
|
|
345
|
+
return AgentResult(
|
|
346
|
+
output="",
|
|
347
|
+
status="error",
|
|
348
|
+
error=(
|
|
349
|
+
f"Cannot use lifecycle workflow '{effective_workflow}' for agent spawning. "
|
|
350
|
+
f"Lifecycle workflows run automatically on events. "
|
|
351
|
+
f"Use a step workflow like 'plan-execute' instead."
|
|
352
|
+
),
|
|
353
|
+
turns_used=0,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# Create child session (now safe - workflow validated above)
|
|
357
|
+
try:
|
|
358
|
+
child_session = self._child_session_manager.create_child_session(
|
|
359
|
+
ChildSessionConfig(
|
|
360
|
+
parent_session_id=parent_session_id,
|
|
361
|
+
project_id=project_id,
|
|
362
|
+
machine_id=machine_id,
|
|
363
|
+
source=config.source,
|
|
364
|
+
workflow_name=effective_workflow,
|
|
365
|
+
title=config.title,
|
|
366
|
+
git_branch=config.git_branch,
|
|
367
|
+
lifecycle_variables=config.lifecycle_variables,
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
except ValueError as e:
|
|
371
|
+
self.logger.error(f"Failed to create child session: {e}")
|
|
372
|
+
return AgentResult(
|
|
373
|
+
output="",
|
|
374
|
+
status="error",
|
|
375
|
+
error=str(e),
|
|
376
|
+
turns_used=0,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# Initialize workflow state if workflow was loaded
|
|
380
|
+
workflow_state = None
|
|
381
|
+
if workflow_definition:
|
|
382
|
+
self.logger.info(
|
|
383
|
+
f"Loaded workflow '{effective_workflow}' for agent "
|
|
384
|
+
f"(type={workflow_definition.type})"
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# Initialize workflow state for child session
|
|
388
|
+
initial_step = ""
|
|
389
|
+
if workflow_definition.steps:
|
|
390
|
+
initial_step = workflow_definition.steps[0].name
|
|
391
|
+
|
|
392
|
+
# Build initial variables with agent depth information
|
|
393
|
+
initial_variables = dict(workflow_definition.variables)
|
|
394
|
+
initial_variables["agent_depth"] = child_session.agent_depth
|
|
395
|
+
initial_variables["max_agent_depth"] = self._child_session_manager.max_agent_depth
|
|
396
|
+
initial_variables["can_spawn"] = (
|
|
397
|
+
child_session.agent_depth < self._child_session_manager.max_agent_depth
|
|
398
|
+
)
|
|
399
|
+
initial_variables["parent_session_id"] = parent_session_id
|
|
400
|
+
|
|
401
|
+
workflow_state = WorkflowState(
|
|
402
|
+
session_id=child_session.id,
|
|
403
|
+
workflow_name=effective_workflow,
|
|
404
|
+
step=initial_step,
|
|
405
|
+
variables=initial_variables,
|
|
406
|
+
)
|
|
407
|
+
self._workflow_state_manager.save_state(workflow_state)
|
|
408
|
+
self.logger.info(
|
|
409
|
+
f"Initialized workflow state for child session {child_session.id} "
|
|
410
|
+
f"(step={initial_step}, agent_depth={child_session.agent_depth})"
|
|
411
|
+
)
|
|
412
|
+
elif effective_workflow:
|
|
413
|
+
# workflow_definition is None but effective_workflow was specified
|
|
414
|
+
self.logger.warning(
|
|
415
|
+
f"Workflow '{effective_workflow}' not found, proceeding without workflow"
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
# Create agent run record
|
|
419
|
+
agent_run = self._run_storage.create(
|
|
420
|
+
parent_session_id=parent_session_id,
|
|
421
|
+
provider=config.provider,
|
|
422
|
+
prompt=config.prompt,
|
|
423
|
+
workflow_name=effective_workflow,
|
|
424
|
+
model=config.model,
|
|
425
|
+
child_session_id=child_session.id,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
# Set terminal pickup metadata on child session for terminal mode
|
|
429
|
+
# This allows terminal-spawned agents to pick up their state via hooks
|
|
430
|
+
self._session_storage.update_terminal_pickup_metadata(
|
|
431
|
+
session_id=child_session.id,
|
|
432
|
+
workflow_name=effective_workflow,
|
|
433
|
+
agent_run_id=agent_run.id,
|
|
434
|
+
context_injected=config.context_injected,
|
|
435
|
+
original_prompt=config.prompt,
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
self.logger.info(
|
|
439
|
+
f"Prepared agent run {agent_run.id} "
|
|
440
|
+
f"(child_session={child_session.id}, provider={config.provider})"
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
# Convert child session (internal type) to Session for storage
|
|
444
|
+
# The create_child_session returns a Session dataclass
|
|
445
|
+
session_obj = child_session
|
|
446
|
+
|
|
447
|
+
return AgentRunContext(
|
|
448
|
+
session=session_obj,
|
|
449
|
+
run=agent_run,
|
|
450
|
+
workflow_state=workflow_state,
|
|
451
|
+
workflow_config=workflow_definition,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
async def execute_run(
|
|
455
|
+
self,
|
|
456
|
+
context: AgentRunContext,
|
|
457
|
+
config: AgentConfig,
|
|
458
|
+
tool_handler: ToolHandler | None = None,
|
|
459
|
+
) -> AgentResult:
|
|
460
|
+
"""
|
|
461
|
+
Execute an agent using prepared context.
|
|
462
|
+
|
|
463
|
+
This method runs the agent loop using the context created by prepare_run().
|
|
464
|
+
For in_process mode only - terminal mode uses a different execution path.
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
context: Prepared run context from prepare_run().
|
|
468
|
+
config: Agent configuration.
|
|
469
|
+
tool_handler: Optional async callable for handling tool calls.
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
AgentResult with execution outcome.
|
|
473
|
+
"""
|
|
474
|
+
# Validate context
|
|
475
|
+
if not context.session or not context.run:
|
|
476
|
+
return AgentResult(
|
|
477
|
+
output="",
|
|
478
|
+
status="error",
|
|
479
|
+
error="Invalid context: missing session or run",
|
|
480
|
+
turns_used=0,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
child_session = context.session
|
|
484
|
+
agent_run = context.run
|
|
485
|
+
workflow_definition = context.workflow_config
|
|
486
|
+
|
|
487
|
+
# Get executor for provider
|
|
488
|
+
executor = self.get_executor(config.provider)
|
|
489
|
+
if not executor:
|
|
490
|
+
error_msg = f"No executor registered for provider: {config.provider}"
|
|
491
|
+
self.logger.error(error_msg)
|
|
492
|
+
self._run_storage.fail(agent_run.id, error=error_msg)
|
|
493
|
+
return AgentResult(
|
|
494
|
+
output="",
|
|
495
|
+
status="error",
|
|
496
|
+
error=error_msg,
|
|
497
|
+
turns_used=0,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
# Start the run
|
|
501
|
+
self._run_storage.start(agent_run.id)
|
|
502
|
+
self.logger.info(
|
|
503
|
+
f"Starting agent run {agent_run.id} "
|
|
504
|
+
f"(child_session={child_session.id}, provider={config.provider})"
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
# Track in memory for real-time status
|
|
508
|
+
# Note: parent_session_id is guaranteed non-None here because execute_run
|
|
509
|
+
# is only called after prepare_run validates it
|
|
510
|
+
self._track_running_agent(
|
|
511
|
+
run_id=agent_run.id,
|
|
512
|
+
parent_session_id=config.parent_session_id,
|
|
513
|
+
child_session_id=child_session.id,
|
|
514
|
+
provider=config.provider,
|
|
515
|
+
prompt=config.prompt,
|
|
516
|
+
mode=config.mode,
|
|
517
|
+
workflow_name=config.get_effective_workflow(),
|
|
518
|
+
model=config.model,
|
|
519
|
+
worktree_id=config.worktree_id,
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
# Set up tool handler with workflow filtering
|
|
523
|
+
async def default_tool_handler(tool_name: str, arguments: dict[str, Any]) -> ToolResult:
|
|
524
|
+
"""Default tool handler that returns not implemented."""
|
|
525
|
+
return ToolResult(
|
|
526
|
+
tool_name=tool_name,
|
|
527
|
+
success=False,
|
|
528
|
+
error=f"Tool {tool_name} not implemented",
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
base_handler = tool_handler or default_tool_handler
|
|
532
|
+
|
|
533
|
+
# Create workflow-filtered handler if workflow is active
|
|
534
|
+
if workflow_definition:
|
|
535
|
+
handler = self._create_workflow_filtered_handler(
|
|
536
|
+
base_handler=base_handler,
|
|
537
|
+
session_id=child_session.id,
|
|
538
|
+
workflow_definition=workflow_definition,
|
|
539
|
+
)
|
|
540
|
+
else:
|
|
541
|
+
handler = base_handler
|
|
542
|
+
|
|
543
|
+
# Track tool calls to preserve partial progress info on exception
|
|
544
|
+
# Note: Each tool call within a turn counts separately. The executor's
|
|
545
|
+
# run() method handles turns - we only track tool calls for monitoring.
|
|
546
|
+
tool_calls_made = 0
|
|
547
|
+
|
|
548
|
+
async def tracking_handler(tool_name: str, arguments: dict[str, Any]) -> ToolResult:
|
|
549
|
+
nonlocal tool_calls_made
|
|
550
|
+
tool_calls_made += 1
|
|
551
|
+
# Update in-memory state for real-time monitoring
|
|
552
|
+
# Note: turns_used is tracked by the executor, not per tool call
|
|
553
|
+
self._update_running_agent(
|
|
554
|
+
agent_run.id,
|
|
555
|
+
tool_calls_count=tool_calls_made,
|
|
556
|
+
)
|
|
557
|
+
return await handler(tool_name, arguments)
|
|
558
|
+
|
|
559
|
+
# Execute the agent
|
|
560
|
+
try:
|
|
561
|
+
result = await executor.run(
|
|
562
|
+
prompt=config.prompt,
|
|
563
|
+
tools=config.tools or [],
|
|
564
|
+
tool_handler=tracking_handler,
|
|
565
|
+
system_prompt=config.system_prompt,
|
|
566
|
+
model=config.model,
|
|
567
|
+
max_turns=config.max_turns,
|
|
568
|
+
timeout=config.timeout,
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
# Update run based on result
|
|
572
|
+
if result.status == "success":
|
|
573
|
+
self._run_storage.complete(
|
|
574
|
+
agent_run.id,
|
|
575
|
+
result=result.output,
|
|
576
|
+
tool_calls_count=len(result.tool_calls),
|
|
577
|
+
turns_used=result.turns_used,
|
|
578
|
+
)
|
|
579
|
+
self.logger.info(
|
|
580
|
+
f"Agent run {agent_run.id} completed successfully "
|
|
581
|
+
f"({result.turns_used} turns, {len(result.tool_calls)} tool calls)"
|
|
582
|
+
)
|
|
583
|
+
elif result.status == "timeout":
|
|
584
|
+
self._run_storage.timeout(agent_run.id, turns_used=result.turns_used)
|
|
585
|
+
self.logger.warning(f"Agent run {agent_run.id} timed out")
|
|
586
|
+
elif result.status == "error":
|
|
587
|
+
self._run_storage.fail(
|
|
588
|
+
agent_run.id,
|
|
589
|
+
error=result.error or "Unknown error",
|
|
590
|
+
tool_calls_count=len(result.tool_calls),
|
|
591
|
+
turns_used=result.turns_used,
|
|
592
|
+
)
|
|
593
|
+
self.logger.error(f"Agent run {agent_run.id} failed: {result.error}")
|
|
594
|
+
else:
|
|
595
|
+
# Partial completion
|
|
596
|
+
self._run_storage.complete(
|
|
597
|
+
agent_run.id,
|
|
598
|
+
result=result.output,
|
|
599
|
+
tool_calls_count=len(result.tool_calls),
|
|
600
|
+
turns_used=result.turns_used,
|
|
601
|
+
)
|
|
602
|
+
self.logger.info(f"Agent run {agent_run.id} completed with status {result.status}")
|
|
603
|
+
|
|
604
|
+
# Update session status
|
|
605
|
+
if result.status in ("success", "partial"):
|
|
606
|
+
self._session_storage.update_status(child_session.id, "completed")
|
|
607
|
+
else:
|
|
608
|
+
self._session_storage.update_status(child_session.id, "failed")
|
|
609
|
+
|
|
610
|
+
# Remove from in-memory tracking
|
|
611
|
+
self._untrack_running_agent(agent_run.id)
|
|
612
|
+
|
|
613
|
+
# Set run_id and child_session_id on the result so callers don't need to call list_runs()
|
|
614
|
+
result.run_id = agent_run.id
|
|
615
|
+
result.child_session_id = child_session.id
|
|
616
|
+
|
|
617
|
+
return result
|
|
618
|
+
|
|
619
|
+
except Exception as e:
|
|
620
|
+
self.logger.error(f"Agent execution failed: {e}", exc_info=True)
|
|
621
|
+
# On exception, we don't know the actual turns used by the executor,
|
|
622
|
+
# so we pass 0. tool_calls_made is the count we tracked.
|
|
623
|
+
self._run_storage.fail(
|
|
624
|
+
agent_run.id,
|
|
625
|
+
error=str(e),
|
|
626
|
+
tool_calls_count=tool_calls_made,
|
|
627
|
+
turns_used=0,
|
|
628
|
+
)
|
|
629
|
+
self._session_storage.update_status(child_session.id, "failed")
|
|
630
|
+
# Remove from in-memory tracking
|
|
631
|
+
self._untrack_running_agent(agent_run.id)
|
|
632
|
+
return AgentResult(
|
|
633
|
+
output="",
|
|
634
|
+
status="error",
|
|
635
|
+
error=str(e),
|
|
636
|
+
turns_used=0,
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
async def run(
|
|
640
|
+
self,
|
|
641
|
+
config: AgentConfig,
|
|
642
|
+
tool_handler: ToolHandler | None = None,
|
|
643
|
+
) -> AgentResult:
|
|
644
|
+
"""
|
|
645
|
+
Run an agent with the given configuration.
|
|
646
|
+
|
|
647
|
+
This is the main entry point that combines prepare_run() and execute_run()
|
|
648
|
+
for in-process agent execution.
|
|
649
|
+
|
|
650
|
+
Args:
|
|
651
|
+
config: Agent configuration.
|
|
652
|
+
tool_handler: Optional async callable for handling tool calls.
|
|
653
|
+
If not provided, uses a default no-op handler.
|
|
654
|
+
|
|
655
|
+
Returns:
|
|
656
|
+
AgentResult with execution outcome.
|
|
657
|
+
"""
|
|
658
|
+
# Prepare the run (create session, run record, workflow state)
|
|
659
|
+
result = self.prepare_run(config)
|
|
660
|
+
|
|
661
|
+
# If prepare_run returned an error, return it
|
|
662
|
+
if isinstance(result, AgentResult):
|
|
663
|
+
return result
|
|
664
|
+
|
|
665
|
+
# Execute the run with the prepared context
|
|
666
|
+
context = result
|
|
667
|
+
return await self.execute_run(context, config, tool_handler)
|
|
668
|
+
|
|
669
|
+
def get_run(self, run_id: str) -> Any | None:
|
|
670
|
+
"""Get an agent run by ID."""
|
|
671
|
+
return self._run_storage.get(run_id)
|
|
672
|
+
|
|
673
|
+
def list_runs(
|
|
674
|
+
self,
|
|
675
|
+
parent_session_id: str,
|
|
676
|
+
status: str | None = None,
|
|
677
|
+
limit: int = 100,
|
|
678
|
+
) -> list[Any]:
|
|
679
|
+
"""List agent runs for a session."""
|
|
680
|
+
return self._run_storage.list_by_session(
|
|
681
|
+
parent_session_id,
|
|
682
|
+
status=status, # type: ignore
|
|
683
|
+
limit=limit,
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
def cancel_run(self, run_id: str) -> bool:
|
|
687
|
+
"""Cancel a running agent."""
|
|
688
|
+
run = self._run_storage.get(run_id)
|
|
689
|
+
if not run:
|
|
690
|
+
return False
|
|
691
|
+
if run.status != "running":
|
|
692
|
+
return False
|
|
693
|
+
|
|
694
|
+
self._run_storage.cancel(run_id)
|
|
695
|
+
|
|
696
|
+
# Also mark session as cancelled
|
|
697
|
+
if run.child_session_id:
|
|
698
|
+
self._session_storage.update_status(run.child_session_id, "cancelled")
|
|
699
|
+
|
|
700
|
+
self.logger.info(f"Cancelled agent run {run_id}")
|
|
701
|
+
|
|
702
|
+
# Remove from in-memory tracking
|
|
703
|
+
with self._running_agents_lock:
|
|
704
|
+
self._running_agents.pop(run_id, None)
|
|
705
|
+
|
|
706
|
+
return True
|
|
707
|
+
|
|
708
|
+
# -------------------------------------------------------------------------
|
|
709
|
+
# In-memory Running Agents Management
|
|
710
|
+
# -------------------------------------------------------------------------
|
|
711
|
+
|
|
712
|
+
def _track_running_agent(
|
|
713
|
+
self,
|
|
714
|
+
run_id: str,
|
|
715
|
+
parent_session_id: str | None,
|
|
716
|
+
child_session_id: str,
|
|
717
|
+
provider: str,
|
|
718
|
+
prompt: str,
|
|
719
|
+
mode: str = "in_process",
|
|
720
|
+
workflow_name: str | None = None,
|
|
721
|
+
model: str | None = None,
|
|
722
|
+
worktree_id: str | None = None,
|
|
723
|
+
pid: int | None = None,
|
|
724
|
+
terminal_type: str | None = None,
|
|
725
|
+
master_fd: int | None = None,
|
|
726
|
+
) -> RunningAgent:
|
|
727
|
+
"""
|
|
728
|
+
Add an agent to the in-memory running agents dict.
|
|
729
|
+
|
|
730
|
+
Thread-safe operation using a lock.
|
|
731
|
+
|
|
732
|
+
Args:
|
|
733
|
+
run_id: The agent run ID.
|
|
734
|
+
parent_session_id: Session that spawned this agent.
|
|
735
|
+
child_session_id: Child session created for this agent.
|
|
736
|
+
provider: LLM provider.
|
|
737
|
+
prompt: The task prompt (not stored, kept for API compatibility).
|
|
738
|
+
mode: Execution mode.
|
|
739
|
+
workflow_name: Workflow being executed.
|
|
740
|
+
model: Model override (not stored, kept for API compatibility).
|
|
741
|
+
worktree_id: Worktree being used.
|
|
742
|
+
pid: Process ID for terminal/headless mode.
|
|
743
|
+
terminal_type: Terminal type for terminal mode.
|
|
744
|
+
master_fd: PTY master file descriptor for embedded mode.
|
|
745
|
+
|
|
746
|
+
Returns:
|
|
747
|
+
The created RunningAgent instance.
|
|
748
|
+
"""
|
|
749
|
+
# Note: The registry's RunningAgent uses 'session_id' for child session
|
|
750
|
+
# and doesn't store prompt/model (those are in the database AgentRun record)
|
|
751
|
+
_ = prompt # Kept for API compatibility, stored in AgentRun
|
|
752
|
+
_ = model # Kept for API compatibility, stored in AgentRun
|
|
753
|
+
running_agent = RunningAgent(
|
|
754
|
+
run_id=run_id,
|
|
755
|
+
session_id=child_session_id,
|
|
756
|
+
parent_session_id=parent_session_id if parent_session_id else "",
|
|
757
|
+
mode=mode,
|
|
758
|
+
provider=provider,
|
|
759
|
+
workflow_name=workflow_name,
|
|
760
|
+
worktree_id=worktree_id,
|
|
761
|
+
pid=pid,
|
|
762
|
+
terminal_type=terminal_type,
|
|
763
|
+
master_fd=master_fd,
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
with self._running_agents_lock:
|
|
767
|
+
self._running_agents[run_id] = running_agent
|
|
768
|
+
|
|
769
|
+
self.logger.debug(f"Tracking running agent {run_id} (mode={mode})")
|
|
770
|
+
return running_agent
|
|
771
|
+
|
|
772
|
+
def _untrack_running_agent(self, run_id: str) -> RunningAgent | None:
|
|
773
|
+
"""
|
|
774
|
+
Remove an agent from the in-memory running agents dict.
|
|
775
|
+
|
|
776
|
+
Thread-safe operation using a lock.
|
|
777
|
+
|
|
778
|
+
Args:
|
|
779
|
+
run_id: The agent run ID to remove.
|
|
780
|
+
|
|
781
|
+
Returns:
|
|
782
|
+
The removed RunningAgent, or None if not found.
|
|
783
|
+
"""
|
|
784
|
+
with self._running_agents_lock:
|
|
785
|
+
agent = self._running_agents.pop(run_id, None)
|
|
786
|
+
|
|
787
|
+
if agent:
|
|
788
|
+
self.logger.debug(f"Untracked running agent {run_id}")
|
|
789
|
+
return agent
|
|
790
|
+
|
|
791
|
+
def _update_running_agent(
|
|
792
|
+
self,
|
|
793
|
+
run_id: str,
|
|
794
|
+
turns_used: int | None = None,
|
|
795
|
+
tool_calls_count: int | None = None,
|
|
796
|
+
) -> RunningAgent | None:
|
|
797
|
+
"""
|
|
798
|
+
Update in-memory state for a running agent.
|
|
799
|
+
|
|
800
|
+
Thread-safe operation using a lock.
|
|
801
|
+
|
|
802
|
+
Note: The registry's RunningAgent is lightweight and doesn't track
|
|
803
|
+
turns_used, tool_calls_count, or last_activity. These are tracked
|
|
804
|
+
in the database AgentRun record instead. This method verifies the
|
|
805
|
+
agent exists but doesn't modify it.
|
|
806
|
+
|
|
807
|
+
Args:
|
|
808
|
+
run_id: The agent run ID.
|
|
809
|
+
turns_used: Updated turns count (logged but not stored in-memory).
|
|
810
|
+
tool_calls_count: Updated tool calls count (logged but not stored in-memory).
|
|
811
|
+
|
|
812
|
+
Returns:
|
|
813
|
+
The RunningAgent if found, None otherwise.
|
|
814
|
+
"""
|
|
815
|
+
_ = turns_used # Tracked in database AgentRun record
|
|
816
|
+
_ = tool_calls_count # Tracked in database AgentRun record
|
|
817
|
+
with self._running_agents_lock:
|
|
818
|
+
agent = self._running_agents.get(run_id)
|
|
819
|
+
|
|
820
|
+
return agent
|
|
821
|
+
|
|
822
|
+
def get_running_agent(self, run_id: str) -> RunningAgent | None:
|
|
823
|
+
"""
|
|
824
|
+
Get a running agent by ID.
|
|
825
|
+
|
|
826
|
+
Thread-safe operation using a lock.
|
|
827
|
+
|
|
828
|
+
Args:
|
|
829
|
+
run_id: The agent run ID.
|
|
830
|
+
|
|
831
|
+
Returns:
|
|
832
|
+
The RunningAgent if found and running, None otherwise.
|
|
833
|
+
"""
|
|
834
|
+
with self._running_agents_lock:
|
|
835
|
+
return self._running_agents.get(run_id)
|
|
836
|
+
|
|
837
|
+
def get_running_agents(
|
|
838
|
+
self,
|
|
839
|
+
parent_session_id: str | None = None,
|
|
840
|
+
) -> list[RunningAgent]:
|
|
841
|
+
"""
|
|
842
|
+
Get all running agents, optionally filtered by parent session.
|
|
843
|
+
|
|
844
|
+
Thread-safe operation using a lock.
|
|
845
|
+
|
|
846
|
+
Args:
|
|
847
|
+
parent_session_id: Optional filter by parent session.
|
|
848
|
+
|
|
849
|
+
Returns:
|
|
850
|
+
List of running agents.
|
|
851
|
+
"""
|
|
852
|
+
with self._running_agents_lock:
|
|
853
|
+
agents = list(self._running_agents.values())
|
|
854
|
+
|
|
855
|
+
if parent_session_id:
|
|
856
|
+
agents = [a for a in agents if a.parent_session_id == parent_session_id]
|
|
857
|
+
|
|
858
|
+
return agents
|
|
859
|
+
|
|
860
|
+
def get_running_agents_count(self) -> int:
|
|
861
|
+
"""
|
|
862
|
+
Get count of running agents.
|
|
863
|
+
|
|
864
|
+
Thread-safe operation using a lock.
|
|
865
|
+
|
|
866
|
+
Returns:
|
|
867
|
+
Number of running agents.
|
|
868
|
+
"""
|
|
869
|
+
with self._running_agents_lock:
|
|
870
|
+
return len(self._running_agents)
|
|
871
|
+
|
|
872
|
+
def is_agent_running(self, run_id: str) -> bool:
|
|
873
|
+
"""
|
|
874
|
+
Check if an agent is currently running.
|
|
875
|
+
|
|
876
|
+
Thread-safe operation using a lock.
|
|
877
|
+
|
|
878
|
+
Args:
|
|
879
|
+
run_id: The agent run ID.
|
|
880
|
+
|
|
881
|
+
Returns:
|
|
882
|
+
True if the agent is in the running dict.
|
|
883
|
+
"""
|
|
884
|
+
with self._running_agents_lock:
|
|
885
|
+
return run_id in self._running_agents
|
|
886
|
+
|
|
887
|
+
def _create_workflow_filtered_handler(
|
|
888
|
+
self,
|
|
889
|
+
base_handler: ToolHandler,
|
|
890
|
+
session_id: str,
|
|
891
|
+
workflow_definition: WorkflowDefinition,
|
|
892
|
+
) -> ToolHandler:
|
|
893
|
+
"""
|
|
894
|
+
Create a tool handler that enforces workflow tool restrictions.
|
|
895
|
+
|
|
896
|
+
Args:
|
|
897
|
+
base_handler: The underlying tool handler to call for allowed tools.
|
|
898
|
+
session_id: Session ID for looking up workflow state.
|
|
899
|
+
workflow_definition: The workflow definition with step restrictions.
|
|
900
|
+
|
|
901
|
+
Returns:
|
|
902
|
+
An async callable that filters tools based on workflow state.
|
|
903
|
+
"""
|
|
904
|
+
|
|
905
|
+
async def filtered_handler(tool_name: str, arguments: dict[str, Any]) -> ToolResult:
|
|
906
|
+
# Get current workflow state
|
|
907
|
+
state = self._workflow_state_manager.get_state(session_id)
|
|
908
|
+
if not state:
|
|
909
|
+
# No state - just pass through
|
|
910
|
+
return await base_handler(tool_name, arguments)
|
|
911
|
+
|
|
912
|
+
# Get current step
|
|
913
|
+
current_step = workflow_definition.get_step(state.step)
|
|
914
|
+
if not current_step:
|
|
915
|
+
# No step defined - pass through
|
|
916
|
+
return await base_handler(tool_name, arguments)
|
|
917
|
+
|
|
918
|
+
# Check blocked_tools first (explicit deny)
|
|
919
|
+
if tool_name in current_step.blocked_tools:
|
|
920
|
+
self.logger.warning(f"Tool '{tool_name}' blocked by workflow step '{state.step}'")
|
|
921
|
+
return ToolResult(
|
|
922
|
+
tool_name=tool_name,
|
|
923
|
+
success=False,
|
|
924
|
+
error=f"Tool '{tool_name}' is blocked in workflow step '{state.step}'",
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
# Check allowed_tools (if not "all")
|
|
928
|
+
if current_step.allowed_tools != "all":
|
|
929
|
+
if tool_name not in current_step.allowed_tools:
|
|
930
|
+
self.logger.warning(
|
|
931
|
+
f"Tool '{tool_name}' not allowed in workflow step '{state.step}'"
|
|
932
|
+
)
|
|
933
|
+
return ToolResult(
|
|
934
|
+
tool_name=tool_name,
|
|
935
|
+
success=False,
|
|
936
|
+
error=(
|
|
937
|
+
f"Tool '{tool_name}' is not allowed in workflow step "
|
|
938
|
+
f"'{state.step}'. Allowed tools: {current_step.allowed_tools}"
|
|
939
|
+
),
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
# Handle 'complete' tool as workflow exit condition
|
|
943
|
+
if tool_name == "complete":
|
|
944
|
+
result_message = arguments.get("result", "Task completed")
|
|
945
|
+
self.logger.info(
|
|
946
|
+
f"Agent called 'complete' tool - workflow exit condition met "
|
|
947
|
+
f"(session={session_id}, step={state.step})"
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
# Update workflow state to indicate completion
|
|
951
|
+
state.variables["workflow_completed"] = True
|
|
952
|
+
state.variables["completion_result"] = result_message
|
|
953
|
+
self._workflow_state_manager.save_state(state)
|
|
954
|
+
|
|
955
|
+
return ToolResult(
|
|
956
|
+
tool_name=tool_name,
|
|
957
|
+
success=True,
|
|
958
|
+
result={
|
|
959
|
+
"status": "completed",
|
|
960
|
+
"message": result_message,
|
|
961
|
+
"step": state.step,
|
|
962
|
+
},
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
# Tool is allowed - pass through to base handler
|
|
966
|
+
return await base_handler(tool_name, arguments)
|
|
967
|
+
|
|
968
|
+
return filtered_handler
|