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
@@ -0,0 +1,382 @@
1
+ import inspect
2
+ import logging
3
+ import uuid
4
+ from typing import Any, Callable, Dict, Optional, Set, cast
5
+
6
+ from .definition import FlowDefinition, StepFn, StepFn2, StepFn3
7
+ from .models import FlowEvent, FlowEventType, FlowRunRecord, FlowRunStatus
8
+ from .reasons import ensure_reason_summary
9
+ from .store import FlowStore, now_iso
10
+
11
+ _logger = logging.getLogger(__name__)
12
+
13
+
14
+ class FlowRuntime:
15
+ def __init__(
16
+ self,
17
+ definition: FlowDefinition,
18
+ store: FlowStore,
19
+ emit_event: Optional[Callable[[FlowEvent], None]] = None,
20
+ ):
21
+ self.definition = definition
22
+ self.store = store
23
+ self.emit_event = emit_event
24
+ self._stop_check_interval = 0.5
25
+
26
+ def _emit(
27
+ self,
28
+ event_type: FlowEventType,
29
+ run_id: str,
30
+ data: Optional[Dict[str, Any]] = None,
31
+ step_id: Optional[str] = None,
32
+ ) -> None:
33
+ event = self.store.create_event(
34
+ event_id=str(uuid.uuid4()),
35
+ run_id=run_id,
36
+ event_type=event_type,
37
+ data=data or {},
38
+ step_id=step_id,
39
+ )
40
+ if self.emit_event:
41
+ try:
42
+ self.emit_event(event)
43
+ except Exception as e:
44
+ _logger.exception("Error emitting event: %s", e)
45
+
46
+ async def run_flow(
47
+ self,
48
+ run_id: str,
49
+ initial_state: Optional[Dict[str, Any]] = None,
50
+ ) -> FlowRunRecord:
51
+ record = self.store.get_flow_run(run_id)
52
+ if not record:
53
+ raise RuntimeError(f"Flow run {run_id} not found")
54
+
55
+ if record.status.is_terminal() and record.status not in {
56
+ FlowRunStatus.STOPPED,
57
+ FlowRunStatus.FAILED,
58
+ }:
59
+ return record
60
+
61
+ try:
62
+ self.store.set_stop_requested(run_id, False)
63
+
64
+ if record.status == FlowRunStatus.PENDING:
65
+ self._emit(FlowEventType.FLOW_STARTED, run_id)
66
+ now = now_iso()
67
+ updated = self.store.update_flow_run_status(
68
+ run_id=run_id,
69
+ status=FlowRunStatus.RUNNING,
70
+ started_at=now,
71
+ state=initial_state if initial_state is not None else record.state,
72
+ current_step=record.current_step or self.definition.initial_step,
73
+ )
74
+ if not updated:
75
+ raise RuntimeError(f"Failed to start flow run {run_id}")
76
+ record = updated
77
+ else:
78
+ self._emit(FlowEventType.FLOW_RESUMED, run_id)
79
+ updated = self.store.update_flow_run_status(
80
+ run_id=run_id,
81
+ status=FlowRunStatus.RUNNING,
82
+ state=initial_state if initial_state is not None else record.state,
83
+ )
84
+ if updated:
85
+ record = updated
86
+
87
+ next_steps: Set[str] = set()
88
+ if record.current_step:
89
+ next_steps.add(record.current_step)
90
+ else:
91
+ next_steps.add(self.definition.initial_step)
92
+
93
+ while next_steps:
94
+ latest = self.store.get_flow_run(run_id)
95
+ if latest:
96
+ record = latest
97
+
98
+ if record.stop_requested:
99
+ self._emit(FlowEventType.FLOW_STOPPED, run_id)
100
+ now = now_iso()
101
+ state = ensure_reason_summary(
102
+ dict(record.state or {}),
103
+ status=FlowRunStatus.STOPPED,
104
+ default="Stopped by user",
105
+ )
106
+ updated = self.store.update_flow_run_status(
107
+ run_id=run_id,
108
+ status=FlowRunStatus.STOPPED,
109
+ finished_at=now,
110
+ state=state,
111
+ )
112
+ if not updated:
113
+ raise RuntimeError(f"Failed to stop flow run {run_id}")
114
+ record = updated
115
+ break
116
+
117
+ step_id = next_steps.pop()
118
+
119
+ record = await self._execute_step(record, step_id)
120
+
121
+ if record.status.is_terminal() or record.status == FlowRunStatus.PAUSED:
122
+ break
123
+
124
+ if record.status == FlowRunStatus.RUNNING:
125
+ if not next_steps and record.current_step:
126
+ next_steps = {record.current_step}
127
+
128
+ return record
129
+
130
+ except Exception as e:
131
+ _logger.exception("Flow run %s failed with exception", run_id)
132
+ self._emit(
133
+ FlowEventType.FLOW_FAILED,
134
+ run_id,
135
+ data={"error": str(e)},
136
+ )
137
+ now = now_iso()
138
+ state = ensure_reason_summary(
139
+ dict(record.state or {}),
140
+ status=FlowRunStatus.FAILED,
141
+ error_message=str(e),
142
+ )
143
+ updated = self.store.update_flow_run_status(
144
+ run_id=run_id,
145
+ status=FlowRunStatus.FAILED,
146
+ finished_at=now,
147
+ error_message=str(e),
148
+ state=state,
149
+ )
150
+ if not updated:
151
+ raise RuntimeError(
152
+ f"Failed to update flow run {run_id} to failed state"
153
+ ) from e
154
+ record = updated
155
+ return record
156
+
157
+ async def _execute_step(
158
+ self,
159
+ record: FlowRunRecord,
160
+ step_id: str,
161
+ ) -> FlowRunRecord:
162
+ if step_id not in self.definition.steps:
163
+ raise ValueError(f"Step '{step_id}' not found in flow definition")
164
+
165
+ step_fn: StepFn = self.definition.steps[step_id]
166
+
167
+ self._emit(
168
+ FlowEventType.STEP_STARTED,
169
+ record.id,
170
+ data={"step_id": step_id, "step_name": step_id},
171
+ step_id=step_id,
172
+ )
173
+
174
+ updated = self.store.update_current_step(
175
+ run_id=record.id,
176
+ current_step=step_id,
177
+ )
178
+ if not updated:
179
+ raise RuntimeError(f"Failed to update current step to {step_id}")
180
+ record = updated
181
+
182
+ try:
183
+
184
+ def _bound_emit(event_type: FlowEventType, data: Dict[str, Any]) -> None:
185
+ self._emit(
186
+ event_type,
187
+ record.id,
188
+ data=data,
189
+ step_id=step_id,
190
+ )
191
+
192
+ def _step_accepts_emit() -> bool:
193
+ try:
194
+ sig = inspect.signature(step_fn)
195
+ except Exception:
196
+ return False
197
+ params = list(sig.parameters.values())
198
+ if any(
199
+ p.kind
200
+ in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
201
+ for p in params
202
+ ):
203
+ return True
204
+ positional = [
205
+ p
206
+ for p in params
207
+ if p.kind
208
+ in (
209
+ inspect.Parameter.POSITIONAL_ONLY,
210
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
211
+ )
212
+ ]
213
+ return len(positional) >= 3
214
+
215
+ if _step_accepts_emit():
216
+ outcome = await cast(StepFn3, step_fn)(
217
+ record, record.input_data, _bound_emit
218
+ )
219
+ else:
220
+ # Backwards-compatible call for older StepFn implementations.
221
+ outcome = await cast(StepFn2, step_fn)(record, record.input_data)
222
+
223
+ if outcome.output:
224
+ record.state.update(outcome.output)
225
+
226
+ if outcome.status == FlowRunStatus.RUNNING:
227
+ self._emit(
228
+ FlowEventType.STEP_COMPLETED,
229
+ record.id,
230
+ data={"step_id": step_id, "next_steps": list(outcome.next_steps)},
231
+ step_id=step_id,
232
+ )
233
+
234
+ if outcome.next_steps:
235
+ next_step = min(outcome.next_steps)
236
+ else:
237
+ next_step = None
238
+
239
+ updated = self.store.update_flow_run_status(
240
+ run_id=record.id,
241
+ status=FlowRunStatus.RUNNING,
242
+ state=record.state,
243
+ current_step=next_step,
244
+ )
245
+ if not updated:
246
+ raise RuntimeError(
247
+ f"Failed to update flow run after step {step_id}"
248
+ )
249
+ record = updated
250
+
251
+ elif outcome.status == FlowRunStatus.COMPLETED:
252
+ self._emit(
253
+ FlowEventType.STEP_COMPLETED,
254
+ record.id,
255
+ data={"step_id": step_id, "status": "completed"},
256
+ step_id=step_id,
257
+ )
258
+ self._emit(FlowEventType.FLOW_COMPLETED, record.id)
259
+
260
+ now = now_iso()
261
+ updated = self.store.update_flow_run_status(
262
+ run_id=record.id,
263
+ status=FlowRunStatus.COMPLETED,
264
+ finished_at=now,
265
+ state=record.state,
266
+ current_step=None,
267
+ )
268
+ if not updated:
269
+ raise RuntimeError(
270
+ f"Failed to update flow run after step {step_id}"
271
+ )
272
+ record = updated
273
+
274
+ elif outcome.status == FlowRunStatus.FAILED:
275
+ self._emit(
276
+ FlowEventType.STEP_FAILED,
277
+ record.id,
278
+ data={"step_id": step_id, "error": outcome.error},
279
+ step_id=step_id,
280
+ )
281
+
282
+ now = now_iso()
283
+ state = ensure_reason_summary(
284
+ dict(record.state or {}),
285
+ status=FlowRunStatus.FAILED,
286
+ error_message=outcome.error,
287
+ )
288
+ updated = self.store.update_flow_run_status(
289
+ run_id=record.id,
290
+ status=FlowRunStatus.FAILED,
291
+ finished_at=now,
292
+ error_message=outcome.error,
293
+ state=state,
294
+ current_step=None,
295
+ )
296
+ if not updated:
297
+ raise RuntimeError(
298
+ f"Failed to update flow run after step {step_id}"
299
+ )
300
+ record = updated
301
+
302
+ elif outcome.status == FlowRunStatus.STOPPED:
303
+ self._emit(
304
+ FlowEventType.STEP_COMPLETED,
305
+ record.id,
306
+ data={"step_id": step_id, "status": "stopped"},
307
+ step_id=step_id,
308
+ )
309
+
310
+ now = now_iso()
311
+ state = ensure_reason_summary(
312
+ dict(record.state or {}),
313
+ status=FlowRunStatus.STOPPED,
314
+ )
315
+ updated = self.store.update_flow_run_status(
316
+ run_id=record.id,
317
+ status=FlowRunStatus.STOPPED,
318
+ finished_at=now,
319
+ state=state,
320
+ current_step=None,
321
+ )
322
+ if not updated:
323
+ raise RuntimeError(
324
+ f"Failed to update flow run after step {step_id}"
325
+ )
326
+ record = updated
327
+
328
+ elif outcome.status == FlowRunStatus.PAUSED:
329
+ self._emit(
330
+ FlowEventType.STEP_COMPLETED,
331
+ record.id,
332
+ data={"step_id": step_id, "status": "paused"},
333
+ step_id=step_id,
334
+ )
335
+
336
+ state = ensure_reason_summary(
337
+ dict(record.state or {}),
338
+ status=FlowRunStatus.PAUSED,
339
+ )
340
+ updated = self.store.update_flow_run_status(
341
+ run_id=record.id,
342
+ status=FlowRunStatus.PAUSED,
343
+ state=state,
344
+ current_step=step_id,
345
+ )
346
+ if not updated:
347
+ raise RuntimeError(
348
+ f"Failed to update flow run after step {step_id}"
349
+ )
350
+ record = updated
351
+
352
+ return record
353
+
354
+ except Exception as e:
355
+ _logger.exception("Step %s failed with exception", step_id)
356
+ self._emit(
357
+ FlowEventType.STEP_FAILED,
358
+ record.id,
359
+ data={"step_id": step_id, "error": str(e)},
360
+ step_id=step_id,
361
+ )
362
+
363
+ now = now_iso()
364
+ state = ensure_reason_summary(
365
+ dict(record.state or {}),
366
+ status=FlowRunStatus.FAILED,
367
+ error_message=str(e),
368
+ )
369
+ updated = self.store.update_flow_run_status(
370
+ run_id=record.id,
371
+ status=FlowRunStatus.FAILED,
372
+ finished_at=now,
373
+ error_message=str(e),
374
+ state=state,
375
+ current_step=None,
376
+ )
377
+ if not updated:
378
+ raise RuntimeError(
379
+ f"Failed to update flow run after step {step_id}"
380
+ ) from e
381
+ record = updated
382
+ return record