gobby 0.2.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gobby/__init__.py +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- gobby-0.2.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Premature stop handling for workflow engine.
|
|
3
|
+
|
|
4
|
+
Extracted from engine.py to reduce complexity.
|
|
5
|
+
Handles the case when a session tries to stop before the workflow's
|
|
6
|
+
exit condition is satisfied.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from types import SimpleNamespace
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
from gobby.hooks.events import HookEvent, HookResponse
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .evaluator import ConditionEvaluator
|
|
18
|
+
from .loader import WorkflowLoader
|
|
19
|
+
from .state_manager import WorkflowStateManager
|
|
20
|
+
from .templates import TemplateEngine
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def check_premature_stop(
|
|
26
|
+
event: HookEvent,
|
|
27
|
+
context_data: dict[str, Any],
|
|
28
|
+
state_manager: "WorkflowStateManager",
|
|
29
|
+
loader: "WorkflowLoader",
|
|
30
|
+
evaluator: "ConditionEvaluator",
|
|
31
|
+
template_engine: "TemplateEngine | None",
|
|
32
|
+
) -> HookResponse | None:
|
|
33
|
+
"""
|
|
34
|
+
Check if an active step workflow should handle a premature stop.
|
|
35
|
+
|
|
36
|
+
Called on STOP events to evaluate whether the workflow's exit_condition
|
|
37
|
+
is met. If not met and workflow has on_premature_stop defined, returns
|
|
38
|
+
an appropriate response.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
event: The STOP hook event
|
|
42
|
+
context_data: Shared context data including session variables
|
|
43
|
+
state_manager: Workflow state manager
|
|
44
|
+
loader: Workflow definition loader
|
|
45
|
+
evaluator: Condition evaluator
|
|
46
|
+
template_engine: Template engine for rendering messages
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
HookResponse if premature stop detected, None otherwise
|
|
50
|
+
"""
|
|
51
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
52
|
+
if not session_id:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
# Check if there's an active step workflow
|
|
56
|
+
state = state_manager.get_state(session_id)
|
|
57
|
+
if not state:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
# Skip lifecycle-only states
|
|
61
|
+
if state.workflow_name == "__lifecycle__":
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
# Load the workflow definition
|
|
65
|
+
project_path = Path(event.cwd) if event.cwd else None
|
|
66
|
+
workflow = loader.load_workflow(state.workflow_name, project_path=project_path)
|
|
67
|
+
if not workflow:
|
|
68
|
+
logger.warning(f"Workflow '{state.workflow_name}' not found for premature stop check")
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
# Check if workflow has exit_condition and on_premature_stop
|
|
72
|
+
if not workflow.exit_condition:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
# Build evaluation context
|
|
76
|
+
# Use SimpleNamespace for variables so dot notation works (variables.session_task)
|
|
77
|
+
eval_context: dict[str, Any] = {
|
|
78
|
+
"workflow_state": state,
|
|
79
|
+
"state": state,
|
|
80
|
+
"variables": SimpleNamespace(**state.variables),
|
|
81
|
+
"current_step": state.step,
|
|
82
|
+
}
|
|
83
|
+
# Add session variables to context
|
|
84
|
+
eval_context.update(context_data)
|
|
85
|
+
|
|
86
|
+
# Evaluate the exit condition
|
|
87
|
+
exit_condition_met = evaluator.evaluate(workflow.exit_condition, eval_context)
|
|
88
|
+
|
|
89
|
+
if exit_condition_met:
|
|
90
|
+
logger.debug(f"Workflow '{workflow.name}' exit_condition met, allowing stop")
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
# Exit condition not met - check for premature stop handler
|
|
94
|
+
if not workflow.on_premature_stop:
|
|
95
|
+
logger.debug(
|
|
96
|
+
f"Workflow '{workflow.name}' exit_condition not met but no on_premature_stop defined"
|
|
97
|
+
)
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
# Failsafe: check if we've exceeded max stop attempts
|
|
101
|
+
# Counter is stored in variables and resets on BEFORE_AGENT (user prompt)
|
|
102
|
+
stop_count = state.variables.get("_premature_stop_count", 0) + 1
|
|
103
|
+
max_attempts = state.variables.get("premature_stop_max_attempts", 3)
|
|
104
|
+
|
|
105
|
+
# Update and persist the counter
|
|
106
|
+
state.variables["_premature_stop_count"] = stop_count
|
|
107
|
+
state_manager.save_state(state)
|
|
108
|
+
|
|
109
|
+
if max_attempts > 0 and stop_count >= max_attempts:
|
|
110
|
+
logger.warning(
|
|
111
|
+
f"Premature stop failsafe triggered for workflow '{workflow.name}': "
|
|
112
|
+
f"stop_count={stop_count} >= max_attempts={max_attempts}"
|
|
113
|
+
)
|
|
114
|
+
return HookResponse(
|
|
115
|
+
decision="allow",
|
|
116
|
+
context=(
|
|
117
|
+
f"⚠️ **Failsafe Exit**: Allowing stop after {stop_count} blocked attempts. "
|
|
118
|
+
f"Task may be incomplete."
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Handle premature stop based on action type
|
|
123
|
+
handler = workflow.on_premature_stop
|
|
124
|
+
|
|
125
|
+
# Render the message through template engine (supports Jinja2 variables)
|
|
126
|
+
rendered_message = handler.message
|
|
127
|
+
if template_engine and handler.message:
|
|
128
|
+
render_context = {
|
|
129
|
+
"variables": state.variables,
|
|
130
|
+
"state": state,
|
|
131
|
+
"workflow": workflow,
|
|
132
|
+
}
|
|
133
|
+
try:
|
|
134
|
+
rendered_message = template_engine.render(handler.message, render_context)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
logger.warning(f"Failed to render on_premature_stop message: {e}")
|
|
137
|
+
# Fall back to unrendered message
|
|
138
|
+
|
|
139
|
+
logger.info(
|
|
140
|
+
f"Premature stop detected for workflow '{workflow.name}': "
|
|
141
|
+
f"action={handler.action}, message={rendered_message[:100] if rendered_message else None}..., "
|
|
142
|
+
f"attempt {stop_count}/{max_attempts}"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if handler.action == "block":
|
|
146
|
+
return HookResponse(
|
|
147
|
+
decision="block",
|
|
148
|
+
reason=rendered_message,
|
|
149
|
+
)
|
|
150
|
+
elif handler.action == "warn":
|
|
151
|
+
return HookResponse(
|
|
152
|
+
decision="allow",
|
|
153
|
+
context=f"⚠️ **Warning**: {rendered_message}",
|
|
154
|
+
)
|
|
155
|
+
else: # guide_continuation (default)
|
|
156
|
+
return HookResponse(
|
|
157
|
+
decision="block",
|
|
158
|
+
reason=rendered_message,
|
|
159
|
+
context=(
|
|
160
|
+
f"📋 **Task Incomplete**\n\n"
|
|
161
|
+
f"{rendered_message}\n\n"
|
|
162
|
+
f"The workflow exit condition `{workflow.exit_condition}` is not yet satisfied."
|
|
163
|
+
),
|
|
164
|
+
)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Session lifecycle workflow actions.
|
|
2
|
+
|
|
3
|
+
Extracted from actions.py as part of strangler fig decomposition.
|
|
4
|
+
These functions handle session status updates, mode switching, and session chaining.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import shlex
|
|
9
|
+
import subprocess # nosec B404 - subprocess needed for session spawning
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def start_new_session(
|
|
16
|
+
session_manager: Any,
|
|
17
|
+
session_id: str,
|
|
18
|
+
command: str | None = None,
|
|
19
|
+
args: list[str] | str | None = None,
|
|
20
|
+
prompt: str | None = None,
|
|
21
|
+
cwd: str | None = None,
|
|
22
|
+
) -> dict[str, Any]:
|
|
23
|
+
"""Start a new CLI session (chaining).
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
session_manager: The session manager instance
|
|
27
|
+
session_id: Current session ID
|
|
28
|
+
command: CLI command to run (default: auto-detect from source)
|
|
29
|
+
args: List of arguments or string to split
|
|
30
|
+
prompt: Initial prompt/context to inject
|
|
31
|
+
cwd: Working directory (default: current session's cwd)
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Dict with started_new_session, pid, and command, or error
|
|
35
|
+
"""
|
|
36
|
+
session = session_manager.get(session_id)
|
|
37
|
+
if not session:
|
|
38
|
+
return {"error": "Session not found"}
|
|
39
|
+
|
|
40
|
+
# Determine command
|
|
41
|
+
if not command:
|
|
42
|
+
source = getattr(session, "source", "claude")
|
|
43
|
+
if source == "claude":
|
|
44
|
+
command = "claude"
|
|
45
|
+
elif source == "antigravity":
|
|
46
|
+
command = "claude" # Antigravity uses Claude Code
|
|
47
|
+
elif source == "gemini":
|
|
48
|
+
command = "gemini"
|
|
49
|
+
else:
|
|
50
|
+
command = "claude" # Default fallthrough
|
|
51
|
+
|
|
52
|
+
# Parse args
|
|
53
|
+
cmd_args: list[str] = []
|
|
54
|
+
if args:
|
|
55
|
+
if isinstance(args, str):
|
|
56
|
+
cmd_args = shlex.split(args)
|
|
57
|
+
else:
|
|
58
|
+
cmd_args = list(args)
|
|
59
|
+
|
|
60
|
+
# Determine working directory
|
|
61
|
+
if not cwd:
|
|
62
|
+
cwd = getattr(session, "project_path", None) or "."
|
|
63
|
+
|
|
64
|
+
logger.info(f"Starting new session: {command} {cmd_args} in {cwd}")
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
full_cmd = [command] + cmd_args
|
|
68
|
+
|
|
69
|
+
# Inject prompt via -p flag for Claude/Gemini if supported
|
|
70
|
+
if prompt and command in ["claude", "gemini"]:
|
|
71
|
+
full_cmd.extend(["-p", prompt])
|
|
72
|
+
|
|
73
|
+
proc = subprocess.Popen( # nosec B603 - cmd built from config, no shell
|
|
74
|
+
full_cmd,
|
|
75
|
+
cwd=cwd,
|
|
76
|
+
stdout=subprocess.DEVNULL,
|
|
77
|
+
stderr=subprocess.DEVNULL,
|
|
78
|
+
stdin=subprocess.DEVNULL,
|
|
79
|
+
start_new_session=True, # Detach
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
logger.info(f"Spawned process {proc.pid}")
|
|
83
|
+
return {"started_new_session": True, "pid": proc.pid, "command": str(full_cmd)}
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
logger.error(f"Failed to start new session: {e}", exc_info=True)
|
|
87
|
+
return {"error": str(e)}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def mark_session_status(
|
|
91
|
+
session_manager: Any,
|
|
92
|
+
session_id: str,
|
|
93
|
+
status: str | None = None,
|
|
94
|
+
target: str = "current_session",
|
|
95
|
+
) -> dict[str, Any]:
|
|
96
|
+
"""Mark a session status (current or parent).
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
session_manager: The session manager instance
|
|
100
|
+
session_id: Current session ID
|
|
101
|
+
status: New status to set
|
|
102
|
+
target: "current_session" or "parent_session"
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Dict with status_updated, session_id, and status, or error
|
|
106
|
+
"""
|
|
107
|
+
if not status:
|
|
108
|
+
return {"error": "Missing status"}
|
|
109
|
+
|
|
110
|
+
target_session_id = session_id
|
|
111
|
+
if target == "parent_session":
|
|
112
|
+
current_session = session_manager.get(session_id)
|
|
113
|
+
if current_session and current_session.parent_session_id:
|
|
114
|
+
target_session_id = current_session.parent_session_id
|
|
115
|
+
else:
|
|
116
|
+
return {"error": "No parent session linked"}
|
|
117
|
+
|
|
118
|
+
session_manager.update_status(target_session_id, status)
|
|
119
|
+
return {"status_updated": True, "session_id": target_session_id, "status": status}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def switch_mode(mode: str | None = None) -> dict[str, Any]:
|
|
123
|
+
"""Signal the agent to switch modes (e.g., PLAN, ACT).
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
mode: The mode to switch to
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Dict with inject_context and mode_switch, or error
|
|
130
|
+
"""
|
|
131
|
+
if not mode:
|
|
132
|
+
return {"error": "Missing mode"}
|
|
133
|
+
|
|
134
|
+
message = (
|
|
135
|
+
f"SYSTEM: SWITCH MODE TO {mode.upper()}\n"
|
|
136
|
+
f"You are now in {mode.upper()} mode. Adjust your behavior accordingly."
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return {"inject_context": message, "mode_switch": mode}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Workflow state management actions.
|
|
2
|
+
|
|
3
|
+
Extracted from actions.py as part of strangler fig decomposition.
|
|
4
|
+
These functions handle workflow state persistence and variable management.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_workflow_state(db: Any, session_id: str, state: Any) -> dict[str, Any]:
|
|
14
|
+
"""Load workflow state from DB into the provided state object.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
db: Database instance
|
|
18
|
+
session_id: Session ID to load state for
|
|
19
|
+
state: WorkflowState object to update
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Dict with state_loaded boolean
|
|
23
|
+
"""
|
|
24
|
+
from gobby.workflows.state_manager import WorkflowStateManager
|
|
25
|
+
|
|
26
|
+
state_manager = WorkflowStateManager(db)
|
|
27
|
+
loaded_state = state_manager.get_state(session_id)
|
|
28
|
+
|
|
29
|
+
if loaded_state:
|
|
30
|
+
# Copy attributes from loaded state to existing state object
|
|
31
|
+
for field in loaded_state.model_fields:
|
|
32
|
+
val = getattr(loaded_state, field)
|
|
33
|
+
setattr(state, field, val)
|
|
34
|
+
return {"state_loaded": True}
|
|
35
|
+
|
|
36
|
+
return {"state_loaded": False}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def save_workflow_state(db: Any, state: Any) -> dict[str, Any]:
|
|
40
|
+
"""Save workflow state to DB.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
db: Database instance
|
|
44
|
+
state: WorkflowState object to save
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Dict with state_saved boolean
|
|
48
|
+
"""
|
|
49
|
+
from gobby.workflows.state_manager import WorkflowStateManager
|
|
50
|
+
|
|
51
|
+
state_manager = WorkflowStateManager(db)
|
|
52
|
+
state_manager.save_state(state)
|
|
53
|
+
return {"state_saved": True}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def set_variable(state: Any, name: str | None, value: Any) -> dict[str, Any] | None:
|
|
57
|
+
"""Set a workflow variable.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
state: WorkflowState object
|
|
61
|
+
name: Variable name
|
|
62
|
+
value: Variable value
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Dict with variable_set and value, or None if no name
|
|
66
|
+
"""
|
|
67
|
+
if not name:
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
if not state.variables:
|
|
71
|
+
state.variables = {}
|
|
72
|
+
|
|
73
|
+
state.variables[name] = value
|
|
74
|
+
return {"variable_set": name, "value": value}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def increment_variable(
|
|
78
|
+
state: Any, name: str | None, amount: int | float = 1
|
|
79
|
+
) -> dict[str, Any] | None:
|
|
80
|
+
"""Increment a numeric workflow variable.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
state: WorkflowState object
|
|
84
|
+
name: Variable name
|
|
85
|
+
amount: Amount to increment by (default: 1)
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Dict with variable_incremented and value, or None if no name
|
|
89
|
+
"""
|
|
90
|
+
if not name:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
if not state.variables:
|
|
94
|
+
state.variables = {}
|
|
95
|
+
|
|
96
|
+
current = state.variables.get(name, 0)
|
|
97
|
+
if not isinstance(current, (int, float)):
|
|
98
|
+
logger.error(
|
|
99
|
+
f"increment_variable: Variable '{name}' is not numeric, got {type(current).__name__}: {current}"
|
|
100
|
+
)
|
|
101
|
+
raise TypeError(
|
|
102
|
+
f"Cannot increment non-numeric variable '{name}': "
|
|
103
|
+
f"expected int or float, got {type(current).__name__} with value {current!r}"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
new_value = current + amount
|
|
107
|
+
state.variables[name] = new_value
|
|
108
|
+
return {"variable_incremented": name, "value": new_value}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def mark_loop_complete(state: Any) -> dict[str, Any]:
|
|
112
|
+
"""Mark the autonomous loop as complete.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
state: WorkflowState object
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Dict with loop_marked_complete boolean
|
|
119
|
+
"""
|
|
120
|
+
if not state.variables:
|
|
121
|
+
state.variables = {}
|
|
122
|
+
state.variables["stop_reason"] = "completed"
|
|
123
|
+
return {"loop_marked_complete": True}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
|
|
5
|
+
from gobby.storage.database import DatabaseProtocol
|
|
6
|
+
|
|
7
|
+
from .definitions import WorkflowState
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WorkflowStateManager:
|
|
13
|
+
"""
|
|
14
|
+
Manages persistence of WorkflowState and Handoffs.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, db: DatabaseProtocol):
|
|
18
|
+
self.db = db
|
|
19
|
+
|
|
20
|
+
def get_state(self, session_id: str) -> WorkflowState | None:
|
|
21
|
+
row = self.db.fetchone("SELECT * FROM workflow_states WHERE session_id = ?", (session_id,))
|
|
22
|
+
if not row:
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
return WorkflowState(
|
|
27
|
+
session_id=row["session_id"],
|
|
28
|
+
workflow_name=row["workflow_name"],
|
|
29
|
+
step=row["step"],
|
|
30
|
+
step_entered_at=(
|
|
31
|
+
datetime.fromisoformat(row["step_entered_at"])
|
|
32
|
+
if row["step_entered_at"]
|
|
33
|
+
else datetime.now(UTC)
|
|
34
|
+
),
|
|
35
|
+
step_action_count=row["step_action_count"],
|
|
36
|
+
total_action_count=row["total_action_count"],
|
|
37
|
+
artifacts=json.loads(row["artifacts"]) if row["artifacts"] else {},
|
|
38
|
+
observations=json.loads(row["observations"]) if row["observations"] else [],
|
|
39
|
+
reflection_pending=bool(row["reflection_pending"]),
|
|
40
|
+
context_injected=bool(row["context_injected"]),
|
|
41
|
+
variables=json.loads(row["variables"]) if row["variables"] else {},
|
|
42
|
+
task_list=json.loads(row["task_list"]) if row["task_list"] else None,
|
|
43
|
+
current_task_index=row["current_task_index"],
|
|
44
|
+
files_modified_this_task=row["files_modified_this_task"],
|
|
45
|
+
updated_at=(
|
|
46
|
+
datetime.fromisoformat(row["updated_at"])
|
|
47
|
+
if row["updated_at"]
|
|
48
|
+
else datetime.now(UTC)
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
logger.error(
|
|
53
|
+
f"Failed to parse workflow state for session {session_id}: {e}", exc_info=True
|
|
54
|
+
)
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
def save_state(self, state: WorkflowState) -> None:
|
|
58
|
+
"""Upsert workflow state."""
|
|
59
|
+
self.db.execute(
|
|
60
|
+
"""
|
|
61
|
+
INSERT INTO workflow_states (
|
|
62
|
+
session_id, workflow_name, step, step_entered_at,
|
|
63
|
+
step_action_count, total_action_count, artifacts,
|
|
64
|
+
observations, reflection_pending, context_injected, variables,
|
|
65
|
+
task_list, current_task_index, files_modified_this_task,
|
|
66
|
+
updated_at
|
|
67
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
68
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
69
|
+
workflow_name = excluded.workflow_name,
|
|
70
|
+
step = excluded.step,
|
|
71
|
+
step_entered_at = excluded.step_entered_at,
|
|
72
|
+
step_action_count = excluded.step_action_count,
|
|
73
|
+
total_action_count = excluded.total_action_count,
|
|
74
|
+
artifacts = excluded.artifacts,
|
|
75
|
+
observations = excluded.observations,
|
|
76
|
+
reflection_pending = excluded.reflection_pending,
|
|
77
|
+
context_injected = excluded.context_injected,
|
|
78
|
+
variables = excluded.variables,
|
|
79
|
+
task_list = excluded.task_list,
|
|
80
|
+
current_task_index = excluded.current_task_index,
|
|
81
|
+
files_modified_this_task = excluded.files_modified_this_task,
|
|
82
|
+
updated_at = excluded.updated_at
|
|
83
|
+
""",
|
|
84
|
+
(
|
|
85
|
+
state.session_id,
|
|
86
|
+
state.workflow_name,
|
|
87
|
+
state.step,
|
|
88
|
+
state.step_entered_at.isoformat(),
|
|
89
|
+
state.step_action_count,
|
|
90
|
+
state.total_action_count,
|
|
91
|
+
json.dumps(state.artifacts),
|
|
92
|
+
json.dumps(state.observations),
|
|
93
|
+
1 if state.reflection_pending else 0,
|
|
94
|
+
1 if state.context_injected else 0,
|
|
95
|
+
json.dumps(state.variables),
|
|
96
|
+
json.dumps(state.task_list) if state.task_list else None,
|
|
97
|
+
state.current_task_index,
|
|
98
|
+
state.files_modified_this_task,
|
|
99
|
+
datetime.now(UTC).isoformat(),
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def delete_state(self, session_id: str) -> None:
|
|
104
|
+
self.db.execute("DELETE FROM workflow_states WHERE session_id = ?", (session_id,))
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Stop signal workflow actions for autonomous execution.
|
|
2
|
+
|
|
3
|
+
These actions enable workflows to check for and respond to stop signals
|
|
4
|
+
sent by external systems (HTTP, WebSocket, CLI, MCP).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from gobby.autonomous.stop_registry import StopRegistry
|
|
12
|
+
from gobby.workflows.definitions import WorkflowState
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def check_stop_signal(
|
|
18
|
+
stop_registry: "StopRegistry | None",
|
|
19
|
+
session_id: str,
|
|
20
|
+
state: "WorkflowState",
|
|
21
|
+
acknowledge: bool = False,
|
|
22
|
+
) -> dict[str, Any]:
|
|
23
|
+
"""Check if a stop signal has been sent for this session.
|
|
24
|
+
|
|
25
|
+
This action can be used in workflow transitions or as a periodic check
|
|
26
|
+
during autonomous execution.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
stop_registry: StopRegistry instance for checking signals
|
|
30
|
+
session_id: The session to check
|
|
31
|
+
state: Current workflow state (updated with signal info)
|
|
32
|
+
acknowledge: If True, acknowledge the signal (session will stop)
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Dict with:
|
|
36
|
+
- has_signal: True if there's a pending stop signal
|
|
37
|
+
- signal: Signal details if present (source, reason, requested_at)
|
|
38
|
+
- acknowledged: True if the signal was acknowledged
|
|
39
|
+
- inject_context: Optional message about the stop signal
|
|
40
|
+
"""
|
|
41
|
+
if not stop_registry:
|
|
42
|
+
logger.warning("No stop_registry available, cannot check stop signal")
|
|
43
|
+
return {"has_signal": False}
|
|
44
|
+
|
|
45
|
+
signal = stop_registry.get_signal(session_id)
|
|
46
|
+
|
|
47
|
+
if not signal or not signal.is_pending:
|
|
48
|
+
return {"has_signal": False}
|
|
49
|
+
|
|
50
|
+
# Store signal info in workflow variables
|
|
51
|
+
state.variables["_stop_signal_pending"] = True
|
|
52
|
+
state.variables["_stop_signal_source"] = signal.source
|
|
53
|
+
state.variables["_stop_signal_reason"] = signal.reason
|
|
54
|
+
|
|
55
|
+
result: dict[str, Any] = {
|
|
56
|
+
"has_signal": True,
|
|
57
|
+
"signal": {
|
|
58
|
+
"source": signal.source,
|
|
59
|
+
"reason": signal.reason,
|
|
60
|
+
"requested_at": signal.requested_at.isoformat(),
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if acknowledge:
|
|
65
|
+
stop_registry.acknowledge(session_id)
|
|
66
|
+
result["acknowledged"] = True
|
|
67
|
+
result["inject_context"] = (
|
|
68
|
+
f"🛑 **Stop Signal Received**\n\n"
|
|
69
|
+
f"Source: {signal.source}\n"
|
|
70
|
+
f"Reason: {signal.reason or 'No reason provided'}\n\n"
|
|
71
|
+
f"The session will stop gracefully."
|
|
72
|
+
)
|
|
73
|
+
logger.info(f"Stop signal acknowledged for session {session_id}")
|
|
74
|
+
else:
|
|
75
|
+
result["acknowledged"] = False
|
|
76
|
+
result["inject_context"] = (
|
|
77
|
+
f"⚠️ **Stop Signal Pending**\n\n"
|
|
78
|
+
f"A stop signal was received from {signal.source}.\n"
|
|
79
|
+
f"Reason: {signal.reason or 'No reason provided'}\n\n"
|
|
80
|
+
f"Complete current work and prepare to stop."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def has_stop_signal(stop_registry: "StopRegistry | None", session_id: str) -> bool:
|
|
87
|
+
"""Condition function to check if a stop signal is pending.
|
|
88
|
+
|
|
89
|
+
Use this in workflow transition conditions:
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
transitions:
|
|
93
|
+
- to: stopping
|
|
94
|
+
when: "has_stop_signal(session.id)"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
stop_registry: StopRegistry instance
|
|
99
|
+
session_id: The session to check
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
True if there's a pending stop signal
|
|
103
|
+
"""
|
|
104
|
+
if not stop_registry:
|
|
105
|
+
return False
|
|
106
|
+
return stop_registry.has_pending_signal(session_id)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def request_stop(
|
|
110
|
+
stop_registry: "StopRegistry | None",
|
|
111
|
+
session_id: str,
|
|
112
|
+
source: str = "workflow",
|
|
113
|
+
reason: str | None = None,
|
|
114
|
+
) -> dict[str, Any]:
|
|
115
|
+
"""Request a session to stop (can be used by stuck detection).
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
stop_registry: StopRegistry instance
|
|
119
|
+
session_id: The session to signal
|
|
120
|
+
source: Source of the request (workflow, stuck_detection, etc.)
|
|
121
|
+
reason: Optional reason for the stop request
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Dict with success status and signal details
|
|
125
|
+
"""
|
|
126
|
+
if not stop_registry:
|
|
127
|
+
logger.warning("No stop_registry available, cannot request stop")
|
|
128
|
+
return {"success": False, "error": "No stop registry available"}
|
|
129
|
+
|
|
130
|
+
signal = stop_registry.signal_stop(session_id, source, reason)
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
"success": True,
|
|
134
|
+
"signal": {
|
|
135
|
+
"session_id": signal.session_id,
|
|
136
|
+
"source": signal.source,
|
|
137
|
+
"reason": signal.reason,
|
|
138
|
+
"requested_at": signal.requested_at.isoformat(),
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def clear_stop_signal(
|
|
144
|
+
stop_registry: "StopRegistry | None",
|
|
145
|
+
session_id: str,
|
|
146
|
+
) -> dict[str, Any]:
|
|
147
|
+
"""Clear any stop signal for a session.
|
|
148
|
+
|
|
149
|
+
Use this after a session has fully stopped or when the signal
|
|
150
|
+
should be cancelled.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
stop_registry: StopRegistry instance
|
|
154
|
+
session_id: The session to clear
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Dict with success status
|
|
158
|
+
"""
|
|
159
|
+
if not stop_registry:
|
|
160
|
+
return {"success": False, "error": "No stop registry available"}
|
|
161
|
+
|
|
162
|
+
cleared = stop_registry.clear(session_id)
|
|
163
|
+
return {"success": True, "cleared": cleared}
|