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/registry.py
ADDED
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
"""
|
|
2
|
+
In-memory registry for tracking running agent processes.
|
|
3
|
+
|
|
4
|
+
This module provides thread-safe tracking of running agents that complements
|
|
5
|
+
the database storage. It tracks runtime information like PIDs and process handles
|
|
6
|
+
that shouldn't be persisted.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
import threading
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from datetime import UTC, datetime
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# Event callback type - (event_type, run_id, data)
|
|
22
|
+
EventCallback = Callable[[str, str, dict[str, Any]], None]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class RunningAgent:
|
|
27
|
+
"""
|
|
28
|
+
In-memory record of a running agent process.
|
|
29
|
+
|
|
30
|
+
Tracks runtime state that isn't appropriate for database storage.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
run_id: str
|
|
34
|
+
"""Agent run ID (matches database record)."""
|
|
35
|
+
|
|
36
|
+
session_id: str
|
|
37
|
+
"""Child session ID for this agent."""
|
|
38
|
+
|
|
39
|
+
parent_session_id: str
|
|
40
|
+
"""Parent session that spawned this agent."""
|
|
41
|
+
|
|
42
|
+
mode: str
|
|
43
|
+
"""Execution mode: in_process, terminal, embedded, headless."""
|
|
44
|
+
|
|
45
|
+
started_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
46
|
+
"""When the agent started running."""
|
|
47
|
+
|
|
48
|
+
# Process tracking (for terminal/embedded/headless modes)
|
|
49
|
+
pid: int | None = None
|
|
50
|
+
"""Process ID if running externally."""
|
|
51
|
+
|
|
52
|
+
master_fd: int | None = None
|
|
53
|
+
"""PTY master file descriptor (embedded mode only)."""
|
|
54
|
+
|
|
55
|
+
terminal_type: str | None = None
|
|
56
|
+
"""Terminal type (ghostty, iterm, etc.) for terminal mode."""
|
|
57
|
+
|
|
58
|
+
# State tracking
|
|
59
|
+
provider: str = "claude"
|
|
60
|
+
"""LLM provider being used."""
|
|
61
|
+
|
|
62
|
+
workflow_name: str | None = None
|
|
63
|
+
"""Workflow being executed, if any."""
|
|
64
|
+
|
|
65
|
+
worktree_id: str | None = None
|
|
66
|
+
"""Associated worktree, if any."""
|
|
67
|
+
|
|
68
|
+
# In-process agent tracking
|
|
69
|
+
task: Any | None = None
|
|
70
|
+
"""Async task object for in-process agents (asyncio.Task)."""
|
|
71
|
+
|
|
72
|
+
monitor_task: Any | None = None
|
|
73
|
+
"""Background monitoring task for headless agents (asyncio.Task)."""
|
|
74
|
+
|
|
75
|
+
def to_dict(self) -> dict[str, Any]:
|
|
76
|
+
"""Convert to dictionary for serialization."""
|
|
77
|
+
return {
|
|
78
|
+
"run_id": self.run_id,
|
|
79
|
+
"session_id": self.session_id,
|
|
80
|
+
"parent_session_id": self.parent_session_id,
|
|
81
|
+
"mode": self.mode,
|
|
82
|
+
"started_at": self.started_at.isoformat(),
|
|
83
|
+
"pid": self.pid,
|
|
84
|
+
"master_fd": self.master_fd,
|
|
85
|
+
"terminal_type": self.terminal_type,
|
|
86
|
+
"provider": self.provider,
|
|
87
|
+
"workflow_name": self.workflow_name,
|
|
88
|
+
"worktree_id": self.worktree_id,
|
|
89
|
+
"has_task": self.task is not None,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class RunningAgentRegistry:
|
|
94
|
+
"""
|
|
95
|
+
Thread-safe registry for tracking running agents.
|
|
96
|
+
|
|
97
|
+
This registry tracks agents that are currently executing, whether
|
|
98
|
+
in-process or in external processes (terminal/headless). It provides:
|
|
99
|
+
|
|
100
|
+
- Thread-safe add/get/remove operations
|
|
101
|
+
- Lookup by run_id, session_id, or parent_session_id
|
|
102
|
+
- PID-based lookup for process management
|
|
103
|
+
- Cleanup of stale entries
|
|
104
|
+
|
|
105
|
+
Example:
|
|
106
|
+
>>> registry = RunningAgentRegistry()
|
|
107
|
+
>>> agent = RunningAgent(
|
|
108
|
+
... run_id="ar-123",
|
|
109
|
+
... session_id="sess-456",
|
|
110
|
+
... parent_session_id="sess-parent",
|
|
111
|
+
... mode="terminal",
|
|
112
|
+
... pid=12345,
|
|
113
|
+
... )
|
|
114
|
+
>>> registry.add(agent)
|
|
115
|
+
>>> registry.get("ar-123")
|
|
116
|
+
RunningAgent(...)
|
|
117
|
+
>>> registry.remove("ar-123")
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def __init__(self) -> None:
|
|
121
|
+
"""Initialize the registry with an empty agents dict and lock."""
|
|
122
|
+
self._agents: dict[str, RunningAgent] = {}
|
|
123
|
+
self._lock = threading.RLock()
|
|
124
|
+
self._logger = logger
|
|
125
|
+
self._event_callbacks: list[EventCallback] = []
|
|
126
|
+
self._event_callbacks_lock = threading.Lock()
|
|
127
|
+
|
|
128
|
+
def add_event_callback(self, callback: EventCallback) -> None:
|
|
129
|
+
"""
|
|
130
|
+
Add an event callback for agent lifecycle events.
|
|
131
|
+
|
|
132
|
+
Callbacks are invoked when agents are added or removed.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
callback: Function that receives (event_type, run_id, data)
|
|
136
|
+
"""
|
|
137
|
+
with self._event_callbacks_lock:
|
|
138
|
+
self._event_callbacks.append(callback)
|
|
139
|
+
|
|
140
|
+
def _emit_event(self, event_type: str, run_id: str, data: dict[str, Any]) -> None:
|
|
141
|
+
"""
|
|
142
|
+
Emit an event to all registered callbacks.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
event_type: Type of event (agent_started, agent_completed, etc.)
|
|
146
|
+
run_id: Agent run ID
|
|
147
|
+
data: Additional event data
|
|
148
|
+
"""
|
|
149
|
+
# Take a snapshot of callbacks under lock, then iterate outside lock
|
|
150
|
+
with self._event_callbacks_lock:
|
|
151
|
+
callbacks = list(self._event_callbacks)
|
|
152
|
+
for callback in callbacks:
|
|
153
|
+
try:
|
|
154
|
+
callback(event_type, run_id, data)
|
|
155
|
+
except Exception as e:
|
|
156
|
+
self._logger.warning(f"Event callback error: {e}")
|
|
157
|
+
|
|
158
|
+
def add(self, agent: RunningAgent) -> None:
|
|
159
|
+
"""
|
|
160
|
+
Add a running agent to the registry.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
agent: The running agent to track.
|
|
164
|
+
"""
|
|
165
|
+
with self._lock:
|
|
166
|
+
self._agents[agent.run_id] = agent
|
|
167
|
+
self._logger.debug(
|
|
168
|
+
f"Registered running agent {agent.run_id} (mode={agent.mode}, pid={agent.pid})"
|
|
169
|
+
)
|
|
170
|
+
# Emit event outside lock
|
|
171
|
+
self._emit_event(
|
|
172
|
+
"agent_started",
|
|
173
|
+
agent.run_id,
|
|
174
|
+
{
|
|
175
|
+
"session_id": agent.session_id,
|
|
176
|
+
"parent_session_id": agent.parent_session_id,
|
|
177
|
+
"mode": agent.mode,
|
|
178
|
+
"provider": agent.provider,
|
|
179
|
+
"pid": agent.pid,
|
|
180
|
+
},
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def get(self, run_id: str) -> RunningAgent | None:
|
|
184
|
+
"""
|
|
185
|
+
Get a running agent by run ID.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
run_id: The agent run ID.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
The RunningAgent if found, None otherwise.
|
|
192
|
+
"""
|
|
193
|
+
with self._lock:
|
|
194
|
+
return self._agents.get(run_id)
|
|
195
|
+
|
|
196
|
+
def remove(self, run_id: str, status: str = "completed") -> RunningAgent | None:
|
|
197
|
+
"""
|
|
198
|
+
Remove a running agent from the registry.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
run_id: The agent run ID to remove.
|
|
202
|
+
status: Final status (completed, failed, cancelled, timeout).
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
The removed RunningAgent if found, None otherwise.
|
|
206
|
+
"""
|
|
207
|
+
with self._lock:
|
|
208
|
+
agent = self._agents.pop(run_id, None)
|
|
209
|
+
if agent:
|
|
210
|
+
self._logger.debug(f"Unregistered running agent {run_id}")
|
|
211
|
+
# Emit event outside lock
|
|
212
|
+
if agent:
|
|
213
|
+
self._emit_event(
|
|
214
|
+
f"agent_{status}",
|
|
215
|
+
run_id,
|
|
216
|
+
{
|
|
217
|
+
"session_id": agent.session_id,
|
|
218
|
+
"parent_session_id": agent.parent_session_id,
|
|
219
|
+
"mode": agent.mode,
|
|
220
|
+
"provider": agent.provider,
|
|
221
|
+
},
|
|
222
|
+
)
|
|
223
|
+
return agent
|
|
224
|
+
|
|
225
|
+
def kill(
|
|
226
|
+
self,
|
|
227
|
+
run_id: str,
|
|
228
|
+
signal_name: str = "TERM",
|
|
229
|
+
timeout: float = 5.0,
|
|
230
|
+
) -> dict[str, Any]:
|
|
231
|
+
"""
|
|
232
|
+
Kill a running agent process.
|
|
233
|
+
|
|
234
|
+
Strategy varies by mode:
|
|
235
|
+
- headless: Direct signal to tracked PID
|
|
236
|
+
- terminal: Check terminal_context for PID, fallback to pgrep
|
|
237
|
+
- embedded: Close PTY fd + signal
|
|
238
|
+
- in_process: Cancel asyncio task
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
run_id: Agent run ID
|
|
242
|
+
signal_name: Signal without SIG prefix (TERM, KILL)
|
|
243
|
+
timeout: Seconds before escalating TERM → KILL
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Dict with success status and details
|
|
247
|
+
"""
|
|
248
|
+
import os
|
|
249
|
+
import signal
|
|
250
|
+
import subprocess # nosec B404 - subprocess needed for process management
|
|
251
|
+
import time
|
|
252
|
+
|
|
253
|
+
agent = self.get(run_id)
|
|
254
|
+
if not agent:
|
|
255
|
+
return {"success": False, "error": "Agent not found in registry"}
|
|
256
|
+
|
|
257
|
+
# Handle in_process mode (asyncio.Task)
|
|
258
|
+
if agent.mode == "in_process" and agent.task:
|
|
259
|
+
agent.task.cancel()
|
|
260
|
+
self.remove(run_id, status="cancelled")
|
|
261
|
+
return {"success": True, "message": "Cancelled in-process task"}
|
|
262
|
+
|
|
263
|
+
# For terminal mode, find PID via multiple strategies
|
|
264
|
+
target_pid = agent.pid
|
|
265
|
+
found_via = "registry"
|
|
266
|
+
|
|
267
|
+
if agent.mode == "terminal" and agent.session_id:
|
|
268
|
+
# Strategy 1: Check session's terminal_context (Claude hooks)
|
|
269
|
+
try:
|
|
270
|
+
from gobby.storage.database import LocalDatabase
|
|
271
|
+
from gobby.storage.sessions import LocalSessionManager
|
|
272
|
+
|
|
273
|
+
db = LocalDatabase()
|
|
274
|
+
session_mgr = LocalSessionManager(db)
|
|
275
|
+
session = session_mgr.get(agent.session_id)
|
|
276
|
+
if session and session.terminal_context:
|
|
277
|
+
ctx_pid = session.terminal_context.get("parent_pid")
|
|
278
|
+
if ctx_pid:
|
|
279
|
+
target_pid = int(ctx_pid)
|
|
280
|
+
found_via = "terminal_context"
|
|
281
|
+
self._logger.info(f"Found PID from session terminal_context: {target_pid}")
|
|
282
|
+
except Exception as e:
|
|
283
|
+
self._logger.debug(f"terminal_context lookup failed: {e}")
|
|
284
|
+
|
|
285
|
+
# Strategy 2: pgrep fallback (for Codex/Gemini without hooks)
|
|
286
|
+
if found_via == "registry" or not target_pid:
|
|
287
|
+
# Validate session_id format (UUID or safe identifier) to prevent injection
|
|
288
|
+
session_id_pattern = re.compile(r"^[a-zA-Z0-9_-]+$")
|
|
289
|
+
if not session_id_pattern.match(agent.session_id):
|
|
290
|
+
self._logger.warning(
|
|
291
|
+
f"Invalid session_id format, skipping pgrep: {agent.session_id}"
|
|
292
|
+
)
|
|
293
|
+
else:
|
|
294
|
+
try:
|
|
295
|
+
# Use -- to prevent pgrep from interpreting pattern as options
|
|
296
|
+
result = subprocess.run( # nosec B603 B607 - pgrep with validated session_id
|
|
297
|
+
["pgrep", "-f", "--", f"session-id {agent.session_id}"],
|
|
298
|
+
capture_output=True,
|
|
299
|
+
text=True,
|
|
300
|
+
timeout=5.0,
|
|
301
|
+
)
|
|
302
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
303
|
+
pids = result.stdout.strip().split("\n")
|
|
304
|
+
if len(pids) == 1:
|
|
305
|
+
target_pid = int(pids[0])
|
|
306
|
+
found_via = "pgrep"
|
|
307
|
+
self._logger.info(f"Found PID via pgrep: {target_pid}")
|
|
308
|
+
else:
|
|
309
|
+
# Multiple PIDs found - need to disambiguate
|
|
310
|
+
self._logger.warning(
|
|
311
|
+
f"pgrep returned {len(pids)} PIDs for session "
|
|
312
|
+
f"{agent.session_id}: {pids}"
|
|
313
|
+
)
|
|
314
|
+
# Inspect each candidate to find the correct one
|
|
315
|
+
matched_pid = None
|
|
316
|
+
for pid_str in pids:
|
|
317
|
+
try:
|
|
318
|
+
candidate_pid = int(pid_str)
|
|
319
|
+
# Query the process command line to verify
|
|
320
|
+
ps_result = subprocess.run( # nosec B603 B607 - ps with numeric PID
|
|
321
|
+
["ps", "-p", str(candidate_pid), "-o", "args="],
|
|
322
|
+
capture_output=True,
|
|
323
|
+
text=True,
|
|
324
|
+
timeout=2.0,
|
|
325
|
+
)
|
|
326
|
+
if ps_result.returncode == 0:
|
|
327
|
+
cmdline = ps_result.stdout.strip()
|
|
328
|
+
# Verify it's actually the agent process
|
|
329
|
+
# (contains session-id and matches expected CLI)
|
|
330
|
+
if (
|
|
331
|
+
f"session-id {agent.session_id}" in cmdline
|
|
332
|
+
and agent.provider in cmdline.lower()
|
|
333
|
+
):
|
|
334
|
+
if matched_pid is not None:
|
|
335
|
+
# Multiple matches - ambiguous
|
|
336
|
+
self._logger.error(
|
|
337
|
+
f"Ambiguous PID match: both {matched_pid} "
|
|
338
|
+
f"and {candidate_pid} match session "
|
|
339
|
+
f"{agent.session_id}"
|
|
340
|
+
)
|
|
341
|
+
matched_pid = None
|
|
342
|
+
break
|
|
343
|
+
matched_pid = candidate_pid
|
|
344
|
+
except (ValueError, subprocess.TimeoutExpired):
|
|
345
|
+
continue
|
|
346
|
+
if matched_pid is not None:
|
|
347
|
+
target_pid = matched_pid
|
|
348
|
+
found_via = "pgrep_disambiguated"
|
|
349
|
+
self._logger.info(
|
|
350
|
+
f"Disambiguated PID via ps inspection: {target_pid}"
|
|
351
|
+
)
|
|
352
|
+
else:
|
|
353
|
+
self._logger.error(
|
|
354
|
+
f"Could not disambiguate PIDs for session "
|
|
355
|
+
f"{agent.session_id}: {pids}"
|
|
356
|
+
)
|
|
357
|
+
except Exception as e:
|
|
358
|
+
self._logger.warning(f"pgrep fallback failed: {e}")
|
|
359
|
+
|
|
360
|
+
if not target_pid:
|
|
361
|
+
return {"success": False, "error": "No target PID found"}
|
|
362
|
+
|
|
363
|
+
# Check if process is alive
|
|
364
|
+
try:
|
|
365
|
+
os.kill(target_pid, 0)
|
|
366
|
+
except ProcessLookupError:
|
|
367
|
+
self.remove(run_id, status="completed")
|
|
368
|
+
return {
|
|
369
|
+
"success": True,
|
|
370
|
+
"message": f"Process {target_pid} already dead",
|
|
371
|
+
"already_dead": True,
|
|
372
|
+
}
|
|
373
|
+
except PermissionError:
|
|
374
|
+
return {"success": False, "error": f"No permission to signal PID {target_pid}"}
|
|
375
|
+
|
|
376
|
+
# Close PTY if embedded mode
|
|
377
|
+
if agent.master_fd is not None:
|
|
378
|
+
try:
|
|
379
|
+
os.close(agent.master_fd)
|
|
380
|
+
except OSError:
|
|
381
|
+
pass
|
|
382
|
+
|
|
383
|
+
# Send signal
|
|
384
|
+
sig = getattr(signal, f"SIG{signal_name}", signal.SIGTERM)
|
|
385
|
+
try:
|
|
386
|
+
os.kill(target_pid, sig)
|
|
387
|
+
except ProcessLookupError:
|
|
388
|
+
self.remove(run_id, status="completed")
|
|
389
|
+
return {
|
|
390
|
+
"success": True,
|
|
391
|
+
"message": "Process died during signal",
|
|
392
|
+
"already_dead": True,
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
# Wait for termination with optional SIGKILL escalation
|
|
396
|
+
if signal_name == "TERM" and timeout > 0:
|
|
397
|
+
deadline = time.time() + timeout
|
|
398
|
+
while time.time() < deadline:
|
|
399
|
+
try:
|
|
400
|
+
os.kill(target_pid, 0)
|
|
401
|
+
time.sleep(0.1)
|
|
402
|
+
except ProcessLookupError:
|
|
403
|
+
break
|
|
404
|
+
else:
|
|
405
|
+
# Still alive - escalate to SIGKILL
|
|
406
|
+
try:
|
|
407
|
+
os.kill(target_pid, signal.SIGKILL)
|
|
408
|
+
self._logger.info(f"Escalated to SIGKILL for PID {target_pid}")
|
|
409
|
+
except ProcessLookupError:
|
|
410
|
+
pass
|
|
411
|
+
|
|
412
|
+
self.remove(run_id, status="killed")
|
|
413
|
+
return {
|
|
414
|
+
"success": True,
|
|
415
|
+
"message": f"Sent SIG{signal_name} to PID {target_pid}",
|
|
416
|
+
"pid": target_pid,
|
|
417
|
+
"signal": signal_name,
|
|
418
|
+
"found_via": found_via,
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
def get_by_session(self, session_id: str) -> RunningAgent | None:
|
|
422
|
+
"""
|
|
423
|
+
Get a running agent by its child session ID.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
session_id: The child session ID.
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
The RunningAgent if found, None otherwise.
|
|
430
|
+
"""
|
|
431
|
+
with self._lock:
|
|
432
|
+
for agent in self._agents.values():
|
|
433
|
+
if agent.session_id == session_id:
|
|
434
|
+
return agent
|
|
435
|
+
return None
|
|
436
|
+
|
|
437
|
+
def get_by_pid(self, pid: int) -> RunningAgent | None:
|
|
438
|
+
"""
|
|
439
|
+
Get a running agent by its process ID.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
pid: The process ID.
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
The RunningAgent if found, None otherwise.
|
|
446
|
+
"""
|
|
447
|
+
with self._lock:
|
|
448
|
+
for agent in self._agents.values():
|
|
449
|
+
if agent.pid == pid:
|
|
450
|
+
return agent
|
|
451
|
+
return None
|
|
452
|
+
|
|
453
|
+
def list_by_parent(self, parent_session_id: str) -> list[RunningAgent]:
|
|
454
|
+
"""
|
|
455
|
+
List all running agents for a parent session.
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
parent_session_id: The parent session ID.
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
List of running agents spawned by this parent.
|
|
462
|
+
"""
|
|
463
|
+
with self._lock:
|
|
464
|
+
return [
|
|
465
|
+
agent
|
|
466
|
+
for agent in self._agents.values()
|
|
467
|
+
if agent.parent_session_id == parent_session_id
|
|
468
|
+
]
|
|
469
|
+
|
|
470
|
+
def list_by_mode(self, mode: str) -> list[RunningAgent]:
|
|
471
|
+
"""
|
|
472
|
+
List all running agents by execution mode.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
mode: Execution mode (in_process, terminal, embedded, headless).
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
List of running agents with this mode.
|
|
479
|
+
"""
|
|
480
|
+
with self._lock:
|
|
481
|
+
return [agent for agent in self._agents.values() if agent.mode == mode]
|
|
482
|
+
|
|
483
|
+
def list_all(self) -> list[RunningAgent]:
|
|
484
|
+
"""
|
|
485
|
+
List all running agents.
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
List of all running agents (copy of current state).
|
|
489
|
+
"""
|
|
490
|
+
with self._lock:
|
|
491
|
+
return list(self._agents.values())
|
|
492
|
+
|
|
493
|
+
def count(self) -> int:
|
|
494
|
+
"""
|
|
495
|
+
Get the number of running agents.
|
|
496
|
+
|
|
497
|
+
Returns:
|
|
498
|
+
Count of running agents.
|
|
499
|
+
"""
|
|
500
|
+
with self._lock:
|
|
501
|
+
return len(self._agents)
|
|
502
|
+
|
|
503
|
+
def count_by_parent(self, parent_session_id: str) -> int:
|
|
504
|
+
"""
|
|
505
|
+
Count running agents for a parent session.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
parent_session_id: The parent session ID.
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
Count of running agents for this parent.
|
|
512
|
+
"""
|
|
513
|
+
with self._lock:
|
|
514
|
+
return sum(
|
|
515
|
+
1 for agent in self._agents.values() if agent.parent_session_id == parent_session_id
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
def cleanup_by_pids(self, dead_pids: set[int]) -> list[RunningAgent]:
|
|
519
|
+
"""
|
|
520
|
+
Remove agents whose PIDs are no longer running.
|
|
521
|
+
|
|
522
|
+
This should be called periodically by a cleanup process that
|
|
523
|
+
checks which PIDs are still alive.
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
dead_pids: Set of PIDs that are no longer running.
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
List of agents that were removed.
|
|
530
|
+
"""
|
|
531
|
+
removed: list[RunningAgent] = []
|
|
532
|
+
with self._lock:
|
|
533
|
+
for run_id, agent in list(self._agents.items()):
|
|
534
|
+
if agent.pid and agent.pid in dead_pids:
|
|
535
|
+
self._agents.pop(run_id)
|
|
536
|
+
removed.append(agent)
|
|
537
|
+
self._logger.info(f"Cleaned up agent {run_id} with dead PID {agent.pid}")
|
|
538
|
+
# Emit events outside lock for each removed agent
|
|
539
|
+
for agent in removed:
|
|
540
|
+
self._emit_event(
|
|
541
|
+
"agent_completed",
|
|
542
|
+
agent.run_id,
|
|
543
|
+
{
|
|
544
|
+
"session_id": agent.session_id,
|
|
545
|
+
"parent_session_id": agent.parent_session_id,
|
|
546
|
+
"mode": agent.mode,
|
|
547
|
+
"provider": agent.provider,
|
|
548
|
+
"cleanup_reason": "dead_pid",
|
|
549
|
+
},
|
|
550
|
+
)
|
|
551
|
+
return removed
|
|
552
|
+
|
|
553
|
+
def cleanup_stale(self, max_age_seconds: float = 3600.0) -> list[RunningAgent]:
|
|
554
|
+
"""
|
|
555
|
+
Remove agents that have been running longer than max_age.
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
max_age_seconds: Maximum age in seconds before cleanup (default: 1 hour).
|
|
559
|
+
|
|
560
|
+
Returns:
|
|
561
|
+
List of agents that were removed.
|
|
562
|
+
"""
|
|
563
|
+
now = datetime.now(UTC)
|
|
564
|
+
removed: list[RunningAgent] = []
|
|
565
|
+
with self._lock:
|
|
566
|
+
for run_id, agent in list(self._agents.items()):
|
|
567
|
+
age = (now - agent.started_at).total_seconds()
|
|
568
|
+
if age > max_age_seconds:
|
|
569
|
+
self._agents.pop(run_id)
|
|
570
|
+
removed.append(agent)
|
|
571
|
+
self._logger.info(f"Cleaned up stale agent {run_id} (age={age:.0f}s)")
|
|
572
|
+
# Emit events outside lock for each removed agent
|
|
573
|
+
for agent in removed:
|
|
574
|
+
self._emit_event(
|
|
575
|
+
"agent_timeout",
|
|
576
|
+
agent.run_id,
|
|
577
|
+
{
|
|
578
|
+
"session_id": agent.session_id,
|
|
579
|
+
"parent_session_id": agent.parent_session_id,
|
|
580
|
+
"mode": agent.mode,
|
|
581
|
+
"provider": agent.provider,
|
|
582
|
+
"cleanup_reason": "stale",
|
|
583
|
+
},
|
|
584
|
+
)
|
|
585
|
+
return removed
|
|
586
|
+
|
|
587
|
+
def clear(self) -> int:
|
|
588
|
+
"""
|
|
589
|
+
Clear all running agents from the registry.
|
|
590
|
+
|
|
591
|
+
Returns:
|
|
592
|
+
Number of agents that were cleared.
|
|
593
|
+
"""
|
|
594
|
+
with self._lock:
|
|
595
|
+
count = len(self._agents)
|
|
596
|
+
self._agents.clear()
|
|
597
|
+
self._logger.info(f"Cleared {count} running agents from registry")
|
|
598
|
+
return count
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
# Global singleton instance
|
|
602
|
+
_default_registry: RunningAgentRegistry | None = None
|
|
603
|
+
_registry_lock = threading.Lock()
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def get_running_agent_registry() -> RunningAgentRegistry:
|
|
607
|
+
"""
|
|
608
|
+
Get the global running agent registry singleton.
|
|
609
|
+
|
|
610
|
+
Returns:
|
|
611
|
+
The shared RunningAgentRegistry instance.
|
|
612
|
+
"""
|
|
613
|
+
global _default_registry
|
|
614
|
+
if _default_registry is None:
|
|
615
|
+
with _registry_lock:
|
|
616
|
+
if _default_registry is None:
|
|
617
|
+
_default_registry = RunningAgentRegistry()
|
|
618
|
+
return _default_registry
|