codex-autorunner 0.1.1__py3-none-any.whl → 1.0.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 (226) hide show
  1. codex_autorunner/__main__.py +4 -0
  2. codex_autorunner/agents/__init__.py +20 -0
  3. codex_autorunner/agents/base.py +2 -2
  4. codex_autorunner/agents/codex/harness.py +1 -1
  5. codex_autorunner/agents/opencode/__init__.py +4 -0
  6. codex_autorunner/agents/opencode/agent_config.py +104 -0
  7. codex_autorunner/agents/opencode/client.py +305 -28
  8. codex_autorunner/agents/opencode/harness.py +71 -20
  9. codex_autorunner/agents/opencode/logging.py +225 -0
  10. codex_autorunner/agents/opencode/run_prompt.py +261 -0
  11. codex_autorunner/agents/opencode/runtime.py +1202 -132
  12. codex_autorunner/agents/opencode/supervisor.py +194 -68
  13. codex_autorunner/agents/registry.py +258 -0
  14. codex_autorunner/agents/types.py +2 -2
  15. codex_autorunner/api.py +25 -0
  16. codex_autorunner/bootstrap.py +19 -40
  17. codex_autorunner/cli.py +234 -151
  18. codex_autorunner/core/about_car.py +44 -32
  19. codex_autorunner/core/adapter_utils.py +21 -0
  20. codex_autorunner/core/app_server_events.py +15 -6
  21. codex_autorunner/core/app_server_logging.py +55 -15
  22. codex_autorunner/core/app_server_prompts.py +28 -259
  23. codex_autorunner/core/app_server_threads.py +15 -26
  24. codex_autorunner/core/circuit_breaker.py +183 -0
  25. codex_autorunner/core/codex_runner.py +6 -0
  26. codex_autorunner/core/config.py +555 -133
  27. codex_autorunner/core/docs.py +54 -9
  28. codex_autorunner/core/drafts.py +82 -0
  29. codex_autorunner/core/engine.py +828 -274
  30. codex_autorunner/core/exceptions.py +60 -0
  31. codex_autorunner/core/flows/__init__.py +25 -0
  32. codex_autorunner/core/flows/controller.py +178 -0
  33. codex_autorunner/core/flows/definition.py +82 -0
  34. codex_autorunner/core/flows/models.py +75 -0
  35. codex_autorunner/core/flows/runtime.py +351 -0
  36. codex_autorunner/core/flows/store.py +485 -0
  37. codex_autorunner/core/flows/transition.py +133 -0
  38. codex_autorunner/core/flows/worker_process.py +242 -0
  39. codex_autorunner/core/hub.py +21 -13
  40. codex_autorunner/core/locks.py +118 -1
  41. codex_autorunner/core/logging_utils.py +9 -6
  42. codex_autorunner/core/path_utils.py +123 -0
  43. codex_autorunner/core/prompt.py +15 -7
  44. codex_autorunner/core/redaction.py +29 -0
  45. codex_autorunner/core/retry.py +61 -0
  46. codex_autorunner/core/review.py +888 -0
  47. codex_autorunner/core/review_context.py +161 -0
  48. codex_autorunner/core/run_index.py +223 -0
  49. codex_autorunner/core/runner_controller.py +44 -1
  50. codex_autorunner/core/runner_process.py +30 -1
  51. codex_autorunner/core/sqlite_utils.py +32 -0
  52. codex_autorunner/core/state.py +273 -44
  53. codex_autorunner/core/static_assets.py +55 -0
  54. codex_autorunner/core/supervisor_utils.py +67 -0
  55. codex_autorunner/core/text_delta_coalescer.py +43 -0
  56. codex_autorunner/core/update.py +20 -11
  57. codex_autorunner/core/update_runner.py +2 -0
  58. codex_autorunner/core/usage.py +107 -75
  59. codex_autorunner/core/utils.py +167 -3
  60. codex_autorunner/discovery.py +3 -3
  61. codex_autorunner/flows/ticket_flow/__init__.py +3 -0
  62. codex_autorunner/flows/ticket_flow/definition.py +91 -0
  63. codex_autorunner/integrations/agents/__init__.py +27 -0
  64. codex_autorunner/integrations/agents/agent_backend.py +142 -0
  65. codex_autorunner/integrations/agents/codex_backend.py +307 -0
  66. codex_autorunner/integrations/agents/opencode_backend.py +325 -0
  67. codex_autorunner/integrations/agents/run_event.py +71 -0
  68. codex_autorunner/integrations/app_server/client.py +708 -153
  69. codex_autorunner/integrations/app_server/supervisor.py +59 -33
  70. codex_autorunner/integrations/telegram/adapter.py +474 -185
  71. codex_autorunner/integrations/telegram/api_schemas.py +120 -0
  72. codex_autorunner/integrations/telegram/config.py +239 -1
  73. codex_autorunner/integrations/telegram/constants.py +19 -1
  74. codex_autorunner/integrations/telegram/dispatch.py +44 -8
  75. codex_autorunner/integrations/telegram/doctor.py +47 -0
  76. codex_autorunner/integrations/telegram/handlers/approvals.py +12 -10
  77. codex_autorunner/integrations/telegram/handlers/callbacks.py +15 -1
  78. codex_autorunner/integrations/telegram/handlers/commands/__init__.py +29 -0
  79. codex_autorunner/integrations/telegram/handlers/commands/approvals.py +173 -0
  80. codex_autorunner/integrations/telegram/handlers/commands/execution.py +2595 -0
  81. codex_autorunner/integrations/telegram/handlers/commands/files.py +1408 -0
  82. codex_autorunner/integrations/telegram/handlers/commands/flows.py +227 -0
  83. codex_autorunner/integrations/telegram/handlers/commands/formatting.py +81 -0
  84. codex_autorunner/integrations/telegram/handlers/commands/github.py +1688 -0
  85. codex_autorunner/integrations/telegram/handlers/commands/shared.py +190 -0
  86. codex_autorunner/integrations/telegram/handlers/commands/voice.py +112 -0
  87. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +2043 -0
  88. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +954 -5689
  89. codex_autorunner/integrations/telegram/handlers/{commands.py → commands_spec.py} +11 -4
  90. codex_autorunner/integrations/telegram/handlers/messages.py +374 -49
  91. codex_autorunner/integrations/telegram/handlers/questions.py +389 -0
  92. codex_autorunner/integrations/telegram/handlers/selections.py +6 -4
  93. codex_autorunner/integrations/telegram/handlers/utils.py +171 -0
  94. codex_autorunner/integrations/telegram/helpers.py +90 -18
  95. codex_autorunner/integrations/telegram/notifications.py +126 -35
  96. codex_autorunner/integrations/telegram/outbox.py +214 -43
  97. codex_autorunner/integrations/telegram/progress_stream.py +42 -19
  98. codex_autorunner/integrations/telegram/runtime.py +24 -13
  99. codex_autorunner/integrations/telegram/service.py +500 -129
  100. codex_autorunner/integrations/telegram/state.py +1278 -330
  101. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
  102. codex_autorunner/integrations/telegram/transport.py +37 -4
  103. codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
  104. codex_autorunner/integrations/telegram/types.py +22 -2
  105. codex_autorunner/integrations/telegram/voice.py +14 -15
  106. codex_autorunner/manifest.py +2 -0
  107. codex_autorunner/plugin_api.py +22 -0
  108. codex_autorunner/routes/__init__.py +25 -14
  109. codex_autorunner/routes/agents.py +18 -78
  110. codex_autorunner/routes/analytics.py +239 -0
  111. codex_autorunner/routes/base.py +142 -113
  112. codex_autorunner/routes/file_chat.py +836 -0
  113. codex_autorunner/routes/flows.py +980 -0
  114. codex_autorunner/routes/messages.py +459 -0
  115. codex_autorunner/routes/repos.py +17 -0
  116. codex_autorunner/routes/review.py +148 -0
  117. codex_autorunner/routes/sessions.py +16 -8
  118. codex_autorunner/routes/settings.py +22 -0
  119. codex_autorunner/routes/shared.py +33 -3
  120. codex_autorunner/routes/system.py +22 -1
  121. codex_autorunner/routes/usage.py +87 -0
  122. codex_autorunner/routes/voice.py +5 -13
  123. codex_autorunner/routes/workspace.py +271 -0
  124. codex_autorunner/server.py +2 -1
  125. codex_autorunner/static/agentControls.js +9 -1
  126. codex_autorunner/static/agentEvents.js +248 -0
  127. codex_autorunner/static/app.js +27 -22
  128. codex_autorunner/static/autoRefresh.js +29 -1
  129. codex_autorunner/static/bootstrap.js +1 -0
  130. codex_autorunner/static/bus.js +1 -0
  131. codex_autorunner/static/cache.js +1 -0
  132. codex_autorunner/static/constants.js +20 -4
  133. codex_autorunner/static/dashboard.js +162 -150
  134. codex_autorunner/static/diffRenderer.js +37 -0
  135. codex_autorunner/static/docChatCore.js +324 -0
  136. codex_autorunner/static/docChatStorage.js +65 -0
  137. codex_autorunner/static/docChatVoice.js +65 -0
  138. codex_autorunner/static/docEditor.js +133 -0
  139. codex_autorunner/static/env.js +1 -0
  140. codex_autorunner/static/eventSummarizer.js +166 -0
  141. codex_autorunner/static/fileChat.js +182 -0
  142. codex_autorunner/static/health.js +155 -0
  143. codex_autorunner/static/hub.js +67 -126
  144. codex_autorunner/static/index.html +788 -807
  145. codex_autorunner/static/liveUpdates.js +59 -0
  146. codex_autorunner/static/loader.js +1 -0
  147. codex_autorunner/static/messages.js +470 -0
  148. codex_autorunner/static/mobileCompact.js +2 -1
  149. codex_autorunner/static/settings.js +24 -205
  150. codex_autorunner/static/styles.css +7577 -3758
  151. codex_autorunner/static/tabs.js +28 -5
  152. codex_autorunner/static/terminal.js +14 -0
  153. codex_autorunner/static/terminalManager.js +53 -59
  154. codex_autorunner/static/ticketChatActions.js +333 -0
  155. codex_autorunner/static/ticketChatEvents.js +16 -0
  156. codex_autorunner/static/ticketChatStorage.js +16 -0
  157. codex_autorunner/static/ticketChatStream.js +264 -0
  158. codex_autorunner/static/ticketEditor.js +750 -0
  159. codex_autorunner/static/ticketVoice.js +9 -0
  160. codex_autorunner/static/tickets.js +1315 -0
  161. codex_autorunner/static/utils.js +32 -3
  162. codex_autorunner/static/voice.js +21 -7
  163. codex_autorunner/static/workspace.js +672 -0
  164. codex_autorunner/static/workspaceApi.js +53 -0
  165. codex_autorunner/static/workspaceFileBrowser.js +504 -0
  166. codex_autorunner/tickets/__init__.py +20 -0
  167. codex_autorunner/tickets/agent_pool.py +377 -0
  168. codex_autorunner/tickets/files.py +85 -0
  169. codex_autorunner/tickets/frontmatter.py +55 -0
  170. codex_autorunner/tickets/lint.py +102 -0
  171. codex_autorunner/tickets/models.py +95 -0
  172. codex_autorunner/tickets/outbox.py +232 -0
  173. codex_autorunner/tickets/replies.py +179 -0
  174. codex_autorunner/tickets/runner.py +823 -0
  175. codex_autorunner/tickets/spec_ingest.py +77 -0
  176. codex_autorunner/voice/capture.py +7 -7
  177. codex_autorunner/voice/service.py +51 -9
  178. codex_autorunner/web/app.py +419 -199
  179. codex_autorunner/web/hub_jobs.py +13 -2
  180. codex_autorunner/web/middleware.py +47 -13
  181. codex_autorunner/web/pty_session.py +26 -13
  182. codex_autorunner/web/schemas.py +114 -109
  183. codex_autorunner/web/static_assets.py +55 -42
  184. codex_autorunner/web/static_refresh.py +86 -0
  185. codex_autorunner/workspace/__init__.py +40 -0
  186. codex_autorunner/workspace/paths.py +319 -0
  187. {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +20 -21
  188. codex_autorunner-1.0.0.dist-info/RECORD +251 -0
  189. {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/WHEEL +1 -1
  190. codex_autorunner/core/doc_chat.py +0 -1415
  191. codex_autorunner/core/snapshot.py +0 -580
  192. codex_autorunner/integrations/github/chatops.py +0 -268
  193. codex_autorunner/integrations/github/pr_flow.py +0 -1314
  194. codex_autorunner/routes/docs.py +0 -381
  195. codex_autorunner/routes/github.py +0 -327
  196. codex_autorunner/routes/runs.py +0 -118
  197. codex_autorunner/spec_ingest.py +0 -788
  198. codex_autorunner/static/docChatActions.js +0 -279
  199. codex_autorunner/static/docChatEvents.js +0 -300
  200. codex_autorunner/static/docChatRender.js +0 -205
  201. codex_autorunner/static/docChatStream.js +0 -361
  202. codex_autorunner/static/docs.js +0 -20
  203. codex_autorunner/static/docsClipboard.js +0 -69
  204. codex_autorunner/static/docsCrud.js +0 -257
  205. codex_autorunner/static/docsDocUpdates.js +0 -62
  206. codex_autorunner/static/docsDrafts.js +0 -16
  207. codex_autorunner/static/docsElements.js +0 -69
  208. codex_autorunner/static/docsInit.js +0 -274
  209. codex_autorunner/static/docsParse.js +0 -160
  210. codex_autorunner/static/docsSnapshot.js +0 -87
  211. codex_autorunner/static/docsSpecIngest.js +0 -263
  212. codex_autorunner/static/docsState.js +0 -127
  213. codex_autorunner/static/docsThreadRegistry.js +0 -44
  214. codex_autorunner/static/docsUi.js +0 -153
  215. codex_autorunner/static/docsVoice.js +0 -56
  216. codex_autorunner/static/github.js +0 -442
  217. codex_autorunner/static/logs.js +0 -640
  218. codex_autorunner/static/runs.js +0 -409
  219. codex_autorunner/static/snapshot.js +0 -124
  220. codex_autorunner/static/state.js +0 -86
  221. codex_autorunner/static/todoPreview.js +0 -27
  222. codex_autorunner/workspace.py +0 -16
  223. codex_autorunner-0.1.1.dist-info/RECORD +0 -191
  224. {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
  225. {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
  226. {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+ from typing import Optional
5
+
6
+
7
+ class Severity(str, Enum):
8
+ """Error severity levels."""
9
+
10
+ DEBUG = "debug"
11
+ INFO = "info"
12
+ WARNING = "warning"
13
+ ERROR = "error"
14
+ CRITICAL = "critical"
15
+
16
+
17
+ class CodexError(Exception):
18
+ """Base exception for all codex-autorunner errors."""
19
+
20
+ severity: Severity = Severity.WARNING
21
+ recoverable: bool = True
22
+ user_message: Optional[str] = None
23
+
24
+ def __init__(self, message: str, *, user_message: Optional[str] = None) -> None:
25
+ super().__init__(message)
26
+ if user_message is not None:
27
+ self.user_message = user_message
28
+
29
+
30
+ class TransientError(CodexError):
31
+ """Retryable errors (network, rate limits, temporary failures)."""
32
+
33
+ recoverable = True
34
+ severity = Severity.WARNING
35
+
36
+
37
+ class PermanentError(CodexError):
38
+ """Non-retryable errors (config, auth, validation)."""
39
+
40
+ recoverable = False
41
+ severity = Severity.ERROR
42
+
43
+
44
+ class CriticalError(CodexError):
45
+ """System-level failures requiring intervention."""
46
+
47
+ severity = Severity.CRITICAL
48
+ recoverable = False
49
+
50
+
51
+ class CircuitOpenError(CriticalError):
52
+ """Raised when a circuit breaker is open."""
53
+
54
+ def __init__(self, service_name: str, message: Optional[str] = None) -> None:
55
+ self.service_name = service_name
56
+ msg = message or f"Circuit breaker is open for {service_name}"
57
+ super().__init__(
58
+ msg,
59
+ user_message=f"{service_name} is temporarily unavailable. Please try again later.",
60
+ )
@@ -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,178 @@
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
+ return cleared
107
+
108
+ def get_status(self, run_id: str) -> Optional[FlowRunRecord]:
109
+ return self.store.get_flow_run(run_id)
110
+
111
+ def list_runs(self, status: Optional[FlowRunStatus] = None) -> list[FlowRunRecord]:
112
+ return self.store.list_flow_runs(
113
+ flow_type=self.definition.flow_type, status=status
114
+ )
115
+
116
+ async def stream_events(
117
+ self, run_id: str, after_seq: Optional[int] = None
118
+ ) -> AsyncGenerator[FlowEvent, None]:
119
+ last_seq = after_seq
120
+
121
+ while True:
122
+ events = self.store.get_events(
123
+ run_id=run_id,
124
+ after_seq=last_seq,
125
+ limit=100,
126
+ )
127
+
128
+ for event in events:
129
+ yield event
130
+ last_seq = event.seq
131
+
132
+ record = self.store.get_flow_run(run_id)
133
+ if (
134
+ record
135
+ and (record.status.is_terminal() or record.status.is_paused())
136
+ and not events
137
+ ):
138
+ break
139
+
140
+ await asyncio.sleep(0.5)
141
+
142
+ def get_events(
143
+ self, run_id: str, after_seq: Optional[int] = None
144
+ ) -> list[FlowEvent]:
145
+ return self.store.get_events(run_id=run_id, after_seq=after_seq)
146
+
147
+ def add_event_listener(self, listener: Callable[[FlowEvent], None]) -> None:
148
+ self._event_listeners.add(listener)
149
+
150
+ def remove_event_listener(self, listener: Callable[[FlowEvent], None]) -> None:
151
+ self._event_listeners.discard(listener)
152
+
153
+ def _emit_event(self, event: FlowEvent) -> None:
154
+ for listener in self._event_listeners:
155
+ try:
156
+ listener(event)
157
+ except Exception as e:
158
+ _logger.exception("Error in event listener: %s", e)
159
+
160
+ def _prepare_artifacts_dir(self, run_id: str) -> Path:
161
+ artifacts_dir = self.artifacts_root / run_id
162
+ artifacts_dir.mkdir(parents=True, exist_ok=True)
163
+ return artifacts_dir
164
+
165
+ def get_artifacts_dir(self, run_id: str) -> Optional[Path]:
166
+ artifacts_dir = self.artifacts_root / run_id
167
+ if artifacts_dir.exists():
168
+ return artifacts_dir
169
+ return None
170
+
171
+ def get_artifacts(self, run_id: str) -> list:
172
+ return self.store.get_artifacts(run_id)
173
+
174
+ async def stream_events_since(
175
+ self, run_id: str, start_seq: Optional[int] = None
176
+ ) -> AsyncGenerator[FlowEvent, None]:
177
+ async for event in self.stream_events(run_id, after_seq=start_seq):
178
+ 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,75 @@
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
+ APP_SERVER_EVENT = "app_server_event"
36
+ TOKEN_USAGE = "token_usage"
37
+ FLOW_STARTED = "flow_started"
38
+ FLOW_STOPPED = "flow_stopped"
39
+ FLOW_RESUMED = "flow_resumed"
40
+ FLOW_COMPLETED = "flow_completed"
41
+ FLOW_FAILED = "flow_failed"
42
+
43
+
44
+ class FlowRunRecord(BaseModel):
45
+ id: str
46
+ flow_type: str
47
+ status: FlowRunStatus
48
+ input_data: Dict[str, Any] = Field(default_factory=dict)
49
+ state: Dict[str, Any] = Field(default_factory=dict)
50
+ current_step: Optional[str] = None
51
+ stop_requested: bool = False
52
+ created_at: str
53
+ started_at: Optional[str] = None
54
+ finished_at: Optional[str] = None
55
+ error_message: Optional[str] = None
56
+ metadata: Dict[str, Any] = Field(default_factory=dict)
57
+
58
+
59
+ class FlowEvent(BaseModel):
60
+ seq: int
61
+ id: str
62
+ run_id: str
63
+ event_type: FlowEventType
64
+ timestamp: str
65
+ data: Dict[str, Any] = Field(default_factory=dict)
66
+ step_id: Optional[str] = None
67
+
68
+
69
+ class FlowArtifact(BaseModel):
70
+ id: str
71
+ run_id: str
72
+ kind: str
73
+ path: str
74
+ created_at: str
75
+ metadata: Dict[str, Any] = Field(default_factory=dict)