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/hooks/__init__.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gobby hooks package for Claude Code, Gemini CLI, and Codex integration.
|
|
3
|
+
|
|
4
|
+
This package provides a hook system for intercepting and processing events
|
|
5
|
+
from AI coding assistants. The architecture follows the Coordinator pattern:
|
|
6
|
+
|
|
7
|
+
Core Components:
|
|
8
|
+
HookManager: Main entry point and coordinator. Receives hook events and
|
|
9
|
+
delegates to specialized components.
|
|
10
|
+
|
|
11
|
+
EventHandlers: Contains all event handler implementations for the 15
|
|
12
|
+
supported event types (session, agent, tool, etc.)
|
|
13
|
+
|
|
14
|
+
SessionCoordinator: Manages session lifecycle - registration, lookup,
|
|
15
|
+
status tracking, and cleanup.
|
|
16
|
+
|
|
17
|
+
HealthMonitor: Background daemon health check monitoring with caching.
|
|
18
|
+
|
|
19
|
+
WebhookDispatcher: Dispatches hook events to external webhook endpoints.
|
|
20
|
+
|
|
21
|
+
Event Models:
|
|
22
|
+
HookEventType: Unified event type enum (15 types across all CLIs)
|
|
23
|
+
SessionSource: Enum identifying which CLI originated the session
|
|
24
|
+
HookEvent: Unified event dataclass from any CLI source
|
|
25
|
+
HookResponse: Unified response dataclass returned to CLIs
|
|
26
|
+
|
|
27
|
+
Plugin System:
|
|
28
|
+
HookPlugin: Base class for custom hook plugins
|
|
29
|
+
PluginLoader: Discovers and loads plugins from configured paths
|
|
30
|
+
hook_handler: Decorator for registering plugin handlers
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
```python
|
|
34
|
+
from gobby.hooks import HookManager, HookEvent, HookEventType
|
|
35
|
+
|
|
36
|
+
# Create manager (typically done once in daemon)
|
|
37
|
+
manager = HookManager()
|
|
38
|
+
|
|
39
|
+
# Handle incoming events
|
|
40
|
+
response = manager.handle(event)
|
|
41
|
+
```
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
# Core coordinator and components
|
|
45
|
+
# Artifact capture hook
|
|
46
|
+
from gobby.hooks.artifact_capture import ArtifactCaptureHook
|
|
47
|
+
from gobby.hooks.event_handlers import EventHandlers
|
|
48
|
+
from gobby.hooks.events import (
|
|
49
|
+
EVENT_TYPE_CLI_SUPPORT,
|
|
50
|
+
HookEvent,
|
|
51
|
+
HookEventType,
|
|
52
|
+
HookResponse,
|
|
53
|
+
SessionSource,
|
|
54
|
+
)
|
|
55
|
+
from gobby.hooks.health_monitor import HealthMonitor
|
|
56
|
+
from gobby.hooks.hook_manager import HookManager
|
|
57
|
+
from gobby.hooks.plugins import (
|
|
58
|
+
HookPlugin,
|
|
59
|
+
PluginLoader,
|
|
60
|
+
PluginRegistry,
|
|
61
|
+
RegisteredHandler,
|
|
62
|
+
hook_handler,
|
|
63
|
+
run_plugin_handlers,
|
|
64
|
+
)
|
|
65
|
+
from gobby.hooks.session_coordinator import SessionCoordinator
|
|
66
|
+
from gobby.hooks.webhooks import WebhookDispatcher
|
|
67
|
+
|
|
68
|
+
# Legacy imports for backward compatibility
|
|
69
|
+
from gobby.sessions.manager import SessionManager
|
|
70
|
+
from gobby.sessions.summary import SummaryFileGenerator
|
|
71
|
+
from gobby.sessions.transcripts.claude import ClaudeTranscriptParser
|
|
72
|
+
|
|
73
|
+
# Backward-compatible alias
|
|
74
|
+
TranscriptProcessor = ClaudeTranscriptParser
|
|
75
|
+
|
|
76
|
+
__all__ = [
|
|
77
|
+
# Core coordinator
|
|
78
|
+
"HookManager",
|
|
79
|
+
# Extracted components (for advanced usage/testing)
|
|
80
|
+
"EventHandlers",
|
|
81
|
+
"SessionCoordinator",
|
|
82
|
+
"HealthMonitor",
|
|
83
|
+
"WebhookDispatcher",
|
|
84
|
+
# Artifact capture
|
|
85
|
+
"ArtifactCaptureHook",
|
|
86
|
+
# Unified hook event models
|
|
87
|
+
"HookEventType",
|
|
88
|
+
"SessionSource",
|
|
89
|
+
"HookEvent",
|
|
90
|
+
"HookResponse",
|
|
91
|
+
"EVENT_TYPE_CLI_SUPPORT",
|
|
92
|
+
# Plugin system
|
|
93
|
+
"HookPlugin",
|
|
94
|
+
"PluginLoader",
|
|
95
|
+
"PluginRegistry",
|
|
96
|
+
"RegisteredHandler",
|
|
97
|
+
"hook_handler",
|
|
98
|
+
"run_plugin_handlers",
|
|
99
|
+
# Legacy exports (backward compatibility)
|
|
100
|
+
"SessionManager",
|
|
101
|
+
"SummaryFileGenerator",
|
|
102
|
+
"TranscriptProcessor",
|
|
103
|
+
"ClaudeTranscriptParser",
|
|
104
|
+
]
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Artifact capture hook for extracting and storing artifacts from messages.
|
|
3
|
+
|
|
4
|
+
Processes assistant messages to extract:
|
|
5
|
+
- Code blocks (with language metadata)
|
|
6
|
+
- File path references
|
|
7
|
+
- Other classified content
|
|
8
|
+
|
|
9
|
+
Uses artifact_classifier for type detection and LocalArtifactManager for storage.
|
|
10
|
+
Tracks content hashes to prevent duplicate storage using a bounded LRU cache.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
import logging
|
|
17
|
+
import re
|
|
18
|
+
from collections import OrderedDict
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
from gobby.storage.artifact_classifier import ArtifactType, classify_artifact
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from gobby.storage.artifacts import Artifact, LocalArtifactManager
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
__all__ = ["ArtifactCaptureHook"]
|
|
29
|
+
|
|
30
|
+
# Maximum number of content hashes to track for duplicate detection
|
|
31
|
+
MAX_HASH_CACHE = 10000
|
|
32
|
+
|
|
33
|
+
# Pattern to extract markdown code blocks
|
|
34
|
+
_CODE_BLOCK_PATTERN = re.compile(r"```(\w*)\n(.*?)```", re.DOTALL)
|
|
35
|
+
|
|
36
|
+
# Pattern to extract backtick-wrapped file paths
|
|
37
|
+
_FILE_REF_PATTERN = re.compile(r"`([^\s`]+\.[a-zA-Z0-9]+)`")
|
|
38
|
+
|
|
39
|
+
# Pattern to extract bare file paths (Unix-style)
|
|
40
|
+
_UNIX_PATH_PATTERN = re.compile(r"(?:^|\s)(/[^\s]+\.[a-zA-Z0-9]+)(?:\s|$)")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ArtifactCaptureHook:
|
|
44
|
+
"""
|
|
45
|
+
Hook for capturing artifacts from assistant messages.
|
|
46
|
+
|
|
47
|
+
Extracts code blocks, file references, and other artifacts from messages,
|
|
48
|
+
classifies them using artifact_classifier, and stores via LocalArtifactManager.
|
|
49
|
+
|
|
50
|
+
Tracks content hashes to prevent storing duplicate artifacts.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, artifact_manager: LocalArtifactManager):
|
|
54
|
+
"""
|
|
55
|
+
Initialize the artifact capture hook.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
artifact_manager: LocalArtifactManager instance for storing artifacts
|
|
59
|
+
"""
|
|
60
|
+
self._artifact_manager = artifact_manager
|
|
61
|
+
# Use OrderedDict as LRU cache for bounded duplicate tracking
|
|
62
|
+
self._seen_hashes: OrderedDict[str, None] = OrderedDict()
|
|
63
|
+
|
|
64
|
+
def _compute_hash(self, content: str) -> str:
|
|
65
|
+
"""Compute a hash for content deduplication."""
|
|
66
|
+
return hashlib.sha256(content.encode()).hexdigest()
|
|
67
|
+
|
|
68
|
+
def _is_duplicate(self, content: str) -> bool:
|
|
69
|
+
"""Check if content has already been seen.
|
|
70
|
+
|
|
71
|
+
Uses a bounded LRU cache to prevent unbounded memory growth.
|
|
72
|
+
"""
|
|
73
|
+
content_hash = self._compute_hash(content)
|
|
74
|
+
if content_hash in self._seen_hashes:
|
|
75
|
+
# Move to end (most recently used)
|
|
76
|
+
self._seen_hashes.move_to_end(content_hash)
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
# Add new hash, evict oldest if over capacity
|
|
80
|
+
self._seen_hashes[content_hash] = None
|
|
81
|
+
if len(self._seen_hashes) > MAX_HASH_CACHE:
|
|
82
|
+
# Remove oldest entry (first item)
|
|
83
|
+
self._seen_hashes.popitem(last=False)
|
|
84
|
+
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
def reset_duplicate_tracking(self) -> None:
|
|
88
|
+
"""Clear the duplicate tracking cache.
|
|
89
|
+
|
|
90
|
+
Useful to reset between sessions or when memory needs to be freed.
|
|
91
|
+
"""
|
|
92
|
+
self._seen_hashes.clear()
|
|
93
|
+
|
|
94
|
+
def _extract_code_blocks(self, content: str) -> list[tuple[str, str]]:
|
|
95
|
+
"""
|
|
96
|
+
Extract code blocks from content.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
content: Message content to extract from
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
List of (language, code) tuples
|
|
103
|
+
"""
|
|
104
|
+
blocks = []
|
|
105
|
+
for match in _CODE_BLOCK_PATTERN.finditer(content):
|
|
106
|
+
language = match.group(1).lower() if match.group(1) else ""
|
|
107
|
+
code = match.group(2).strip()
|
|
108
|
+
if code:
|
|
109
|
+
blocks.append((language, code))
|
|
110
|
+
return blocks
|
|
111
|
+
|
|
112
|
+
def _extract_file_references(self, content: str) -> list[str]:
|
|
113
|
+
"""
|
|
114
|
+
Extract file path references from content.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
content: Message content to extract from
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
List of file paths
|
|
121
|
+
"""
|
|
122
|
+
paths = set()
|
|
123
|
+
|
|
124
|
+
# Extract backtick-wrapped file paths
|
|
125
|
+
for match in _FILE_REF_PATTERN.finditer(content):
|
|
126
|
+
path = match.group(1)
|
|
127
|
+
# Filter out obvious non-paths
|
|
128
|
+
if "/" in path or "\\" in path:
|
|
129
|
+
paths.add(path)
|
|
130
|
+
|
|
131
|
+
# Extract Unix-style paths
|
|
132
|
+
for match in _UNIX_PATH_PATTERN.finditer(content):
|
|
133
|
+
paths.add(match.group(1))
|
|
134
|
+
|
|
135
|
+
return list(paths)
|
|
136
|
+
|
|
137
|
+
def process_message(
|
|
138
|
+
self,
|
|
139
|
+
session_id: str,
|
|
140
|
+
role: str,
|
|
141
|
+
content: str,
|
|
142
|
+
) -> list[Artifact] | None:
|
|
143
|
+
"""
|
|
144
|
+
Process a message and extract artifacts.
|
|
145
|
+
|
|
146
|
+
Only processes assistant messages. Extracts code blocks and file
|
|
147
|
+
references, classifies them, and stores them as artifacts.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
session_id: ID of the session this message belongs to
|
|
151
|
+
role: Message role ("assistant" or "user")
|
|
152
|
+
content: Message content
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
List of created Artifact objects, or None/empty if none created
|
|
156
|
+
"""
|
|
157
|
+
# Only process assistant messages
|
|
158
|
+
if role != "assistant":
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
if not content or not content.strip():
|
|
162
|
+
return []
|
|
163
|
+
|
|
164
|
+
artifacts: list[Artifact] = []
|
|
165
|
+
|
|
166
|
+
# Extract and store code blocks
|
|
167
|
+
code_blocks = self._extract_code_blocks(content)
|
|
168
|
+
for language, code in code_blocks:
|
|
169
|
+
if self._is_duplicate(code):
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
# Use classifier to get type and metadata
|
|
173
|
+
result = classify_artifact(f"```{language}\n{code}\n```")
|
|
174
|
+
metadata = result.metadata.copy()
|
|
175
|
+
|
|
176
|
+
# Ensure language is in metadata
|
|
177
|
+
if language and "language" not in metadata:
|
|
178
|
+
metadata["language"] = language
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
artifact = self._artifact_manager.create_artifact(
|
|
182
|
+
session_id=session_id,
|
|
183
|
+
artifact_type=result.artifact_type.value,
|
|
184
|
+
content=code,
|
|
185
|
+
metadata=metadata,
|
|
186
|
+
)
|
|
187
|
+
artifacts.append(artifact)
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logger.error(f"Failed to create code artifact: {e}")
|
|
190
|
+
|
|
191
|
+
# Extract and store file references
|
|
192
|
+
file_paths = self._extract_file_references(content)
|
|
193
|
+
for path in file_paths:
|
|
194
|
+
if self._is_duplicate(path):
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
# Use classifier to verify it's a file path
|
|
198
|
+
result = classify_artifact(path)
|
|
199
|
+
if result.artifact_type != ArtifactType.FILE_PATH:
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
artifact = self._artifact_manager.create_artifact(
|
|
204
|
+
session_id=session_id,
|
|
205
|
+
artifact_type=ArtifactType.FILE_PATH.value,
|
|
206
|
+
content=path,
|
|
207
|
+
metadata=result.metadata,
|
|
208
|
+
)
|
|
209
|
+
artifacts.append(artifact)
|
|
210
|
+
except Exception as e:
|
|
211
|
+
logger.error(f"Failed to create file path artifact: {e}")
|
|
212
|
+
|
|
213
|
+
return artifacts
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hook Event Broadcaster.
|
|
3
|
+
|
|
4
|
+
Broadcasting of hook events to WebSocket clients with filtering and sanitization.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from gobby.config.app import DaemonConfig
|
|
12
|
+
from gobby.hooks.events import HookEvent, HookResponse
|
|
13
|
+
from gobby.hooks.hook_types import (
|
|
14
|
+
HOOK_INPUT_MODELS,
|
|
15
|
+
HOOK_OUTPUT_MODELS,
|
|
16
|
+
HookInput,
|
|
17
|
+
HookOutput,
|
|
18
|
+
HookType,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Mapping from unified HookEventType to specific HookType Pydantic models
|
|
25
|
+
EVENT_TYPE_TO_HOOK_TYPE: dict[str, HookType] = {
|
|
26
|
+
"session_start": HookType.SESSION_START,
|
|
27
|
+
"session_end": HookType.SESSION_END,
|
|
28
|
+
"before_agent": HookType.USER_PROMPT_SUBMIT,
|
|
29
|
+
"after_agent": HookType.STOP,
|
|
30
|
+
"stop": HookType.STOP,
|
|
31
|
+
"before_tool": HookType.PRE_TOOL_USE,
|
|
32
|
+
"after_tool": HookType.POST_TOOL_USE,
|
|
33
|
+
"before_tool_selection": HookType.PRE_TOOL_USE, # Maps to same as before_tool
|
|
34
|
+
"pre_compact": HookType.PRE_COMPACT,
|
|
35
|
+
"subagent_start": HookType.SUBAGENT_START,
|
|
36
|
+
"subagent_stop": HookType.SUBAGENT_STOP,
|
|
37
|
+
"notification": HookType.NOTIFICATION,
|
|
38
|
+
"before_model": HookType.BEFORE_MODEL,
|
|
39
|
+
"after_model": HookType.AFTER_MODEL,
|
|
40
|
+
"permission_request": HookType.PERMISSION_REQUEST,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class HookEventBroadcaster:
|
|
45
|
+
"""
|
|
46
|
+
Broadcasts hook events to connected WebSocket clients.
|
|
47
|
+
|
|
48
|
+
Handles configuration checking, filtering, payload sanitization,
|
|
49
|
+
and message formatting.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, websocket_server: Any | None, config: DaemonConfig | None):
|
|
53
|
+
"""
|
|
54
|
+
Initialize broadcaster.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
websocket_server: WebSocketServer instance (can be None)
|
|
58
|
+
config: Daemon configuration (can be None)
|
|
59
|
+
"""
|
|
60
|
+
self.websocket_server = websocket_server
|
|
61
|
+
self.config = config
|
|
62
|
+
|
|
63
|
+
async def broadcast_event(self, event: HookEvent, response: HookResponse | None = None) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Broadcast a unified HookEvent to all connected clients.
|
|
66
|
+
|
|
67
|
+
Automatically converts HookEvent to appropriate Pydantic models.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
event: The unified HookEvent
|
|
71
|
+
response: Optional HookResponse result
|
|
72
|
+
"""
|
|
73
|
+
if not self.websocket_server:
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
# Map unified event type to HookType enum for Pydantic models
|
|
78
|
+
# Use value string lookup to avoid circular imports if HookEventType not available here
|
|
79
|
+
# (Though we imported HookType, we didn't import HookEventType enum class yet, just used strings in dict keys safely)
|
|
80
|
+
enum_hook_type = EVENT_TYPE_TO_HOOK_TYPE.get(event.event_type.value)
|
|
81
|
+
|
|
82
|
+
if not enum_hook_type:
|
|
83
|
+
# Try direct map if values match (fallback)
|
|
84
|
+
try:
|
|
85
|
+
enum_hook_type = HookType(event.event_type.value)
|
|
86
|
+
except ValueError:
|
|
87
|
+
logger.warning(
|
|
88
|
+
f"Skipping broadcast for unknown hook type: {event.event_type.value}"
|
|
89
|
+
)
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# Get input/output models
|
|
93
|
+
input_model_cls = HOOK_INPUT_MODELS.get(enum_hook_type)
|
|
94
|
+
output_model_cls = HOOK_OUTPUT_MODELS.get(enum_hook_type)
|
|
95
|
+
|
|
96
|
+
if not input_model_cls or not output_model_cls:
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
# Prepare input data
|
|
100
|
+
raw_input = event.data.copy()
|
|
101
|
+
# Map 'session_id' -> 'external_id' if needed
|
|
102
|
+
if "external_id" not in raw_input and event.session_id:
|
|
103
|
+
raw_input["external_id"] = event.session_id
|
|
104
|
+
|
|
105
|
+
# Special handling for Subagent events: ensure subagent_id is present
|
|
106
|
+
if enum_hook_type in (HookType.SUBAGENT_START, HookType.SUBAGENT_STOP):
|
|
107
|
+
if "subagent_id" not in raw_input and "external_id" in raw_input:
|
|
108
|
+
raw_input["subagent_id"] = raw_input["external_id"]
|
|
109
|
+
|
|
110
|
+
# Map 'prompt' -> 'prompt_text' for UserPromptSubmit
|
|
111
|
+
if enum_hook_type == HookType.USER_PROMPT_SUBMIT:
|
|
112
|
+
if "prompt_text" not in raw_input and "prompt" in raw_input:
|
|
113
|
+
raw_input["prompt_text"] = raw_input["prompt"]
|
|
114
|
+
|
|
115
|
+
# Ensure 'permission_type' has a default for PermissionRequest
|
|
116
|
+
if enum_hook_type == HookType.PERMISSION_REQUEST:
|
|
117
|
+
if "permission_type" not in raw_input:
|
|
118
|
+
raw_input["permission_type"] = "unknown"
|
|
119
|
+
|
|
120
|
+
# Validate input data structure matches Pydantic model
|
|
121
|
+
# Use construct/model_validate to avoid strict validation errors if possible,
|
|
122
|
+
# or just try/except. Let's rely on standard validation.
|
|
123
|
+
validated_input = input_model_cls(**raw_input)
|
|
124
|
+
|
|
125
|
+
# Prepare output data if response provided
|
|
126
|
+
validated_output = None
|
|
127
|
+
if response:
|
|
128
|
+
# Map unified HookResponse to dict that matches Pydantic output model
|
|
129
|
+
# Note: HookResponse is unified, but Pydantic output models vary.
|
|
130
|
+
# Usually outputs have: continue, decision, etc.
|
|
131
|
+
# Simplest is to dump HookResponse to dict and filter/map.
|
|
132
|
+
|
|
133
|
+
# Default mapping from HookResponse
|
|
134
|
+
response_dict: dict[str, Any] = {
|
|
135
|
+
"continue": response.decision != "deny",
|
|
136
|
+
"decision": response.decision,
|
|
137
|
+
"stopReason": response.reason,
|
|
138
|
+
"systemMessage": response.system_message,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# Special handling: hookSpecificOutput from context
|
|
142
|
+
if response.context:
|
|
143
|
+
# This is tricky without specific model knowledge, but assuming
|
|
144
|
+
# generic structure or specific model fields.
|
|
145
|
+
# For SessionStartOutput: { continue: bool, message: str, ... }
|
|
146
|
+
# SessionStartOutput has: context: dict[str, Any] | None
|
|
147
|
+
|
|
148
|
+
# If model expects 'context' as dict, but we have string.
|
|
149
|
+
# We identified this mismatch earlier.
|
|
150
|
+
# If input model expects dict, we need to wrap or parse.
|
|
151
|
+
# Let's check the fields of the output model.
|
|
152
|
+
if "context" in output_model_cls.model_fields:
|
|
153
|
+
if isinstance(response.context, str):
|
|
154
|
+
# Wrap string in dict if needed, or just pass if model allows str
|
|
155
|
+
# SessionStartOutput.context is dict[str, Any] | None.
|
|
156
|
+
response_dict["context"] = {"additionalContext": response.context}
|
|
157
|
+
else:
|
|
158
|
+
response_dict["context"] = response.context
|
|
159
|
+
|
|
160
|
+
# Clean None values
|
|
161
|
+
response_dict = {k: v for k, v in response_dict.items() if v is not None}
|
|
162
|
+
|
|
163
|
+
# Allow pydantic to ignore extra fields
|
|
164
|
+
validated_output = output_model_cls.model_validate(response_dict, strict=False)
|
|
165
|
+
|
|
166
|
+
# Call internal broadcast method
|
|
167
|
+
await self.broadcast_hook_event(enum_hook_type, validated_input, validated_output)
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logger.warning(f"Failed to broadcast event {event.event_type}: {e}")
|
|
171
|
+
|
|
172
|
+
async def broadcast_hook_event(
|
|
173
|
+
self,
|
|
174
|
+
event_type: HookType,
|
|
175
|
+
event_input: HookInput,
|
|
176
|
+
event_output: HookOutput | None = None,
|
|
177
|
+
) -> None:
|
|
178
|
+
"""
|
|
179
|
+
Broadcast a specific hook event type.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
event_type: The type of hook event
|
|
183
|
+
event_input: The input data for the hook
|
|
184
|
+
event_output: The output data from the hook (optional)
|
|
185
|
+
"""
|
|
186
|
+
# Checks: WebSocket server implementation required
|
|
187
|
+
if not self.websocket_server:
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
# Checks: Feature enabled
|
|
191
|
+
if not self.config:
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
ws_config = self.config.hook_extensions.websocket
|
|
195
|
+
if not ws_config.enabled:
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
# Checks: Event filtering
|
|
199
|
+
if event_type.value not in ws_config.broadcast_events:
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
# Construct payload
|
|
204
|
+
payload: dict[str, Any] = {
|
|
205
|
+
"type": "hook_event",
|
|
206
|
+
"event_type": event_type.value,
|
|
207
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# Add input data if enabled
|
|
211
|
+
if ws_config.include_payload:
|
|
212
|
+
# Convert Pydantic model to dict
|
|
213
|
+
input_data = event_input.model_dump(mode="json", exclude_none=True)
|
|
214
|
+
|
|
215
|
+
# Ensuring privacy/security -> stripping potentially sensitive fields could go here
|
|
216
|
+
|
|
217
|
+
# Add to payload
|
|
218
|
+
payload["data"] = input_data
|
|
219
|
+
|
|
220
|
+
# Add specific fields top-level if needed for convenience
|
|
221
|
+
# e.g. extract session_id from input
|
|
222
|
+
if hasattr(event_input, "external_id"):
|
|
223
|
+
payload["session_id"] = event_input.external_id
|
|
224
|
+
elif hasattr(event_input, "session_id"):
|
|
225
|
+
payload["session_id"] = event_input.session_id
|
|
226
|
+
|
|
227
|
+
# Add output data if present and enabled
|
|
228
|
+
if event_output and ws_config.include_payload:
|
|
229
|
+
output_data = event_output.model_dump(mode="json", exclude_none=True)
|
|
230
|
+
payload["result"] = output_data
|
|
231
|
+
|
|
232
|
+
# Add task context if present
|
|
233
|
+
if hasattr(event_input, "task_id") and event_input.task_id:
|
|
234
|
+
payload["task_id"] = event_input.task_id
|
|
235
|
+
# Include full task context if available in metadata
|
|
236
|
+
if hasattr(event_input, "metadata") and "_task_context" in event_input.metadata:
|
|
237
|
+
payload["task_context"] = event_input.metadata["_task_context"]
|
|
238
|
+
|
|
239
|
+
# Broadcast message
|
|
240
|
+
await self.websocket_server.broadcast(payload)
|
|
241
|
+
|
|
242
|
+
except Exception as e:
|
|
243
|
+
logger.exception(f"Error broadcasting hook event {event_type.value}: {e}")
|