codex-autorunner 0.1.2__py3-none-any.whl → 1.1.0__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.
- codex_autorunner/__init__.py +12 -1
- codex_autorunner/__main__.py +4 -0
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/client.py +68 -35
- codex_autorunner/agents/opencode/constants.py +3 -0
- codex_autorunner/agents/opencode/harness.py +6 -1
- codex_autorunner/agents/opencode/logging.py +21 -5
- codex_autorunner/agents/opencode/run_prompt.py +1 -0
- codex_autorunner/agents/opencode/runtime.py +176 -47
- codex_autorunner/agents/opencode/supervisor.py +36 -48
- codex_autorunner/agents/registry.py +155 -8
- codex_autorunner/api.py +25 -0
- codex_autorunner/bootstrap.py +22 -37
- codex_autorunner/cli.py +5 -1156
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +4 -0
- codex_autorunner/core/about_car.py +49 -32
- codex_autorunner/core/adapter_utils.py +21 -0
- codex_autorunner/core/app_server_ids.py +59 -0
- codex_autorunner/core/app_server_logging.py +7 -3
- codex_autorunner/core/app_server_prompts.py +27 -260
- codex_autorunner/core/app_server_threads.py +26 -28
- codex_autorunner/core/app_server_utils.py +165 -0
- codex_autorunner/core/archive.py +349 -0
- codex_autorunner/core/codex_runner.py +12 -2
- codex_autorunner/core/config.py +587 -103
- codex_autorunner/core/docs.py +10 -2
- codex_autorunner/core/drafts.py +136 -0
- codex_autorunner/core/engine.py +1531 -866
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/flows/__init__.py +25 -0
- codex_autorunner/core/flows/controller.py +202 -0
- codex_autorunner/core/flows/definition.py +82 -0
- codex_autorunner/core/flows/models.py +88 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +131 -0
- codex_autorunner/core/flows/runtime.py +382 -0
- codex_autorunner/core/flows/store.py +568 -0
- codex_autorunner/core/flows/transition.py +138 -0
- codex_autorunner/core/flows/ux_helpers.py +257 -0
- codex_autorunner/core/flows/worker_process.py +242 -0
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +136 -16
- codex_autorunner/core/locks.py +4 -0
- codex_autorunner/core/notifications.py +14 -2
- codex_autorunner/core/ports/__init__.py +28 -0
- codex_autorunner/core/ports/agent_backend.py +150 -0
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/core/ports/run_event.py +91 -0
- codex_autorunner/core/prompt.py +15 -7
- codex_autorunner/core/redaction.py +29 -0
- codex_autorunner/core/review_context.py +5 -8
- codex_autorunner/core/run_index.py +6 -0
- codex_autorunner/core/runner_process.py +5 -2
- codex_autorunner/core/state.py +0 -88
- codex_autorunner/core/state_roots.py +57 -0
- codex_autorunner/core/supervisor_protocol.py +15 -0
- codex_autorunner/core/supervisor_utils.py +67 -0
- codex_autorunner/core/text_delta_coalescer.py +54 -0
- codex_autorunner/core/ticket_linter_cli.py +201 -0
- codex_autorunner/core/ticket_manager_cli.py +432 -0
- codex_autorunner/core/update.py +24 -16
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/update_runner.py +2 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +120 -11
- codex_autorunner/discovery.py +2 -4
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +15 -10
- codex_autorunner/flows/ticket_flow/__init__.py +3 -0
- codex_autorunner/flows/ticket_flow/definition.py +98 -0
- codex_autorunner/integrations/agents/__init__.py +17 -0
- codex_autorunner/integrations/agents/backend_orchestrator.py +284 -0
- codex_autorunner/integrations/agents/codex_adapter.py +90 -0
- codex_autorunner/integrations/agents/codex_backend.py +448 -0
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +598 -0
- codex_autorunner/integrations/agents/runner.py +91 -0
- codex_autorunner/integrations/agents/wiring.py +271 -0
- codex_autorunner/integrations/app_server/client.py +583 -152
- codex_autorunner/integrations/app_server/env.py +2 -107
- codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
- codex_autorunner/integrations/app_server/supervisor.py +59 -33
- codex_autorunner/integrations/telegram/adapter.py +204 -165
- codex_autorunner/integrations/telegram/api_schemas.py +120 -0
- codex_autorunner/integrations/telegram/config.py +221 -0
- codex_autorunner/integrations/telegram/constants.py +17 -2
- codex_autorunner/integrations/telegram/dispatch.py +17 -0
- codex_autorunner/integrations/telegram/doctor.py +47 -0
- codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -4
- codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
- codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +1364 -0
- codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
- codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +137 -478
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +17 -4
- codex_autorunner/integrations/telegram/handlers/messages.py +121 -9
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +111 -16
- codex_autorunner/integrations/telegram/outbox.py +208 -37
- codex_autorunner/integrations/telegram/progress_stream.py +3 -10
- codex_autorunner/integrations/telegram/service.py +221 -42
- codex_autorunner/integrations/telegram/state.py +100 -2
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +611 -0
- codex_autorunner/integrations/telegram/transport.py +39 -4
- codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
- codex_autorunner/manifest.py +2 -0
- codex_autorunner/plugin_api.py +22 -0
- codex_autorunner/routes/__init__.py +37 -67
- codex_autorunner/routes/agents.py +2 -137
- codex_autorunner/routes/analytics.py +3 -0
- codex_autorunner/routes/app_server.py +2 -131
- codex_autorunner/routes/base.py +2 -624
- codex_autorunner/routes/file_chat.py +7 -0
- codex_autorunner/routes/flows.py +7 -0
- codex_autorunner/routes/messages.py +7 -0
- codex_autorunner/routes/repos.py +2 -196
- codex_autorunner/routes/review.py +2 -147
- codex_autorunner/routes/sessions.py +2 -175
- codex_autorunner/routes/settings.py +2 -168
- codex_autorunner/routes/shared.py +2 -275
- codex_autorunner/routes/system.py +4 -188
- codex_autorunner/routes/usage.py +3 -0
- codex_autorunner/routes/voice.py +2 -119
- codex_autorunner/routes/workspace.py +3 -0
- codex_autorunner/server.py +3 -2
- codex_autorunner/static/agentControls.js +41 -11
- codex_autorunner/static/agentEvents.js +248 -0
- codex_autorunner/static/app.js +35 -24
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +36 -8
- codex_autorunner/static/bootstrap.js +1 -0
- codex_autorunner/static/bus.js +1 -0
- codex_autorunner/static/cache.js +1 -0
- codex_autorunner/static/constants.js +20 -4
- codex_autorunner/static/dashboard.js +344 -325
- codex_autorunner/static/diffRenderer.js +37 -0
- codex_autorunner/static/docChatCore.js +324 -0
- codex_autorunner/static/docChatStorage.js +65 -0
- codex_autorunner/static/docChatVoice.js +65 -0
- codex_autorunner/static/docEditor.js +133 -0
- codex_autorunner/static/env.js +1 -0
- codex_autorunner/static/eventSummarizer.js +166 -0
- codex_autorunner/static/fileChat.js +182 -0
- codex_autorunner/static/health.js +155 -0
- codex_autorunner/static/hub.js +126 -185
- codex_autorunner/static/index.html +839 -863
- codex_autorunner/static/liveUpdates.js +1 -0
- codex_autorunner/static/loader.js +1 -0
- codex_autorunner/static/messages.js +873 -0
- codex_autorunner/static/mobileCompact.js +2 -1
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +149 -217
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/styles.css +8850 -3876
- codex_autorunner/static/tabs.js +175 -11
- codex_autorunner/static/terminal.js +32 -0
- codex_autorunner/static/terminalManager.js +34 -59
- codex_autorunner/static/ticketChatActions.js +333 -0
- codex_autorunner/static/ticketChatEvents.js +16 -0
- codex_autorunner/static/ticketChatStorage.js +16 -0
- codex_autorunner/static/ticketChatStream.js +264 -0
- codex_autorunner/static/ticketEditor.js +844 -0
- codex_autorunner/static/ticketVoice.js +9 -0
- codex_autorunner/static/tickets.js +1988 -0
- codex_autorunner/static/utils.js +43 -3
- codex_autorunner/static/voice.js +1 -0
- codex_autorunner/static/workspace.js +765 -0
- codex_autorunner/static/workspaceApi.js +53 -0
- codex_autorunner/static/workspaceFileBrowser.js +504 -0
- codex_autorunner/surfaces/__init__.py +5 -0
- codex_autorunner/surfaces/cli/__init__.py +6 -0
- codex_autorunner/surfaces/cli/cli.py +1224 -0
- codex_autorunner/surfaces/cli/codex_cli.py +20 -0
- codex_autorunner/surfaces/telegram/__init__.py +3 -0
- codex_autorunner/surfaces/web/__init__.py +1 -0
- codex_autorunner/surfaces/web/app.py +2019 -0
- codex_autorunner/surfaces/web/hub_jobs.py +192 -0
- codex_autorunner/surfaces/web/middleware.py +587 -0
- codex_autorunner/surfaces/web/pty_session.py +370 -0
- codex_autorunner/surfaces/web/review.py +6 -0
- codex_autorunner/surfaces/web/routes/__init__.py +78 -0
- codex_autorunner/surfaces/web/routes/agents.py +138 -0
- codex_autorunner/surfaces/web/routes/analytics.py +277 -0
- codex_autorunner/surfaces/web/routes/app_server.py +132 -0
- codex_autorunner/surfaces/web/routes/archive.py +357 -0
- codex_autorunner/surfaces/web/routes/base.py +615 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +836 -0
- codex_autorunner/surfaces/web/routes/flows.py +1164 -0
- codex_autorunner/surfaces/web/routes/messages.py +459 -0
- codex_autorunner/surfaces/web/routes/repos.py +197 -0
- codex_autorunner/surfaces/web/routes/review.py +148 -0
- codex_autorunner/surfaces/web/routes/sessions.py +176 -0
- codex_autorunner/surfaces/web/routes/settings.py +169 -0
- codex_autorunner/surfaces/web/routes/shared.py +280 -0
- codex_autorunner/surfaces/web/routes/system.py +196 -0
- codex_autorunner/surfaces/web/routes/usage.py +89 -0
- codex_autorunner/surfaces/web/routes/voice.py +120 -0
- codex_autorunner/surfaces/web/routes/workspace.py +271 -0
- codex_autorunner/surfaces/web/runner_manager.py +25 -0
- codex_autorunner/surfaces/web/schemas.py +417 -0
- codex_autorunner/surfaces/web/static_assets.py +490 -0
- codex_autorunner/surfaces/web/static_refresh.py +86 -0
- codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
- codex_autorunner/tickets/__init__.py +27 -0
- codex_autorunner/tickets/agent_pool.py +399 -0
- codex_autorunner/tickets/files.py +89 -0
- codex_autorunner/tickets/frontmatter.py +55 -0
- codex_autorunner/tickets/lint.py +102 -0
- codex_autorunner/tickets/models.py +97 -0
- codex_autorunner/tickets/outbox.py +244 -0
- codex_autorunner/tickets/replies.py +179 -0
- codex_autorunner/tickets/runner.py +881 -0
- codex_autorunner/tickets/spec_ingest.py +77 -0
- codex_autorunner/web/__init__.py +5 -1
- codex_autorunner/web/app.py +2 -1771
- codex_autorunner/web/hub_jobs.py +2 -191
- codex_autorunner/web/middleware.py +2 -587
- codex_autorunner/web/pty_session.py +2 -369
- codex_autorunner/web/runner_manager.py +2 -24
- codex_autorunner/web/schemas.py +2 -396
- codex_autorunner/web/static_assets.py +4 -484
- codex_autorunner/web/static_refresh.py +2 -85
- codex_autorunner/web/terminal_sessions.py +2 -77
- codex_autorunner/workspace/__init__.py +40 -0
- codex_autorunner/workspace/paths.py +335 -0
- codex_autorunner-1.1.0.dist-info/METADATA +154 -0
- codex_autorunner-1.1.0.dist-info/RECORD +308 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/WHEEL +1 -1
- codex_autorunner/agents/execution/policy.py +0 -292
- codex_autorunner/agents/factory.py +0 -52
- codex_autorunner/agents/orchestrator.py +0 -358
- codex_autorunner/core/doc_chat.py +0 -1446
- codex_autorunner/core/snapshot.py +0 -580
- codex_autorunner/integrations/github/chatops.py +0 -268
- codex_autorunner/integrations/github/pr_flow.py +0 -1314
- codex_autorunner/routes/docs.py +0 -381
- codex_autorunner/routes/github.py +0 -327
- codex_autorunner/routes/runs.py +0 -250
- codex_autorunner/spec_ingest.py +0 -812
- codex_autorunner/static/docChatActions.js +0 -287
- codex_autorunner/static/docChatEvents.js +0 -300
- codex_autorunner/static/docChatRender.js +0 -205
- codex_autorunner/static/docChatStream.js +0 -361
- codex_autorunner/static/docs.js +0 -20
- codex_autorunner/static/docsClipboard.js +0 -69
- codex_autorunner/static/docsCrud.js +0 -257
- codex_autorunner/static/docsDocUpdates.js +0 -62
- codex_autorunner/static/docsDrafts.js +0 -16
- codex_autorunner/static/docsElements.js +0 -69
- codex_autorunner/static/docsInit.js +0 -285
- codex_autorunner/static/docsParse.js +0 -160
- codex_autorunner/static/docsSnapshot.js +0 -87
- codex_autorunner/static/docsSpecIngest.js +0 -263
- codex_autorunner/static/docsState.js +0 -127
- codex_autorunner/static/docsThreadRegistry.js +0 -44
- codex_autorunner/static/docsUi.js +0 -153
- codex_autorunner/static/docsVoice.js +0 -56
- codex_autorunner/static/github.js +0 -504
- codex_autorunner/static/logs.js +0 -678
- codex_autorunner/static/review.js +0 -157
- codex_autorunner/static/runs.js +0 -418
- codex_autorunner/static/snapshot.js +0 -124
- codex_autorunner/static/state.js +0 -94
- codex_autorunner/static/todoPreview.js +0 -27
- codex_autorunner/workspace.py +0 -16
- codex_autorunner-0.1.2.dist-info/METADATA +0 -249
- codex_autorunner-0.1.2.dist-info/RECORD +0 -222
- /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .controller import FlowController
|
|
2
|
+
from .definition import FlowDefinition, StepFn, StepOutcome
|
|
3
|
+
from .models import (
|
|
4
|
+
FlowArtifact,
|
|
5
|
+
FlowEvent,
|
|
6
|
+
FlowEventType,
|
|
7
|
+
FlowRunRecord,
|
|
8
|
+
FlowRunStatus,
|
|
9
|
+
)
|
|
10
|
+
from .runtime import FlowRuntime
|
|
11
|
+
from .store import FlowStore
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"FlowController",
|
|
15
|
+
"FlowDefinition",
|
|
16
|
+
"StepFn",
|
|
17
|
+
"StepOutcome",
|
|
18
|
+
"FlowArtifact",
|
|
19
|
+
"FlowEvent",
|
|
20
|
+
"FlowEventType",
|
|
21
|
+
"FlowRunRecord",
|
|
22
|
+
"FlowRunStatus",
|
|
23
|
+
"FlowRuntime",
|
|
24
|
+
"FlowStore",
|
|
25
|
+
]
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import uuid
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, AsyncGenerator, Callable, Dict, Optional, Set
|
|
6
|
+
|
|
7
|
+
from .definition import FlowDefinition
|
|
8
|
+
from .models import FlowEvent, FlowRunRecord, FlowRunStatus
|
|
9
|
+
from .runtime import FlowRuntime
|
|
10
|
+
from .store import FlowStore
|
|
11
|
+
|
|
12
|
+
_logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FlowController:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
definition: FlowDefinition,
|
|
19
|
+
db_path: Path,
|
|
20
|
+
artifacts_root: Path,
|
|
21
|
+
):
|
|
22
|
+
self.definition = definition
|
|
23
|
+
self.db_path = db_path
|
|
24
|
+
self.artifacts_root = artifacts_root
|
|
25
|
+
self.store = FlowStore(db_path)
|
|
26
|
+
self._event_listeners: Set[Callable[[FlowEvent], None]] = set()
|
|
27
|
+
self._lock = asyncio.Lock()
|
|
28
|
+
|
|
29
|
+
def initialize(self) -> None:
|
|
30
|
+
self.artifacts_root.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
self.store.initialize()
|
|
32
|
+
|
|
33
|
+
def shutdown(self) -> None:
|
|
34
|
+
self.store.close()
|
|
35
|
+
|
|
36
|
+
async def start_flow(
|
|
37
|
+
self,
|
|
38
|
+
input_data: Dict[str, Any],
|
|
39
|
+
run_id: Optional[str] = None,
|
|
40
|
+
initial_state: Optional[Dict[str, Any]] = None,
|
|
41
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
42
|
+
) -> FlowRunRecord:
|
|
43
|
+
"""Create a new flow run record without executing the flow."""
|
|
44
|
+
if run_id is None:
|
|
45
|
+
run_id = str(uuid.uuid4())
|
|
46
|
+
|
|
47
|
+
async with self._lock:
|
|
48
|
+
existing = self.store.get_flow_run(run_id)
|
|
49
|
+
if existing:
|
|
50
|
+
raise ValueError(f"Flow run {run_id} already exists")
|
|
51
|
+
|
|
52
|
+
self._prepare_artifacts_dir(run_id)
|
|
53
|
+
|
|
54
|
+
record = self.store.create_flow_run(
|
|
55
|
+
run_id=run_id,
|
|
56
|
+
flow_type=self.definition.flow_type,
|
|
57
|
+
input_data=input_data,
|
|
58
|
+
metadata=metadata,
|
|
59
|
+
state=initial_state or {},
|
|
60
|
+
current_step=self.definition.initial_step,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return record
|
|
64
|
+
|
|
65
|
+
async def run_flow(
|
|
66
|
+
self, run_id: str, initial_state: Optional[Dict[str, Any]] = None
|
|
67
|
+
) -> FlowRunRecord:
|
|
68
|
+
"""Run or resume a flow to completion in-process (used by workers/tests)."""
|
|
69
|
+
runtime = FlowRuntime(
|
|
70
|
+
definition=self.definition,
|
|
71
|
+
store=self.store,
|
|
72
|
+
emit_event=self._emit_event,
|
|
73
|
+
)
|
|
74
|
+
return await runtime.run_flow(run_id=run_id, initial_state=initial_state)
|
|
75
|
+
|
|
76
|
+
async def stop_flow(self, run_id: str) -> FlowRunRecord:
|
|
77
|
+
record = self.store.set_stop_requested(run_id, True)
|
|
78
|
+
if not record:
|
|
79
|
+
raise ValueError(f"Flow run {run_id} not found")
|
|
80
|
+
|
|
81
|
+
if record.status == FlowRunStatus.RUNNING:
|
|
82
|
+
updated = self.store.update_flow_run_status(
|
|
83
|
+
run_id=run_id,
|
|
84
|
+
status=FlowRunStatus.STOPPING,
|
|
85
|
+
)
|
|
86
|
+
if updated:
|
|
87
|
+
record = updated
|
|
88
|
+
|
|
89
|
+
updated = self.store.get_flow_run(run_id)
|
|
90
|
+
if not updated:
|
|
91
|
+
raise RuntimeError(f"Failed to get record for run {run_id}")
|
|
92
|
+
return updated
|
|
93
|
+
|
|
94
|
+
async def resume_flow(self, run_id: str) -> FlowRunRecord:
|
|
95
|
+
async with self._lock:
|
|
96
|
+
record = self.store.get_flow_run(run_id)
|
|
97
|
+
if not record:
|
|
98
|
+
raise ValueError(f"Flow run {run_id} not found")
|
|
99
|
+
|
|
100
|
+
if record.status == FlowRunStatus.RUNNING:
|
|
101
|
+
raise ValueError(f"Flow run {run_id} is already active")
|
|
102
|
+
|
|
103
|
+
cleared = self.store.set_stop_requested(run_id, False)
|
|
104
|
+
if not cleared:
|
|
105
|
+
raise RuntimeError(f"Failed to clear stop flag for run {run_id}")
|
|
106
|
+
if record.status == FlowRunStatus.COMPLETED:
|
|
107
|
+
return cleared
|
|
108
|
+
state = dict(record.state or {})
|
|
109
|
+
engine = state.get("ticket_engine")
|
|
110
|
+
if isinstance(engine, dict):
|
|
111
|
+
engine = dict(engine)
|
|
112
|
+
engine["status"] = "running"
|
|
113
|
+
engine.pop("reason", None)
|
|
114
|
+
engine.pop("reason_details", None)
|
|
115
|
+
engine.pop("reason_code", None)
|
|
116
|
+
state["ticket_engine"] = engine
|
|
117
|
+
state.pop("reason_summary", None)
|
|
118
|
+
|
|
119
|
+
updated = self.store.update_flow_run_status(
|
|
120
|
+
run_id=run_id,
|
|
121
|
+
status=FlowRunStatus.RUNNING,
|
|
122
|
+
state=state,
|
|
123
|
+
)
|
|
124
|
+
if updated:
|
|
125
|
+
return updated
|
|
126
|
+
|
|
127
|
+
updated = self.store.get_flow_run(run_id)
|
|
128
|
+
if not updated:
|
|
129
|
+
raise RuntimeError(f"Failed to get record for run {run_id}")
|
|
130
|
+
return updated
|
|
131
|
+
|
|
132
|
+
def get_status(self, run_id: str) -> Optional[FlowRunRecord]:
|
|
133
|
+
return self.store.get_flow_run(run_id)
|
|
134
|
+
|
|
135
|
+
def list_runs(self, status: Optional[FlowRunStatus] = None) -> list[FlowRunRecord]:
|
|
136
|
+
return self.store.list_flow_runs(
|
|
137
|
+
flow_type=self.definition.flow_type, status=status
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
async def stream_events(
|
|
141
|
+
self, run_id: str, after_seq: Optional[int] = None
|
|
142
|
+
) -> AsyncGenerator[FlowEvent, None]:
|
|
143
|
+
last_seq = after_seq
|
|
144
|
+
|
|
145
|
+
while True:
|
|
146
|
+
events = self.store.get_events(
|
|
147
|
+
run_id=run_id,
|
|
148
|
+
after_seq=last_seq,
|
|
149
|
+
limit=100,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
for event in events:
|
|
153
|
+
yield event
|
|
154
|
+
last_seq = event.seq
|
|
155
|
+
|
|
156
|
+
record = self.store.get_flow_run(run_id)
|
|
157
|
+
if (
|
|
158
|
+
record
|
|
159
|
+
and (record.status.is_terminal() or record.status.is_paused())
|
|
160
|
+
and not events
|
|
161
|
+
):
|
|
162
|
+
break
|
|
163
|
+
|
|
164
|
+
await asyncio.sleep(0.5)
|
|
165
|
+
|
|
166
|
+
def get_events(
|
|
167
|
+
self, run_id: str, after_seq: Optional[int] = None
|
|
168
|
+
) -> list[FlowEvent]:
|
|
169
|
+
return self.store.get_events(run_id=run_id, after_seq=after_seq)
|
|
170
|
+
|
|
171
|
+
def add_event_listener(self, listener: Callable[[FlowEvent], None]) -> None:
|
|
172
|
+
self._event_listeners.add(listener)
|
|
173
|
+
|
|
174
|
+
def remove_event_listener(self, listener: Callable[[FlowEvent], None]) -> None:
|
|
175
|
+
self._event_listeners.discard(listener)
|
|
176
|
+
|
|
177
|
+
def _emit_event(self, event: FlowEvent) -> None:
|
|
178
|
+
for listener in self._event_listeners:
|
|
179
|
+
try:
|
|
180
|
+
listener(event)
|
|
181
|
+
except Exception as e:
|
|
182
|
+
_logger.exception("Error in event listener: %s", e)
|
|
183
|
+
|
|
184
|
+
def _prepare_artifacts_dir(self, run_id: str) -> Path:
|
|
185
|
+
artifacts_dir = self.artifacts_root / run_id
|
|
186
|
+
artifacts_dir.mkdir(parents=True, exist_ok=True)
|
|
187
|
+
return artifacts_dir
|
|
188
|
+
|
|
189
|
+
def get_artifacts_dir(self, run_id: str) -> Optional[Path]:
|
|
190
|
+
artifacts_dir = self.artifacts_root / run_id
|
|
191
|
+
if artifacts_dir.exists():
|
|
192
|
+
return artifacts_dir
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
def get_artifacts(self, run_id: str) -> list:
|
|
196
|
+
return self.store.get_artifacts(run_id)
|
|
197
|
+
|
|
198
|
+
async def stream_events_since(
|
|
199
|
+
self, run_id: str, start_seq: Optional[int] = None
|
|
200
|
+
) -> AsyncGenerator[FlowEvent, None]:
|
|
201
|
+
async for event in self.stream_events(run_id, after_seq=start_seq):
|
|
202
|
+
yield event
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any, Awaitable, Callable, Dict, Optional, Set, Union
|
|
4
|
+
|
|
5
|
+
from .models import FlowEventType, FlowRunRecord, FlowRunStatus
|
|
6
|
+
|
|
7
|
+
_logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StepOutcome:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
status: FlowRunStatus,
|
|
14
|
+
next_steps: Optional[Set[str]] = None,
|
|
15
|
+
output: Optional[Dict[str, Any]] = None,
|
|
16
|
+
error: Optional[str] = None,
|
|
17
|
+
):
|
|
18
|
+
self.status = status
|
|
19
|
+
self.next_steps = next_steps or set()
|
|
20
|
+
self.output = output or {}
|
|
21
|
+
self.error = error
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def continue_to(
|
|
25
|
+
cls, next_steps: Set[str], output: Optional[Dict[str, Any]] = None
|
|
26
|
+
) -> "StepOutcome":
|
|
27
|
+
return cls(status=FlowRunStatus.RUNNING, next_steps=next_steps, output=output)
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def complete(cls, output: Optional[Dict[str, Any]] = None) -> "StepOutcome":
|
|
31
|
+
return cls(status=FlowRunStatus.COMPLETED, output=output)
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def fail(cls, error: str, output: Optional[Dict[str, Any]] = None) -> "StepOutcome":
|
|
35
|
+
return cls(status=FlowRunStatus.FAILED, error=error, output=output)
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def stop(cls, output: Optional[Dict[str, Any]] = None) -> "StepOutcome":
|
|
39
|
+
return cls(status=FlowRunStatus.STOPPED, output=output)
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def pause(cls, output: Optional[Dict[str, Any]] = None) -> "StepOutcome":
|
|
43
|
+
return cls(status=FlowRunStatus.PAUSED, output=output)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
EmitEventFn = Callable[[FlowEventType, Dict[str, Any]], None]
|
|
47
|
+
StepFn2 = Callable[[FlowRunRecord, Dict[str, Any]], Awaitable[StepOutcome]]
|
|
48
|
+
StepFn3 = Callable[
|
|
49
|
+
[FlowRunRecord, Dict[str, Any], Optional[EmitEventFn]], Awaitable[StepOutcome]
|
|
50
|
+
]
|
|
51
|
+
StepFn = Union[StepFn2, StepFn3]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class FlowDefinition:
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
flow_type: str,
|
|
58
|
+
initial_step: str,
|
|
59
|
+
steps: Dict[str, StepFn],
|
|
60
|
+
*,
|
|
61
|
+
name: Optional[str] = None,
|
|
62
|
+
description: Optional[str] = None,
|
|
63
|
+
input_schema: Optional[Dict[str, Any]] = None,
|
|
64
|
+
):
|
|
65
|
+
self.flow_type = flow_type
|
|
66
|
+
self.initial_step = initial_step
|
|
67
|
+
self.steps = steps
|
|
68
|
+
self.name = name or flow_type
|
|
69
|
+
self.description = description
|
|
70
|
+
self.input_schema = input_schema
|
|
71
|
+
|
|
72
|
+
def validate(self) -> None:
|
|
73
|
+
if self.initial_step not in self.steps:
|
|
74
|
+
raise ValueError(
|
|
75
|
+
f"Initial step '{self.initial_step}' not found in steps: {list(self.steps.keys())}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
for step_id, step_fn in self.steps.items():
|
|
79
|
+
if not asyncio.iscoroutinefunction(step_fn):
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"Step function for '{step_id}' must be async (coroutine function)"
|
|
82
|
+
)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
_logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FlowRunStatus(str, Enum):
|
|
11
|
+
PENDING = "pending"
|
|
12
|
+
RUNNING = "running"
|
|
13
|
+
PAUSED = "paused"
|
|
14
|
+
STOPPING = "stopping"
|
|
15
|
+
STOPPED = "stopped"
|
|
16
|
+
COMPLETED = "completed"
|
|
17
|
+
FAILED = "failed"
|
|
18
|
+
|
|
19
|
+
def is_terminal(self) -> bool:
|
|
20
|
+
return self in {self.COMPLETED, self.FAILED, self.STOPPED}
|
|
21
|
+
|
|
22
|
+
def is_active(self) -> bool:
|
|
23
|
+
return self in {self.PENDING, self.RUNNING, self.STOPPING}
|
|
24
|
+
|
|
25
|
+
def is_paused(self) -> bool:
|
|
26
|
+
return self == self.PAUSED
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FlowEventType(str, Enum):
|
|
30
|
+
STEP_STARTED = "step_started"
|
|
31
|
+
STEP_PROGRESS = "step_progress"
|
|
32
|
+
STEP_COMPLETED = "step_completed"
|
|
33
|
+
STEP_FAILED = "step_failed"
|
|
34
|
+
AGENT_STREAM_DELTA = "agent_stream_delta"
|
|
35
|
+
AGENT_MESSAGE_COMPLETE = "agent_message_complete"
|
|
36
|
+
AGENT_FAILED = "agent_failed"
|
|
37
|
+
APP_SERVER_EVENT = "app_server_event"
|
|
38
|
+
TOOL_CALL = "tool_call"
|
|
39
|
+
TOOL_RESULT = "tool_result"
|
|
40
|
+
APPROVAL_REQUESTED = "approval_requested"
|
|
41
|
+
TOKEN_USAGE = "token_usage"
|
|
42
|
+
FLOW_STARTED = "flow_started"
|
|
43
|
+
FLOW_STOPPED = "flow_stopped"
|
|
44
|
+
FLOW_RESUMED = "flow_resumed"
|
|
45
|
+
FLOW_COMPLETED = "flow_completed"
|
|
46
|
+
FLOW_FAILED = "flow_failed"
|
|
47
|
+
RUN_STARTED = "run_started"
|
|
48
|
+
RUN_FINISHED = "run_finished"
|
|
49
|
+
RUN_STATE_CHANGED = "run_state_changed"
|
|
50
|
+
RUN_NO_PROGRESS = "run_no_progress"
|
|
51
|
+
PLAN_UPDATED = "plan_updated"
|
|
52
|
+
DIFF_UPDATED = "diff_updated"
|
|
53
|
+
RUN_TIMEOUT = "run_timeout"
|
|
54
|
+
RUN_CANCELLED = "run_cancelled"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class FlowRunRecord(BaseModel):
|
|
58
|
+
id: str
|
|
59
|
+
flow_type: str
|
|
60
|
+
status: FlowRunStatus
|
|
61
|
+
input_data: Dict[str, Any] = Field(default_factory=dict)
|
|
62
|
+
state: Dict[str, Any] = Field(default_factory=dict)
|
|
63
|
+
current_step: Optional[str] = None
|
|
64
|
+
stop_requested: bool = False
|
|
65
|
+
created_at: str
|
|
66
|
+
started_at: Optional[str] = None
|
|
67
|
+
finished_at: Optional[str] = None
|
|
68
|
+
error_message: Optional[str] = None
|
|
69
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class FlowEvent(BaseModel):
|
|
73
|
+
seq: int
|
|
74
|
+
id: str
|
|
75
|
+
run_id: str
|
|
76
|
+
event_type: FlowEventType
|
|
77
|
+
timestamp: str
|
|
78
|
+
data: Dict[str, Any] = Field(default_factory=dict)
|
|
79
|
+
step_id: Optional[str] = None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class FlowArtifact(BaseModel):
|
|
83
|
+
id: str
|
|
84
|
+
run_id: str
|
|
85
|
+
kind: str
|
|
86
|
+
path: str
|
|
87
|
+
created_at: str
|
|
88
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from .models import FlowRunStatus
|
|
6
|
+
|
|
7
|
+
MAX_REASON_SUMMARY_LEN = 120
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _truncate(text: str, max_len: int = MAX_REASON_SUMMARY_LEN) -> str:
|
|
11
|
+
if len(text) <= max_len:
|
|
12
|
+
return text
|
|
13
|
+
return f"{text[:max_len].rstrip()}…"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def ensure_reason_summary(
|
|
17
|
+
state: Any,
|
|
18
|
+
*,
|
|
19
|
+
status: FlowRunStatus,
|
|
20
|
+
error_message: Optional[str] = None,
|
|
21
|
+
default: Optional[str] = None,
|
|
22
|
+
) -> dict[str, Any]:
|
|
23
|
+
"""Ensure state includes a short reason_summary when stopping/pausing/failing."""
|
|
24
|
+
normalized: dict[str, Any] = dict(state) if isinstance(state, dict) else {}
|
|
25
|
+
existing = normalized.get("reason_summary")
|
|
26
|
+
if isinstance(existing, str) and existing.strip():
|
|
27
|
+
return normalized
|
|
28
|
+
|
|
29
|
+
reason: Optional[str] = None
|
|
30
|
+
engine = normalized.get("ticket_engine")
|
|
31
|
+
if isinstance(engine, dict):
|
|
32
|
+
engine_reason = engine.get("reason")
|
|
33
|
+
if isinstance(engine_reason, str) and engine_reason.strip():
|
|
34
|
+
reason = engine_reason.strip()
|
|
35
|
+
|
|
36
|
+
if not reason and isinstance(error_message, str) and error_message.strip():
|
|
37
|
+
reason = error_message.strip()
|
|
38
|
+
|
|
39
|
+
if not reason:
|
|
40
|
+
if default:
|
|
41
|
+
reason = default
|
|
42
|
+
else:
|
|
43
|
+
fallback = {
|
|
44
|
+
FlowRunStatus.PAUSED: "Paused",
|
|
45
|
+
FlowRunStatus.FAILED: "Failed",
|
|
46
|
+
FlowRunStatus.STOPPED: "Stopped",
|
|
47
|
+
}
|
|
48
|
+
reason = fallback.get(status)
|
|
49
|
+
|
|
50
|
+
if reason:
|
|
51
|
+
normalized["reason_summary"] = _truncate(reason)
|
|
52
|
+
return normalized
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from ..locks import FileLockBusy, file_lock
|
|
9
|
+
from .models import FlowRunRecord, FlowRunStatus
|
|
10
|
+
from .store import UNSET, FlowStore
|
|
11
|
+
from .transition import resolve_flow_transition
|
|
12
|
+
from .worker_process import FlowWorkerHealth, check_worker_health, clear_worker_metadata
|
|
13
|
+
|
|
14
|
+
_logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
_ACTIVE_STATUSES = (
|
|
17
|
+
FlowRunStatus.RUNNING,
|
|
18
|
+
FlowRunStatus.STOPPING,
|
|
19
|
+
FlowRunStatus.PAUSED,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class FlowReconcileSummary:
|
|
25
|
+
checked: int = 0
|
|
26
|
+
active: int = 0
|
|
27
|
+
updated: int = 0
|
|
28
|
+
locked: int = 0
|
|
29
|
+
errors: int = 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class FlowReconcileResult:
|
|
34
|
+
records: list[FlowRunRecord]
|
|
35
|
+
summary: FlowReconcileSummary
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _reconcile_lock_path(repo_root: Path, run_id: str) -> Path:
|
|
39
|
+
return repo_root / ".codex-autorunner" / "flows" / run_id / "reconcile.lock"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _ensure_worker_not_stale(health: FlowWorkerHealth) -> None:
|
|
43
|
+
if health.status in {"dead", "mismatch", "invalid"}:
|
|
44
|
+
try:
|
|
45
|
+
clear_worker_metadata(health.artifact_path.parent)
|
|
46
|
+
except Exception:
|
|
47
|
+
_logger.debug("Failed to clear worker metadata: %s", health.artifact_path)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def reconcile_flow_run(
|
|
51
|
+
repo_root: Path,
|
|
52
|
+
record: FlowRunRecord,
|
|
53
|
+
store: FlowStore,
|
|
54
|
+
*,
|
|
55
|
+
logger: Optional[logging.Logger] = None,
|
|
56
|
+
) -> tuple[FlowRunRecord, bool, bool]:
|
|
57
|
+
if record.status not in _ACTIVE_STATUSES:
|
|
58
|
+
return record, False, False
|
|
59
|
+
|
|
60
|
+
lock_path = _reconcile_lock_path(repo_root, record.id)
|
|
61
|
+
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
try:
|
|
63
|
+
with file_lock(lock_path, blocking=False):
|
|
64
|
+
health = check_worker_health(repo_root, record.id)
|
|
65
|
+
decision = resolve_flow_transition(record, health)
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
decision.status == record.status
|
|
69
|
+
and decision.finished_at == record.finished_at
|
|
70
|
+
and decision.state == (record.state or {})
|
|
71
|
+
):
|
|
72
|
+
return record, False, False
|
|
73
|
+
|
|
74
|
+
(logger or _logger).info(
|
|
75
|
+
"Reconciling flow %s: %s -> %s (%s)",
|
|
76
|
+
record.id,
|
|
77
|
+
record.status.value,
|
|
78
|
+
decision.status.value,
|
|
79
|
+
decision.note or "reconcile",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
updated = store.update_flow_run_status(
|
|
83
|
+
run_id=record.id,
|
|
84
|
+
status=decision.status,
|
|
85
|
+
state=decision.state,
|
|
86
|
+
finished_at=decision.finished_at if decision.finished_at else UNSET,
|
|
87
|
+
)
|
|
88
|
+
_ensure_worker_not_stale(health)
|
|
89
|
+
return (updated or record), bool(updated), False
|
|
90
|
+
except FileLockBusy:
|
|
91
|
+
return record, False, True
|
|
92
|
+
except Exception as exc:
|
|
93
|
+
(logger or _logger).warning("Failed to reconcile flow %s: %s", record.id, exc)
|
|
94
|
+
return record, False, False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def reconcile_flow_runs(
|
|
98
|
+
repo_root: Path,
|
|
99
|
+
*,
|
|
100
|
+
flow_type: Optional[str] = None,
|
|
101
|
+
logger: Optional[logging.Logger] = None,
|
|
102
|
+
) -> FlowReconcileResult:
|
|
103
|
+
db_path = repo_root / ".codex-autorunner" / "flows.db"
|
|
104
|
+
if not db_path.exists():
|
|
105
|
+
return FlowReconcileResult(records=[], summary=FlowReconcileSummary())
|
|
106
|
+
store = FlowStore(db_path)
|
|
107
|
+
summary = FlowReconcileSummary()
|
|
108
|
+
records: list[FlowRunRecord] = []
|
|
109
|
+
try:
|
|
110
|
+
store.initialize()
|
|
111
|
+
for record in store.list_flow_runs(flow_type=flow_type):
|
|
112
|
+
if record.status in _ACTIVE_STATUSES:
|
|
113
|
+
summary.active += 1
|
|
114
|
+
summary.checked += 1
|
|
115
|
+
record, updated, locked = reconcile_flow_run(
|
|
116
|
+
repo_root, record, store, logger=logger
|
|
117
|
+
)
|
|
118
|
+
if updated:
|
|
119
|
+
summary.updated += 1
|
|
120
|
+
if locked:
|
|
121
|
+
summary.locked += 1
|
|
122
|
+
records.append(record)
|
|
123
|
+
except Exception as exc:
|
|
124
|
+
summary.errors += 1
|
|
125
|
+
(logger or _logger).warning("Flow reconcile run failed: %s", exc)
|
|
126
|
+
finally:
|
|
127
|
+
try:
|
|
128
|
+
store.close()
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
return FlowReconcileResult(records=records, summary=summary)
|