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,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Approval flow handling for workflow engine.
|
|
3
|
+
|
|
4
|
+
Extracted from engine.py to reduce complexity.
|
|
5
|
+
Handles user approval requests and responses for workflow gates.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from gobby.hooks.events import HookEvent, HookResponse
|
|
13
|
+
|
|
14
|
+
from .evaluator import check_approval_response
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .definitions import WorkflowState
|
|
18
|
+
from .evaluator import ConditionEvaluator
|
|
19
|
+
from .state_manager import WorkflowStateManager
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def handle_approval_response(
|
|
25
|
+
event: HookEvent,
|
|
26
|
+
state: "WorkflowState",
|
|
27
|
+
current_step: Any,
|
|
28
|
+
evaluator: "ConditionEvaluator",
|
|
29
|
+
state_manager: "WorkflowStateManager",
|
|
30
|
+
) -> HookResponse | None:
|
|
31
|
+
"""
|
|
32
|
+
Handle user response to approval request.
|
|
33
|
+
|
|
34
|
+
Called on BEFORE_AGENT events to check if user is responding to
|
|
35
|
+
a pending approval request.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
event: The hook event
|
|
39
|
+
state: Current workflow state
|
|
40
|
+
current_step: Current workflow step definition
|
|
41
|
+
evaluator: Condition evaluator for checking pending approvals
|
|
42
|
+
state_manager: State manager for persisting state changes
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
HookResponse if approval was handled, None otherwise.
|
|
46
|
+
"""
|
|
47
|
+
# Get user prompt from event
|
|
48
|
+
prompt = event.data.get("prompt", "") if event.data else ""
|
|
49
|
+
|
|
50
|
+
# Check if we're waiting for approval
|
|
51
|
+
if state.approval_pending:
|
|
52
|
+
response = check_approval_response(prompt)
|
|
53
|
+
|
|
54
|
+
if response == "approved":
|
|
55
|
+
# Mark approval granted
|
|
56
|
+
condition_id = state.approval_condition_id
|
|
57
|
+
approved_var = f"_approval_{condition_id}_granted"
|
|
58
|
+
state.variables[approved_var] = True
|
|
59
|
+
state.approval_pending = False
|
|
60
|
+
state.approval_condition_id = None
|
|
61
|
+
state.approval_prompt = None
|
|
62
|
+
state.approval_requested_at = None
|
|
63
|
+
state_manager.save_state(state)
|
|
64
|
+
|
|
65
|
+
logger.info(f"User approved condition '{condition_id}' in step '{state.step}'")
|
|
66
|
+
return HookResponse(
|
|
67
|
+
decision="allow",
|
|
68
|
+
context=f"✓ Approval granted for: {state.approval_prompt or 'action'}",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
elif response == "rejected":
|
|
72
|
+
# Mark approval rejected
|
|
73
|
+
condition_id = state.approval_condition_id
|
|
74
|
+
rejected_var = f"_approval_{condition_id}_rejected"
|
|
75
|
+
state.variables[rejected_var] = True
|
|
76
|
+
state.approval_pending = False
|
|
77
|
+
state.approval_condition_id = None
|
|
78
|
+
state.approval_prompt = None
|
|
79
|
+
state.approval_requested_at = None
|
|
80
|
+
state_manager.save_state(state)
|
|
81
|
+
|
|
82
|
+
logger.info(f"User rejected condition '{condition_id}' in step '{state.step}'")
|
|
83
|
+
return HookResponse(
|
|
84
|
+
decision="block",
|
|
85
|
+
reason="User rejected the approval request.",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
else:
|
|
89
|
+
# User didn't respond with approval keyword - remind them
|
|
90
|
+
return HookResponse(
|
|
91
|
+
decision="allow",
|
|
92
|
+
context=(
|
|
93
|
+
f"⏳ **Waiting for approval:** {state.approval_prompt}\n\n"
|
|
94
|
+
f"Please respond with 'yes' or 'no' to continue."
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Check if we need to request approval
|
|
99
|
+
approval_check = evaluator.check_pending_approval(current_step.exit_conditions, state)
|
|
100
|
+
|
|
101
|
+
if approval_check and approval_check.needs_approval:
|
|
102
|
+
# Set approval pending state
|
|
103
|
+
state.approval_pending = True
|
|
104
|
+
state.approval_condition_id = approval_check.condition_id
|
|
105
|
+
state.approval_prompt = approval_check.prompt
|
|
106
|
+
state.approval_requested_at = datetime.now(UTC)
|
|
107
|
+
state.approval_timeout_seconds = approval_check.timeout_seconds
|
|
108
|
+
state_manager.save_state(state)
|
|
109
|
+
|
|
110
|
+
logger.info(
|
|
111
|
+
f"Requesting approval for condition '{approval_check.condition_id}' "
|
|
112
|
+
f"in step '{state.step}'"
|
|
113
|
+
)
|
|
114
|
+
return HookResponse(
|
|
115
|
+
decision="allow",
|
|
116
|
+
context=(
|
|
117
|
+
f"🔔 **Approval Required**\n\n"
|
|
118
|
+
f"{approval_check.prompt}\n\n"
|
|
119
|
+
f"Please respond with 'yes' to approve or 'no' to reject."
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if approval_check and approval_check.is_timed_out:
|
|
124
|
+
# Timeout - treat as rejection
|
|
125
|
+
condition_id = approval_check.condition_id
|
|
126
|
+
rejected_var = f"_approval_{condition_id}_rejected"
|
|
127
|
+
state.variables[rejected_var] = True
|
|
128
|
+
state.approval_pending = False
|
|
129
|
+
state.approval_condition_id = None
|
|
130
|
+
state_manager.save_state(state)
|
|
131
|
+
|
|
132
|
+
logger.info(f"Approval timed out for condition '{condition_id}'")
|
|
133
|
+
return HookResponse(
|
|
134
|
+
decision="block",
|
|
135
|
+
reason=f"Approval request timed out after {approval_check.timeout_seconds} seconds.",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return None
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Artifact capture and read workflow actions.
|
|
2
|
+
|
|
3
|
+
Extracted from actions.py as part of strangler fig decomposition.
|
|
4
|
+
These functions handle file artifact capture and reading.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import glob
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def capture_artifact(
|
|
16
|
+
state: Any,
|
|
17
|
+
pattern: str | None = None,
|
|
18
|
+
save_as: str | None = None,
|
|
19
|
+
) -> dict[str, Any] | None:
|
|
20
|
+
"""Capture an artifact (file) and store its path in state.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
state: WorkflowState object with artifacts dict
|
|
24
|
+
pattern: Glob pattern to match files
|
|
25
|
+
save_as: Name to store the artifact under
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Dict with captured filepath, or None if no match
|
|
29
|
+
"""
|
|
30
|
+
if not pattern:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
# Use iglob generator to avoid building entire list on deep trees
|
|
34
|
+
# Select the lexicographically smallest match for determinism
|
|
35
|
+
first_match: str | None = None
|
|
36
|
+
for match in glob.iglob(pattern, recursive=True):
|
|
37
|
+
if first_match is None or match < first_match:
|
|
38
|
+
first_match = match
|
|
39
|
+
|
|
40
|
+
if first_match is None:
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
filepath = os.path.abspath(first_match)
|
|
44
|
+
|
|
45
|
+
if save_as:
|
|
46
|
+
if not state.artifacts:
|
|
47
|
+
state.artifacts = {}
|
|
48
|
+
state.artifacts[save_as] = filepath
|
|
49
|
+
|
|
50
|
+
return {"captured": filepath}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def read_artifact(
|
|
54
|
+
state: Any,
|
|
55
|
+
pattern: str | None = None,
|
|
56
|
+
variable_name: str | None = None,
|
|
57
|
+
) -> dict[str, Any] | None:
|
|
58
|
+
"""Read an artifact's content into a workflow variable.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
state: WorkflowState object with artifacts and variables dicts
|
|
62
|
+
pattern: Glob pattern or artifact key to read
|
|
63
|
+
variable_name: Variable name to store content in
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Dict with read_artifact, variable, and length, or None on error
|
|
67
|
+
"""
|
|
68
|
+
if not pattern:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
if not variable_name:
|
|
72
|
+
logger.warning("read_artifact: 'as' argument missing")
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
# Check if pattern matches an existing artifact key first
|
|
76
|
+
filepath = None
|
|
77
|
+
if state.artifacts:
|
|
78
|
+
filepath = state.artifacts.get(pattern)
|
|
79
|
+
|
|
80
|
+
if not filepath:
|
|
81
|
+
# Try as glob pattern - use sorted() for deterministic selection
|
|
82
|
+
matches = sorted(glob.glob(pattern, recursive=True))
|
|
83
|
+
if matches:
|
|
84
|
+
filepath = os.path.abspath(matches[0])
|
|
85
|
+
|
|
86
|
+
if not filepath or not os.path.exists(filepath):
|
|
87
|
+
logger.warning(f"read_artifact: File not found for pattern '{pattern}'")
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
# Use explicit encoding and error handling for cross-platform safety
|
|
92
|
+
with open(filepath, encoding="utf-8", errors="replace") as f:
|
|
93
|
+
content = f.read()
|
|
94
|
+
|
|
95
|
+
if not state.variables:
|
|
96
|
+
state.variables = {}
|
|
97
|
+
|
|
98
|
+
state.variables[variable_name] = content
|
|
99
|
+
return {"read_artifact": True, "variable": variable_name, "length": len(content)}
|
|
100
|
+
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.error(f"read_artifact: Failed to read {filepath}: {e}")
|
|
103
|
+
return None
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Audit logging helper functions for workflow engine.
|
|
3
|
+
|
|
4
|
+
Extracted from engine.py to reduce complexity.
|
|
5
|
+
These are pure logging functions with no side effects beyond audit.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from gobby.storage.workflow_audit import WorkflowAuditManager
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def log_tool_call(
|
|
18
|
+
audit_manager: "WorkflowAuditManager | None",
|
|
19
|
+
session_id: str,
|
|
20
|
+
step: str,
|
|
21
|
+
tool_name: str,
|
|
22
|
+
result: str,
|
|
23
|
+
reason: str | None = None,
|
|
24
|
+
context: dict[str, Any] | None = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Log a tool call permission check to the audit log."""
|
|
27
|
+
if audit_manager:
|
|
28
|
+
try:
|
|
29
|
+
audit_manager.log_tool_call(
|
|
30
|
+
session_id=session_id,
|
|
31
|
+
step=step,
|
|
32
|
+
tool_name=tool_name,
|
|
33
|
+
result=result,
|
|
34
|
+
reason=reason,
|
|
35
|
+
context=context,
|
|
36
|
+
)
|
|
37
|
+
except Exception as e:
|
|
38
|
+
logger.debug(f"Failed to log tool call audit: {e}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def log_rule_eval(
|
|
42
|
+
audit_manager: "WorkflowAuditManager | None",
|
|
43
|
+
session_id: str,
|
|
44
|
+
step: str,
|
|
45
|
+
rule_id: str,
|
|
46
|
+
condition: str,
|
|
47
|
+
result: str,
|
|
48
|
+
reason: str | None = None,
|
|
49
|
+
context: dict[str, Any] | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Log a rule evaluation to the audit log."""
|
|
52
|
+
if audit_manager:
|
|
53
|
+
try:
|
|
54
|
+
audit_manager.log_rule_eval(
|
|
55
|
+
session_id=session_id,
|
|
56
|
+
step=step,
|
|
57
|
+
rule_id=rule_id,
|
|
58
|
+
condition=condition,
|
|
59
|
+
result=result,
|
|
60
|
+
reason=reason,
|
|
61
|
+
context=context,
|
|
62
|
+
)
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logger.debug(f"Failed to log rule eval audit: {e}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def log_transition(
|
|
68
|
+
audit_manager: "WorkflowAuditManager | None",
|
|
69
|
+
session_id: str,
|
|
70
|
+
from_step: str,
|
|
71
|
+
to_step: str,
|
|
72
|
+
reason: str | None = None,
|
|
73
|
+
context: dict[str, Any] | None = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Log a step transition to the audit log."""
|
|
76
|
+
if audit_manager:
|
|
77
|
+
try:
|
|
78
|
+
audit_manager.log_transition(
|
|
79
|
+
session_id=session_id,
|
|
80
|
+
from_step=from_step,
|
|
81
|
+
to_step=to_step,
|
|
82
|
+
reason=reason,
|
|
83
|
+
context=context,
|
|
84
|
+
)
|
|
85
|
+
except Exception as e:
|
|
86
|
+
logger.debug(f"Failed to log transition audit: {e}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def log_approval(
|
|
90
|
+
audit_manager: "WorkflowAuditManager | None",
|
|
91
|
+
session_id: str,
|
|
92
|
+
step: str,
|
|
93
|
+
result: str,
|
|
94
|
+
condition_id: str | None = None,
|
|
95
|
+
prompt: str | None = None,
|
|
96
|
+
context: dict[str, Any] | None = None,
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Log an approval gate event to the audit log."""
|
|
99
|
+
if audit_manager:
|
|
100
|
+
try:
|
|
101
|
+
audit_manager.log_approval(
|
|
102
|
+
session_id=session_id,
|
|
103
|
+
step=step,
|
|
104
|
+
result=result,
|
|
105
|
+
condition_id=condition_id,
|
|
106
|
+
prompt=prompt,
|
|
107
|
+
context=context,
|
|
108
|
+
)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.debug(f"Failed to log approval audit: {e}")
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""Autonomous execution workflow actions.
|
|
2
|
+
|
|
3
|
+
Actions for managing autonomous loop execution including:
|
|
4
|
+
- Progress tracking (start, stop, record)
|
|
5
|
+
- Stuck detection (detect task loops, tool loops)
|
|
6
|
+
- Task selection recording
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from gobby.autonomous.progress_tracker import ProgressTracker, ProgressType
|
|
14
|
+
from gobby.autonomous.stuck_detector import StuckDetector
|
|
15
|
+
from gobby.workflows.definitions import WorkflowState
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def start_progress_tracking(
|
|
21
|
+
progress_tracker: "ProgressTracker | None",
|
|
22
|
+
session_id: str,
|
|
23
|
+
state: "WorkflowState",
|
|
24
|
+
) -> dict[str, Any]:
|
|
25
|
+
"""Start progress tracking for a session.
|
|
26
|
+
|
|
27
|
+
Marks the session as actively being tracked and clears any
|
|
28
|
+
previous progress data.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
progress_tracker: ProgressTracker instance
|
|
32
|
+
session_id: The session to track
|
|
33
|
+
state: Current workflow state (updated with tracking info)
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Dict with success status
|
|
37
|
+
"""
|
|
38
|
+
if not progress_tracker:
|
|
39
|
+
logger.warning("No progress_tracker available")
|
|
40
|
+
return {"success": False, "error": "Progress tracker not available"}
|
|
41
|
+
|
|
42
|
+
# Clear any existing progress data
|
|
43
|
+
progress_tracker.clear_session(session_id)
|
|
44
|
+
|
|
45
|
+
# Mark as tracking in workflow state
|
|
46
|
+
state.variables["_progress_tracking_active"] = True
|
|
47
|
+
|
|
48
|
+
logger.info(f"Started progress tracking for session {session_id}")
|
|
49
|
+
return {"success": True, "session_id": session_id}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def stop_progress_tracking(
|
|
53
|
+
progress_tracker: "ProgressTracker | None",
|
|
54
|
+
session_id: str,
|
|
55
|
+
state: "WorkflowState",
|
|
56
|
+
keep_data: bool = False,
|
|
57
|
+
) -> dict[str, Any]:
|
|
58
|
+
"""Stop progress tracking for a session.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
progress_tracker: ProgressTracker instance
|
|
62
|
+
session_id: The session to stop tracking
|
|
63
|
+
state: Current workflow state
|
|
64
|
+
keep_data: If True, preserve progress data; otherwise clear it
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Dict with success status and final summary
|
|
68
|
+
"""
|
|
69
|
+
if not progress_tracker:
|
|
70
|
+
return {"success": False, "error": "Progress tracker not available"}
|
|
71
|
+
|
|
72
|
+
# Get final summary before stopping
|
|
73
|
+
summary = progress_tracker.get_summary(session_id)
|
|
74
|
+
|
|
75
|
+
# Clear if requested
|
|
76
|
+
if not keep_data:
|
|
77
|
+
progress_tracker.clear_session(session_id)
|
|
78
|
+
|
|
79
|
+
# Mark as not tracking
|
|
80
|
+
state.variables["_progress_tracking_active"] = False
|
|
81
|
+
|
|
82
|
+
logger.info(f"Stopped progress tracking for session {session_id}")
|
|
83
|
+
return {
|
|
84
|
+
"success": True,
|
|
85
|
+
"session_id": session_id,
|
|
86
|
+
"final_summary": {
|
|
87
|
+
"total_events": summary.total_events,
|
|
88
|
+
"high_value_events": summary.high_value_events,
|
|
89
|
+
"was_stagnant": summary.is_stagnant,
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def record_progress(
|
|
95
|
+
progress_tracker: "ProgressTracker | None",
|
|
96
|
+
session_id: str,
|
|
97
|
+
progress_type: "ProgressType | str",
|
|
98
|
+
tool_name: str | None = None,
|
|
99
|
+
details: dict[str, Any] | None = None,
|
|
100
|
+
) -> dict[str, Any]:
|
|
101
|
+
"""Record a progress event.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
progress_tracker: ProgressTracker instance
|
|
105
|
+
session_id: The session to record for
|
|
106
|
+
progress_type: Type of progress (from ProgressType enum or string)
|
|
107
|
+
tool_name: Optional tool name that generated the event
|
|
108
|
+
details: Optional additional details
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Dict with success status and event info
|
|
112
|
+
"""
|
|
113
|
+
if not progress_tracker:
|
|
114
|
+
return {"success": False, "error": "Progress tracker not available"}
|
|
115
|
+
|
|
116
|
+
from gobby.autonomous.progress_tracker import ProgressType
|
|
117
|
+
|
|
118
|
+
# Convert string to enum if needed
|
|
119
|
+
if isinstance(progress_type, str):
|
|
120
|
+
try:
|
|
121
|
+
progress_type = ProgressType(progress_type)
|
|
122
|
+
except ValueError:
|
|
123
|
+
progress_type = ProgressType.TOOL_CALL
|
|
124
|
+
|
|
125
|
+
event = progress_tracker.record_event(
|
|
126
|
+
session_id=session_id,
|
|
127
|
+
progress_type=progress_type,
|
|
128
|
+
tool_name=tool_name,
|
|
129
|
+
details=details,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
"success": True,
|
|
134
|
+
"event": {
|
|
135
|
+
"type": event.progress_type.value,
|
|
136
|
+
"is_high_value": event.is_high_value,
|
|
137
|
+
"timestamp": event.timestamp.isoformat(),
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def detect_task_loop(
|
|
143
|
+
stuck_detector: "StuckDetector | None",
|
|
144
|
+
session_id: str,
|
|
145
|
+
state: "WorkflowState",
|
|
146
|
+
) -> dict[str, Any]:
|
|
147
|
+
"""Detect if the session is stuck in a task selection loop.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
stuck_detector: StuckDetector instance
|
|
151
|
+
session_id: The session to check
|
|
152
|
+
state: Current workflow state (updated with detection results)
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Dict with detection results
|
|
156
|
+
"""
|
|
157
|
+
if not stuck_detector:
|
|
158
|
+
return {"is_stuck": False, "error": "Stuck detector not available"}
|
|
159
|
+
|
|
160
|
+
result = stuck_detector.detect_task_loop(session_id)
|
|
161
|
+
|
|
162
|
+
# Update workflow state
|
|
163
|
+
state.variables["_task_loop_detected"] = result.is_stuck
|
|
164
|
+
if result.is_stuck:
|
|
165
|
+
state.variables["_task_loop_task_id"] = (
|
|
166
|
+
result.details.get("task_id") if result.details else None
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
"is_stuck": result.is_stuck,
|
|
171
|
+
"reason": result.reason,
|
|
172
|
+
"layer": result.layer,
|
|
173
|
+
"details": result.details,
|
|
174
|
+
"suggested_action": result.suggested_action,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def detect_stuck(
|
|
179
|
+
stuck_detector: "StuckDetector | None",
|
|
180
|
+
session_id: str,
|
|
181
|
+
state: "WorkflowState",
|
|
182
|
+
) -> dict[str, Any]:
|
|
183
|
+
"""Run full stuck detection (all layers).
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
stuck_detector: StuckDetector instance
|
|
187
|
+
session_id: The session to check
|
|
188
|
+
state: Current workflow state (updated with detection results)
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Dict with detection results and optional inject_context
|
|
192
|
+
"""
|
|
193
|
+
if not stuck_detector:
|
|
194
|
+
return {"is_stuck": False, "error": "Stuck detector not available"}
|
|
195
|
+
|
|
196
|
+
result = stuck_detector.is_stuck(session_id)
|
|
197
|
+
|
|
198
|
+
# Update workflow state
|
|
199
|
+
state.variables["_is_stuck"] = result.is_stuck
|
|
200
|
+
state.variables["_stuck_layer"] = result.layer
|
|
201
|
+
state.variables["_stuck_reason"] = result.reason
|
|
202
|
+
|
|
203
|
+
response: dict[str, Any] = {
|
|
204
|
+
"is_stuck": result.is_stuck,
|
|
205
|
+
"reason": result.reason,
|
|
206
|
+
"layer": result.layer,
|
|
207
|
+
"details": result.details,
|
|
208
|
+
"suggested_action": result.suggested_action,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
# Add context injection if stuck
|
|
212
|
+
if result.is_stuck:
|
|
213
|
+
response["inject_context"] = (
|
|
214
|
+
f"⚠️ **Stuck Detected** ({result.layer})\n\n"
|
|
215
|
+
f"Reason: {result.reason}\n"
|
|
216
|
+
f"Suggested action: {result.suggested_action or 'Review approach'}\n\n"
|
|
217
|
+
f"Consider stopping or changing your approach."
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return response
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def record_task_selection(
|
|
224
|
+
stuck_detector: "StuckDetector | None",
|
|
225
|
+
session_id: str,
|
|
226
|
+
task_id: str,
|
|
227
|
+
context: dict[str, Any] | None = None,
|
|
228
|
+
) -> dict[str, Any]:
|
|
229
|
+
"""Record a task selection for loop detection.
|
|
230
|
+
|
|
231
|
+
Called when the autonomous loop selects a task to work on.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
stuck_detector: StuckDetector instance
|
|
235
|
+
session_id: The session selecting the task
|
|
236
|
+
task_id: The task being selected
|
|
237
|
+
context: Optional context about the selection
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Dict with success status
|
|
241
|
+
"""
|
|
242
|
+
if not stuck_detector:
|
|
243
|
+
return {"success": False, "error": "Stuck detector not available"}
|
|
244
|
+
|
|
245
|
+
event = stuck_detector.record_task_selection(
|
|
246
|
+
session_id=session_id,
|
|
247
|
+
task_id=task_id,
|
|
248
|
+
context=context,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
"success": True,
|
|
253
|
+
"task_id": event.task_id,
|
|
254
|
+
"recorded_at": event.selected_at.isoformat(),
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def get_progress_summary(
|
|
259
|
+
progress_tracker: "ProgressTracker | None",
|
|
260
|
+
session_id: str,
|
|
261
|
+
) -> dict[str, Any]:
|
|
262
|
+
"""Get a summary of progress for a session.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
progress_tracker: ProgressTracker instance
|
|
266
|
+
session_id: The session to get summary for
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Dict with progress summary
|
|
270
|
+
"""
|
|
271
|
+
if not progress_tracker:
|
|
272
|
+
return {"error": "Progress tracker not available"}
|
|
273
|
+
|
|
274
|
+
summary = progress_tracker.get_summary(session_id)
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
"total_events": summary.total_events,
|
|
278
|
+
"high_value_events": summary.high_value_events,
|
|
279
|
+
"is_stagnant": summary.is_stagnant,
|
|
280
|
+
"stagnation_duration_seconds": summary.stagnation_duration_seconds,
|
|
281
|
+
"last_high_value_at": (
|
|
282
|
+
summary.last_high_value_at.isoformat() if summary.last_high_value_at else None
|
|
283
|
+
),
|
|
284
|
+
"last_event_at": (summary.last_event_at.isoformat() if summary.last_event_at else None),
|
|
285
|
+
"events_by_type": {k.value: v for k, v in summary.events_by_type.items()},
|
|
286
|
+
}
|