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.
Files changed (276) hide show
  1. codex_autorunner/__init__.py +12 -1
  2. codex_autorunner/__main__.py +4 -0
  3. codex_autorunner/agents/codex/harness.py +1 -1
  4. codex_autorunner/agents/opencode/client.py +68 -35
  5. codex_autorunner/agents/opencode/constants.py +3 -0
  6. codex_autorunner/agents/opencode/harness.py +6 -1
  7. codex_autorunner/agents/opencode/logging.py +21 -5
  8. codex_autorunner/agents/opencode/run_prompt.py +1 -0
  9. codex_autorunner/agents/opencode/runtime.py +176 -47
  10. codex_autorunner/agents/opencode/supervisor.py +36 -48
  11. codex_autorunner/agents/registry.py +155 -8
  12. codex_autorunner/api.py +25 -0
  13. codex_autorunner/bootstrap.py +22 -37
  14. codex_autorunner/cli.py +5 -1156
  15. codex_autorunner/codex_cli.py +20 -84
  16. codex_autorunner/core/__init__.py +4 -0
  17. codex_autorunner/core/about_car.py +49 -32
  18. codex_autorunner/core/adapter_utils.py +21 -0
  19. codex_autorunner/core/app_server_ids.py +59 -0
  20. codex_autorunner/core/app_server_logging.py +7 -3
  21. codex_autorunner/core/app_server_prompts.py +27 -260
  22. codex_autorunner/core/app_server_threads.py +26 -28
  23. codex_autorunner/core/app_server_utils.py +165 -0
  24. codex_autorunner/core/archive.py +349 -0
  25. codex_autorunner/core/codex_runner.py +12 -2
  26. codex_autorunner/core/config.py +587 -103
  27. codex_autorunner/core/docs.py +10 -2
  28. codex_autorunner/core/drafts.py +136 -0
  29. codex_autorunner/core/engine.py +1531 -866
  30. codex_autorunner/core/exceptions.py +4 -0
  31. codex_autorunner/core/flows/__init__.py +25 -0
  32. codex_autorunner/core/flows/controller.py +202 -0
  33. codex_autorunner/core/flows/definition.py +82 -0
  34. codex_autorunner/core/flows/models.py +88 -0
  35. codex_autorunner/core/flows/reasons.py +52 -0
  36. codex_autorunner/core/flows/reconciler.py +131 -0
  37. codex_autorunner/core/flows/runtime.py +382 -0
  38. codex_autorunner/core/flows/store.py +568 -0
  39. codex_autorunner/core/flows/transition.py +138 -0
  40. codex_autorunner/core/flows/ux_helpers.py +257 -0
  41. codex_autorunner/core/flows/worker_process.py +242 -0
  42. codex_autorunner/core/git_utils.py +62 -0
  43. codex_autorunner/core/hub.py +136 -16
  44. codex_autorunner/core/locks.py +4 -0
  45. codex_autorunner/core/notifications.py +14 -2
  46. codex_autorunner/core/ports/__init__.py +28 -0
  47. codex_autorunner/core/ports/agent_backend.py +150 -0
  48. codex_autorunner/core/ports/backend_orchestrator.py +41 -0
  49. codex_autorunner/core/ports/run_event.py +91 -0
  50. codex_autorunner/core/prompt.py +15 -7
  51. codex_autorunner/core/redaction.py +29 -0
  52. codex_autorunner/core/review_context.py +5 -8
  53. codex_autorunner/core/run_index.py +6 -0
  54. codex_autorunner/core/runner_process.py +5 -2
  55. codex_autorunner/core/state.py +0 -88
  56. codex_autorunner/core/state_roots.py +57 -0
  57. codex_autorunner/core/supervisor_protocol.py +15 -0
  58. codex_autorunner/core/supervisor_utils.py +67 -0
  59. codex_autorunner/core/text_delta_coalescer.py +54 -0
  60. codex_autorunner/core/ticket_linter_cli.py +201 -0
  61. codex_autorunner/core/ticket_manager_cli.py +432 -0
  62. codex_autorunner/core/update.py +24 -16
  63. codex_autorunner/core/update_paths.py +28 -0
  64. codex_autorunner/core/update_runner.py +2 -0
  65. codex_autorunner/core/usage.py +164 -12
  66. codex_autorunner/core/utils.py +120 -11
  67. codex_autorunner/discovery.py +2 -4
  68. codex_autorunner/flows/review/__init__.py +17 -0
  69. codex_autorunner/{core/review.py → flows/review/service.py} +15 -10
  70. codex_autorunner/flows/ticket_flow/__init__.py +3 -0
  71. codex_autorunner/flows/ticket_flow/definition.py +98 -0
  72. codex_autorunner/integrations/agents/__init__.py +17 -0
  73. codex_autorunner/integrations/agents/backend_orchestrator.py +284 -0
  74. codex_autorunner/integrations/agents/codex_adapter.py +90 -0
  75. codex_autorunner/integrations/agents/codex_backend.py +448 -0
  76. codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
  77. codex_autorunner/integrations/agents/opencode_backend.py +598 -0
  78. codex_autorunner/integrations/agents/runner.py +91 -0
  79. codex_autorunner/integrations/agents/wiring.py +271 -0
  80. codex_autorunner/integrations/app_server/client.py +583 -152
  81. codex_autorunner/integrations/app_server/env.py +2 -107
  82. codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
  83. codex_autorunner/integrations/app_server/supervisor.py +59 -33
  84. codex_autorunner/integrations/telegram/adapter.py +204 -165
  85. codex_autorunner/integrations/telegram/api_schemas.py +120 -0
  86. codex_autorunner/integrations/telegram/config.py +221 -0
  87. codex_autorunner/integrations/telegram/constants.py +17 -2
  88. codex_autorunner/integrations/telegram/dispatch.py +17 -0
  89. codex_autorunner/integrations/telegram/doctor.py +47 -0
  90. codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -4
  91. codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
  92. codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
  93. codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
  94. codex_autorunner/integrations/telegram/handlers/commands/flows.py +1364 -0
  95. codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
  96. codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
  97. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
  98. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +137 -478
  99. codex_autorunner/integrations/telegram/handlers/commands_spec.py +17 -4
  100. codex_autorunner/integrations/telegram/handlers/messages.py +121 -9
  101. codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
  102. codex_autorunner/integrations/telegram/helpers.py +111 -16
  103. codex_autorunner/integrations/telegram/outbox.py +208 -37
  104. codex_autorunner/integrations/telegram/progress_stream.py +3 -10
  105. codex_autorunner/integrations/telegram/service.py +221 -42
  106. codex_autorunner/integrations/telegram/state.py +100 -2
  107. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +611 -0
  108. codex_autorunner/integrations/telegram/transport.py +39 -4
  109. codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
  110. codex_autorunner/manifest.py +2 -0
  111. codex_autorunner/plugin_api.py +22 -0
  112. codex_autorunner/routes/__init__.py +37 -67
  113. codex_autorunner/routes/agents.py +2 -137
  114. codex_autorunner/routes/analytics.py +3 -0
  115. codex_autorunner/routes/app_server.py +2 -131
  116. codex_autorunner/routes/base.py +2 -624
  117. codex_autorunner/routes/file_chat.py +7 -0
  118. codex_autorunner/routes/flows.py +7 -0
  119. codex_autorunner/routes/messages.py +7 -0
  120. codex_autorunner/routes/repos.py +2 -196
  121. codex_autorunner/routes/review.py +2 -147
  122. codex_autorunner/routes/sessions.py +2 -175
  123. codex_autorunner/routes/settings.py +2 -168
  124. codex_autorunner/routes/shared.py +2 -275
  125. codex_autorunner/routes/system.py +4 -188
  126. codex_autorunner/routes/usage.py +3 -0
  127. codex_autorunner/routes/voice.py +2 -119
  128. codex_autorunner/routes/workspace.py +3 -0
  129. codex_autorunner/server.py +3 -2
  130. codex_autorunner/static/agentControls.js +41 -11
  131. codex_autorunner/static/agentEvents.js +248 -0
  132. codex_autorunner/static/app.js +35 -24
  133. codex_autorunner/static/archive.js +826 -0
  134. codex_autorunner/static/archiveApi.js +37 -0
  135. codex_autorunner/static/autoRefresh.js +36 -8
  136. codex_autorunner/static/bootstrap.js +1 -0
  137. codex_autorunner/static/bus.js +1 -0
  138. codex_autorunner/static/cache.js +1 -0
  139. codex_autorunner/static/constants.js +20 -4
  140. codex_autorunner/static/dashboard.js +344 -325
  141. codex_autorunner/static/diffRenderer.js +37 -0
  142. codex_autorunner/static/docChatCore.js +324 -0
  143. codex_autorunner/static/docChatStorage.js +65 -0
  144. codex_autorunner/static/docChatVoice.js +65 -0
  145. codex_autorunner/static/docEditor.js +133 -0
  146. codex_autorunner/static/env.js +1 -0
  147. codex_autorunner/static/eventSummarizer.js +166 -0
  148. codex_autorunner/static/fileChat.js +182 -0
  149. codex_autorunner/static/health.js +155 -0
  150. codex_autorunner/static/hub.js +126 -185
  151. codex_autorunner/static/index.html +839 -863
  152. codex_autorunner/static/liveUpdates.js +1 -0
  153. codex_autorunner/static/loader.js +1 -0
  154. codex_autorunner/static/messages.js +873 -0
  155. codex_autorunner/static/mobileCompact.js +2 -1
  156. codex_autorunner/static/preserve.js +17 -0
  157. codex_autorunner/static/settings.js +149 -217
  158. codex_autorunner/static/smartRefresh.js +52 -0
  159. codex_autorunner/static/styles.css +8850 -3876
  160. codex_autorunner/static/tabs.js +175 -11
  161. codex_autorunner/static/terminal.js +32 -0
  162. codex_autorunner/static/terminalManager.js +34 -59
  163. codex_autorunner/static/ticketChatActions.js +333 -0
  164. codex_autorunner/static/ticketChatEvents.js +16 -0
  165. codex_autorunner/static/ticketChatStorage.js +16 -0
  166. codex_autorunner/static/ticketChatStream.js +264 -0
  167. codex_autorunner/static/ticketEditor.js +844 -0
  168. codex_autorunner/static/ticketVoice.js +9 -0
  169. codex_autorunner/static/tickets.js +1988 -0
  170. codex_autorunner/static/utils.js +43 -3
  171. codex_autorunner/static/voice.js +1 -0
  172. codex_autorunner/static/workspace.js +765 -0
  173. codex_autorunner/static/workspaceApi.js +53 -0
  174. codex_autorunner/static/workspaceFileBrowser.js +504 -0
  175. codex_autorunner/surfaces/__init__.py +5 -0
  176. codex_autorunner/surfaces/cli/__init__.py +6 -0
  177. codex_autorunner/surfaces/cli/cli.py +1224 -0
  178. codex_autorunner/surfaces/cli/codex_cli.py +20 -0
  179. codex_autorunner/surfaces/telegram/__init__.py +3 -0
  180. codex_autorunner/surfaces/web/__init__.py +1 -0
  181. codex_autorunner/surfaces/web/app.py +2019 -0
  182. codex_autorunner/surfaces/web/hub_jobs.py +192 -0
  183. codex_autorunner/surfaces/web/middleware.py +587 -0
  184. codex_autorunner/surfaces/web/pty_session.py +370 -0
  185. codex_autorunner/surfaces/web/review.py +6 -0
  186. codex_autorunner/surfaces/web/routes/__init__.py +78 -0
  187. codex_autorunner/surfaces/web/routes/agents.py +138 -0
  188. codex_autorunner/surfaces/web/routes/analytics.py +277 -0
  189. codex_autorunner/surfaces/web/routes/app_server.py +132 -0
  190. codex_autorunner/surfaces/web/routes/archive.py +357 -0
  191. codex_autorunner/surfaces/web/routes/base.py +615 -0
  192. codex_autorunner/surfaces/web/routes/file_chat.py +836 -0
  193. codex_autorunner/surfaces/web/routes/flows.py +1164 -0
  194. codex_autorunner/surfaces/web/routes/messages.py +459 -0
  195. codex_autorunner/surfaces/web/routes/repos.py +197 -0
  196. codex_autorunner/surfaces/web/routes/review.py +148 -0
  197. codex_autorunner/surfaces/web/routes/sessions.py +176 -0
  198. codex_autorunner/surfaces/web/routes/settings.py +169 -0
  199. codex_autorunner/surfaces/web/routes/shared.py +280 -0
  200. codex_autorunner/surfaces/web/routes/system.py +196 -0
  201. codex_autorunner/surfaces/web/routes/usage.py +89 -0
  202. codex_autorunner/surfaces/web/routes/voice.py +120 -0
  203. codex_autorunner/surfaces/web/routes/workspace.py +271 -0
  204. codex_autorunner/surfaces/web/runner_manager.py +25 -0
  205. codex_autorunner/surfaces/web/schemas.py +417 -0
  206. codex_autorunner/surfaces/web/static_assets.py +490 -0
  207. codex_autorunner/surfaces/web/static_refresh.py +86 -0
  208. codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
  209. codex_autorunner/tickets/__init__.py +27 -0
  210. codex_autorunner/tickets/agent_pool.py +399 -0
  211. codex_autorunner/tickets/files.py +89 -0
  212. codex_autorunner/tickets/frontmatter.py +55 -0
  213. codex_autorunner/tickets/lint.py +102 -0
  214. codex_autorunner/tickets/models.py +97 -0
  215. codex_autorunner/tickets/outbox.py +244 -0
  216. codex_autorunner/tickets/replies.py +179 -0
  217. codex_autorunner/tickets/runner.py +881 -0
  218. codex_autorunner/tickets/spec_ingest.py +77 -0
  219. codex_autorunner/web/__init__.py +5 -1
  220. codex_autorunner/web/app.py +2 -1771
  221. codex_autorunner/web/hub_jobs.py +2 -191
  222. codex_autorunner/web/middleware.py +2 -587
  223. codex_autorunner/web/pty_session.py +2 -369
  224. codex_autorunner/web/runner_manager.py +2 -24
  225. codex_autorunner/web/schemas.py +2 -396
  226. codex_autorunner/web/static_assets.py +4 -484
  227. codex_autorunner/web/static_refresh.py +2 -85
  228. codex_autorunner/web/terminal_sessions.py +2 -77
  229. codex_autorunner/workspace/__init__.py +40 -0
  230. codex_autorunner/workspace/paths.py +335 -0
  231. codex_autorunner-1.1.0.dist-info/METADATA +154 -0
  232. codex_autorunner-1.1.0.dist-info/RECORD +308 -0
  233. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/WHEEL +1 -1
  234. codex_autorunner/agents/execution/policy.py +0 -292
  235. codex_autorunner/agents/factory.py +0 -52
  236. codex_autorunner/agents/orchestrator.py +0 -358
  237. codex_autorunner/core/doc_chat.py +0 -1446
  238. codex_autorunner/core/snapshot.py +0 -580
  239. codex_autorunner/integrations/github/chatops.py +0 -268
  240. codex_autorunner/integrations/github/pr_flow.py +0 -1314
  241. codex_autorunner/routes/docs.py +0 -381
  242. codex_autorunner/routes/github.py +0 -327
  243. codex_autorunner/routes/runs.py +0 -250
  244. codex_autorunner/spec_ingest.py +0 -812
  245. codex_autorunner/static/docChatActions.js +0 -287
  246. codex_autorunner/static/docChatEvents.js +0 -300
  247. codex_autorunner/static/docChatRender.js +0 -205
  248. codex_autorunner/static/docChatStream.js +0 -361
  249. codex_autorunner/static/docs.js +0 -20
  250. codex_autorunner/static/docsClipboard.js +0 -69
  251. codex_autorunner/static/docsCrud.js +0 -257
  252. codex_autorunner/static/docsDocUpdates.js +0 -62
  253. codex_autorunner/static/docsDrafts.js +0 -16
  254. codex_autorunner/static/docsElements.js +0 -69
  255. codex_autorunner/static/docsInit.js +0 -285
  256. codex_autorunner/static/docsParse.js +0 -160
  257. codex_autorunner/static/docsSnapshot.js +0 -87
  258. codex_autorunner/static/docsSpecIngest.js +0 -263
  259. codex_autorunner/static/docsState.js +0 -127
  260. codex_autorunner/static/docsThreadRegistry.js +0 -44
  261. codex_autorunner/static/docsUi.js +0 -153
  262. codex_autorunner/static/docsVoice.js +0 -56
  263. codex_autorunner/static/github.js +0 -504
  264. codex_autorunner/static/logs.js +0 -678
  265. codex_autorunner/static/review.js +0 -157
  266. codex_autorunner/static/runs.js +0 -418
  267. codex_autorunner/static/snapshot.js +0 -124
  268. codex_autorunner/static/state.js +0 -94
  269. codex_autorunner/static/todoPreview.js +0 -27
  270. codex_autorunner/workspace.py +0 -16
  271. codex_autorunner-0.1.2.dist-info/METADATA +0 -249
  272. codex_autorunner-0.1.2.dist-info/RECORD +0 -222
  273. /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
  274. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/entry_points.txt +0 -0
  275. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/licenses/LICENSE +0 -0
  276. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/top_level.txt +0 -0
@@ -58,3 +58,7 @@ class CircuitOpenError(CriticalError):
58
58
  msg,
59
59
  user_message=f"{service_name} is temporarily unavailable. Please try again later.",
60
60
  )
61
+
62
+
63
+ class AppServerError(CodexError):
64
+ pass
@@ -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)