codex-autorunner 0.1.2__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 (189) hide show
  1. codex_autorunner/__main__.py +4 -0
  2. codex_autorunner/agents/opencode/client.py +68 -35
  3. codex_autorunner/agents/opencode/logging.py +21 -5
  4. codex_autorunner/agents/opencode/run_prompt.py +1 -0
  5. codex_autorunner/agents/opencode/runtime.py +118 -30
  6. codex_autorunner/agents/opencode/supervisor.py +36 -48
  7. codex_autorunner/agents/registry.py +136 -8
  8. codex_autorunner/api.py +25 -0
  9. codex_autorunner/bootstrap.py +16 -35
  10. codex_autorunner/cli.py +157 -139
  11. codex_autorunner/core/about_car.py +44 -32
  12. codex_autorunner/core/adapter_utils.py +21 -0
  13. codex_autorunner/core/app_server_logging.py +7 -3
  14. codex_autorunner/core/app_server_prompts.py +27 -260
  15. codex_autorunner/core/app_server_threads.py +15 -26
  16. codex_autorunner/core/codex_runner.py +6 -0
  17. codex_autorunner/core/config.py +390 -100
  18. codex_autorunner/core/docs.py +10 -2
  19. codex_autorunner/core/drafts.py +82 -0
  20. codex_autorunner/core/engine.py +278 -262
  21. codex_autorunner/core/flows/__init__.py +25 -0
  22. codex_autorunner/core/flows/controller.py +178 -0
  23. codex_autorunner/core/flows/definition.py +82 -0
  24. codex_autorunner/core/flows/models.py +75 -0
  25. codex_autorunner/core/flows/runtime.py +351 -0
  26. codex_autorunner/core/flows/store.py +485 -0
  27. codex_autorunner/core/flows/transition.py +133 -0
  28. codex_autorunner/core/flows/worker_process.py +242 -0
  29. codex_autorunner/core/hub.py +15 -9
  30. codex_autorunner/core/locks.py +4 -0
  31. codex_autorunner/core/prompt.py +15 -7
  32. codex_autorunner/core/redaction.py +29 -0
  33. codex_autorunner/core/review_context.py +5 -8
  34. codex_autorunner/core/run_index.py +6 -0
  35. codex_autorunner/core/runner_process.py +5 -2
  36. codex_autorunner/core/state.py +0 -88
  37. codex_autorunner/core/static_assets.py +55 -0
  38. codex_autorunner/core/supervisor_utils.py +67 -0
  39. codex_autorunner/core/update.py +20 -11
  40. codex_autorunner/core/update_runner.py +2 -0
  41. codex_autorunner/core/utils.py +29 -2
  42. codex_autorunner/discovery.py +2 -4
  43. codex_autorunner/flows/ticket_flow/__init__.py +3 -0
  44. codex_autorunner/flows/ticket_flow/definition.py +91 -0
  45. codex_autorunner/integrations/agents/__init__.py +27 -0
  46. codex_autorunner/integrations/agents/agent_backend.py +142 -0
  47. codex_autorunner/integrations/agents/codex_backend.py +307 -0
  48. codex_autorunner/integrations/agents/opencode_backend.py +325 -0
  49. codex_autorunner/integrations/agents/run_event.py +71 -0
  50. codex_autorunner/integrations/app_server/client.py +576 -92
  51. codex_autorunner/integrations/app_server/supervisor.py +59 -33
  52. codex_autorunner/integrations/telegram/adapter.py +141 -167
  53. codex_autorunner/integrations/telegram/api_schemas.py +120 -0
  54. codex_autorunner/integrations/telegram/config.py +175 -0
  55. codex_autorunner/integrations/telegram/constants.py +16 -1
  56. codex_autorunner/integrations/telegram/dispatch.py +17 -0
  57. codex_autorunner/integrations/telegram/doctor.py +47 -0
  58. codex_autorunner/integrations/telegram/handlers/callbacks.py +0 -4
  59. codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
  60. codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
  61. codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
  62. codex_autorunner/integrations/telegram/handlers/commands/flows.py +227 -0
  63. codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
  64. codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
  65. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
  66. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +133 -475
  67. codex_autorunner/integrations/telegram/handlers/commands_spec.py +11 -4
  68. codex_autorunner/integrations/telegram/handlers/messages.py +120 -9
  69. codex_autorunner/integrations/telegram/helpers.py +88 -16
  70. codex_autorunner/integrations/telegram/outbox.py +208 -37
  71. codex_autorunner/integrations/telegram/progress_stream.py +3 -10
  72. codex_autorunner/integrations/telegram/service.py +214 -40
  73. codex_autorunner/integrations/telegram/state.py +100 -2
  74. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
  75. codex_autorunner/integrations/telegram/transport.py +36 -3
  76. codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
  77. codex_autorunner/manifest.py +2 -0
  78. codex_autorunner/plugin_api.py +22 -0
  79. codex_autorunner/routes/__init__.py +23 -14
  80. codex_autorunner/routes/analytics.py +239 -0
  81. codex_autorunner/routes/base.py +81 -109
  82. codex_autorunner/routes/file_chat.py +836 -0
  83. codex_autorunner/routes/flows.py +980 -0
  84. codex_autorunner/routes/messages.py +459 -0
  85. codex_autorunner/routes/system.py +6 -1
  86. codex_autorunner/routes/usage.py +87 -0
  87. codex_autorunner/routes/workspace.py +271 -0
  88. codex_autorunner/server.py +2 -1
  89. codex_autorunner/static/agentControls.js +1 -0
  90. codex_autorunner/static/agentEvents.js +248 -0
  91. codex_autorunner/static/app.js +25 -22
  92. codex_autorunner/static/autoRefresh.js +29 -1
  93. codex_autorunner/static/bootstrap.js +1 -0
  94. codex_autorunner/static/bus.js +1 -0
  95. codex_autorunner/static/cache.js +1 -0
  96. codex_autorunner/static/constants.js +20 -4
  97. codex_autorunner/static/dashboard.js +162 -196
  98. codex_autorunner/static/diffRenderer.js +37 -0
  99. codex_autorunner/static/docChatCore.js +324 -0
  100. codex_autorunner/static/docChatStorage.js +65 -0
  101. codex_autorunner/static/docChatVoice.js +65 -0
  102. codex_autorunner/static/docEditor.js +133 -0
  103. codex_autorunner/static/env.js +1 -0
  104. codex_autorunner/static/eventSummarizer.js +166 -0
  105. codex_autorunner/static/fileChat.js +182 -0
  106. codex_autorunner/static/health.js +155 -0
  107. codex_autorunner/static/hub.js +41 -118
  108. codex_autorunner/static/index.html +787 -858
  109. codex_autorunner/static/liveUpdates.js +1 -0
  110. codex_autorunner/static/loader.js +1 -0
  111. codex_autorunner/static/messages.js +470 -0
  112. codex_autorunner/static/mobileCompact.js +2 -1
  113. codex_autorunner/static/settings.js +24 -211
  114. codex_autorunner/static/styles.css +7567 -3865
  115. codex_autorunner/static/tabs.js +28 -5
  116. codex_autorunner/static/terminal.js +14 -0
  117. codex_autorunner/static/terminalManager.js +34 -59
  118. codex_autorunner/static/ticketChatActions.js +333 -0
  119. codex_autorunner/static/ticketChatEvents.js +16 -0
  120. codex_autorunner/static/ticketChatStorage.js +16 -0
  121. codex_autorunner/static/ticketChatStream.js +264 -0
  122. codex_autorunner/static/ticketEditor.js +750 -0
  123. codex_autorunner/static/ticketVoice.js +9 -0
  124. codex_autorunner/static/tickets.js +1315 -0
  125. codex_autorunner/static/utils.js +32 -3
  126. codex_autorunner/static/voice.js +1 -0
  127. codex_autorunner/static/workspace.js +672 -0
  128. codex_autorunner/static/workspaceApi.js +53 -0
  129. codex_autorunner/static/workspaceFileBrowser.js +504 -0
  130. codex_autorunner/tickets/__init__.py +20 -0
  131. codex_autorunner/tickets/agent_pool.py +377 -0
  132. codex_autorunner/tickets/files.py +85 -0
  133. codex_autorunner/tickets/frontmatter.py +55 -0
  134. codex_autorunner/tickets/lint.py +102 -0
  135. codex_autorunner/tickets/models.py +95 -0
  136. codex_autorunner/tickets/outbox.py +232 -0
  137. codex_autorunner/tickets/replies.py +179 -0
  138. codex_autorunner/tickets/runner.py +823 -0
  139. codex_autorunner/tickets/spec_ingest.py +77 -0
  140. codex_autorunner/web/app.py +269 -91
  141. codex_autorunner/web/middleware.py +3 -4
  142. codex_autorunner/web/schemas.py +89 -109
  143. codex_autorunner/web/static_assets.py +1 -44
  144. codex_autorunner/workspace/__init__.py +40 -0
  145. codex_autorunner/workspace/paths.py +319 -0
  146. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +18 -21
  147. codex_autorunner-1.0.0.dist-info/RECORD +251 -0
  148. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/WHEEL +1 -1
  149. codex_autorunner/agents/execution/policy.py +0 -292
  150. codex_autorunner/agents/factory.py +0 -52
  151. codex_autorunner/agents/orchestrator.py +0 -358
  152. codex_autorunner/core/doc_chat.py +0 -1446
  153. codex_autorunner/core/snapshot.py +0 -580
  154. codex_autorunner/integrations/github/chatops.py +0 -268
  155. codex_autorunner/integrations/github/pr_flow.py +0 -1314
  156. codex_autorunner/routes/docs.py +0 -381
  157. codex_autorunner/routes/github.py +0 -327
  158. codex_autorunner/routes/runs.py +0 -250
  159. codex_autorunner/spec_ingest.py +0 -812
  160. codex_autorunner/static/docChatActions.js +0 -287
  161. codex_autorunner/static/docChatEvents.js +0 -300
  162. codex_autorunner/static/docChatRender.js +0 -205
  163. codex_autorunner/static/docChatStream.js +0 -361
  164. codex_autorunner/static/docs.js +0 -20
  165. codex_autorunner/static/docsClipboard.js +0 -69
  166. codex_autorunner/static/docsCrud.js +0 -257
  167. codex_autorunner/static/docsDocUpdates.js +0 -62
  168. codex_autorunner/static/docsDrafts.js +0 -16
  169. codex_autorunner/static/docsElements.js +0 -69
  170. codex_autorunner/static/docsInit.js +0 -285
  171. codex_autorunner/static/docsParse.js +0 -160
  172. codex_autorunner/static/docsSnapshot.js +0 -87
  173. codex_autorunner/static/docsSpecIngest.js +0 -263
  174. codex_autorunner/static/docsState.js +0 -127
  175. codex_autorunner/static/docsThreadRegistry.js +0 -44
  176. codex_autorunner/static/docsUi.js +0 -153
  177. codex_autorunner/static/docsVoice.js +0 -56
  178. codex_autorunner/static/github.js +0 -504
  179. codex_autorunner/static/logs.js +0 -678
  180. codex_autorunner/static/review.js +0 -157
  181. codex_autorunner/static/runs.js +0 -418
  182. codex_autorunner/static/snapshot.js +0 -124
  183. codex_autorunner/static/state.js +0 -94
  184. codex_autorunner/static/todoPreview.js +0 -27
  185. codex_autorunner/workspace.py +0 -16
  186. codex_autorunner-0.1.2.dist-info/RECORD +0 -222
  187. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
  188. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
  189. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import time
5
+ from typing import Any, Callable, TypeVar
6
+
7
+ from .logging_utils import log_event
8
+
9
+ HandleT = TypeVar("HandleT", bound=Any)
10
+
11
+
12
+ def evict_lru_handle_locked(
13
+ handles: dict[str, HandleT],
14
+ max_handles: int | None,
15
+ logger: logging.Logger,
16
+ event_prefix: str,
17
+ *,
18
+ last_used_at_getter: Callable[[HandleT], float],
19
+ ) -> HandleT | None:
20
+ if not max_handles or max_handles <= 0:
21
+ return None
22
+ if len(handles) < max_handles:
23
+ return None
24
+ lru_handle = min(handles.values(), key=last_used_at_getter)
25
+ log_event(
26
+ logger,
27
+ logging.INFO,
28
+ f"{event_prefix}.handle.evicted",
29
+ reason="max_handles",
30
+ workspace_id=lru_handle.workspace_id,
31
+ workspace_root=str(lru_handle.workspace_root),
32
+ max_handles=max_handles,
33
+ handle_count=len(handles),
34
+ last_used_at=last_used_at_getter(lru_handle),
35
+ )
36
+ handles.pop(lru_handle.workspace_id, None)
37
+ return lru_handle
38
+
39
+
40
+ def pop_idle_handles_locked(
41
+ handles: dict[str, HandleT],
42
+ idle_ttl_seconds: float | None,
43
+ logger: logging.Logger,
44
+ event_prefix: str,
45
+ *,
46
+ last_used_at_getter: Callable[[HandleT], float],
47
+ should_skip_prune: Callable[[HandleT], bool] | None = None,
48
+ ) -> list[HandleT]:
49
+ if not idle_ttl_seconds or idle_ttl_seconds <= 0:
50
+ return []
51
+ cutoff = time.monotonic() - idle_ttl_seconds
52
+ stale: list[HandleT] = []
53
+ for handle in list(handles.values()):
54
+ if should_skip_prune and should_skip_prune(handle):
55
+ log_event(
56
+ logger,
57
+ logging.INFO,
58
+ f"{event_prefix}.handle.prune.skipped",
59
+ reason="should_skip",
60
+ workspace_id=handle.workspace_id,
61
+ workspace_root=str(handle.workspace_root),
62
+ )
63
+ continue
64
+ if last_used_at_getter(handle) and last_used_at_getter(handle) < cutoff:
65
+ handles.pop(handle.workspace_id, None)
66
+ stale.append(handle)
67
+ return stale
@@ -378,6 +378,7 @@ def _system_update_worker(
378
378
  update_dir: Path,
379
379
  logger: logging.Logger,
380
380
  update_target: str = "both",
381
+ skip_checks: bool = False,
381
382
  ) -> None:
382
383
  status_path = _update_status_path()
383
384
  lock_acquired = False
@@ -457,10 +458,14 @@ def _system_update_worker(
457
458
  _run_cmd(["git", "fetch", "origin", repo_ref], cwd=update_dir)
458
459
  _run_cmd(["git", "reset", "--hard", "FETCH_HEAD"], cwd=update_dir)
459
460
 
460
- if os.environ.get("CODEX_AUTORUNNER_SKIP_UPDATE_CHECKS") == "1":
461
- logger.info(
462
- "Skipping update checks (CODEX_AUTORUNNER_SKIP_UPDATE_CHECKS=1)."
463
- )
461
+ skip_checks_env = os.environ.get("CODEX_AUTORUNNER_SKIP_UPDATE_CHECKS") == "1"
462
+ if skip_checks_env or skip_checks:
463
+ if skip_checks_env:
464
+ logger.info(
465
+ "Skipping update checks (CODEX_AUTORUNNER_SKIP_UPDATE_CHECKS=1)."
466
+ )
467
+ else:
468
+ logger.info("Skipping update checks (update.skip_checks=true).")
464
469
  else:
465
470
  logger.info("Running checks...")
466
471
  try:
@@ -526,6 +531,7 @@ def _spawn_update_process(
526
531
  update_dir: Path,
527
532
  logger: logging.Logger,
528
533
  update_target: str = "both",
534
+ skip_checks: bool = False,
529
535
  notify_chat_id: Optional[int] = None,
530
536
  notify_thread_id: Optional[int] = None,
531
537
  notify_reply_to: Optional[int] = None,
@@ -565,14 +571,17 @@ def _spawn_update_process(
565
571
  "--log-path",
566
572
  str(log_path),
567
573
  ]
574
+ if skip_checks:
575
+ cmd.append("--skip-checks")
568
576
  try:
569
- subprocess.Popen(
570
- cmd,
571
- cwd=str(update_dir.parent),
572
- start_new_session=True,
573
- stdout=subprocess.DEVNULL,
574
- stderr=subprocess.DEVNULL,
575
- )
577
+ with log_path.open("a", encoding="utf-8") as log_file:
578
+ subprocess.Popen(
579
+ cmd,
580
+ cwd=str(update_dir.parent),
581
+ start_new_session=True,
582
+ stdout=log_file,
583
+ stderr=log_file,
584
+ )
576
585
  except Exception:
577
586
  logger.exception("Failed to spawn update worker")
578
587
  _write_update_status(
@@ -23,6 +23,7 @@ def main(argv: list[str] | None = None) -> int:
23
23
  parser.add_argument("--update-dir", required=True)
24
24
  parser.add_argument("--log-path", required=True)
25
25
  parser.add_argument("--target", default="both")
26
+ parser.add_argument("--skip-checks", action="store_true")
26
27
  args = parser.parse_args(argv)
27
28
 
28
29
  update_dir = Path(args.update_dir).expanduser()
@@ -36,6 +37,7 @@ def main(argv: list[str] | None = None) -> int:
36
37
  update_dir=update_dir,
37
38
  logger=logger,
38
39
  update_target=args.target,
40
+ skip_checks=bool(args.skip_checks),
39
41
  )
40
42
  return 0
41
43
 
@@ -1,3 +1,4 @@
1
+ import contextvars
1
2
  import json
2
3
  import logging
3
4
  import os
@@ -23,8 +24,32 @@ class RepoNotFoundError(Exception):
23
24
  pass
24
25
 
25
26
 
26
- def find_repo_root(start: Path) -> Path:
27
- current = start.resolve()
27
+ _repo_root_ctx: contextvars.ContextVar[Optional[Path]] = contextvars.ContextVar(
28
+ "codex_autorunner_repo_root", default=None
29
+ )
30
+
31
+
32
+ def set_repo_root_context(
33
+ repo_root: Optional[Path],
34
+ ) -> contextvars.Token[Optional[Path]]:
35
+ """Set the current repo root for the active context."""
36
+ return _repo_root_ctx.set(repo_root.resolve() if repo_root else None)
37
+
38
+
39
+ def reset_repo_root_context(token: contextvars.Token[Optional[Path]]) -> None:
40
+ _repo_root_ctx.reset(token)
41
+
42
+
43
+ def get_repo_root_context() -> Optional[Path]:
44
+ return _repo_root_ctx.get()
45
+
46
+
47
+ def find_repo_root(start: Optional[Path] = None) -> Path:
48
+ ctx_root = get_repo_root_context()
49
+ if ctx_root is not None and (ctx_root / ".git").exists():
50
+ return ctx_root
51
+
52
+ current = (start or Path.cwd()).resolve()
28
53
  for parent in [current] + list(current.parents):
29
54
  if (parent / ".git").exists():
30
55
  return parent
@@ -190,6 +215,7 @@ def build_opencode_supervisor(
190
215
  request_timeout: Optional[float] = None,
191
216
  max_handles: Optional[int] = None,
192
217
  idle_ttl_seconds: Optional[float] = None,
218
+ session_stall_timeout_seconds: Optional[float] = None,
193
219
  base_env: Optional[MutableMapping[str, str]] = None,
194
220
  subagent_models: Optional[Mapping[str, str]] = None,
195
221
  ) -> Optional["OpenCodeSupervisor"]:
@@ -244,6 +270,7 @@ def build_opencode_supervisor(
244
270
  request_timeout=request_timeout,
245
271
  max_handles=max_handles,
246
272
  idle_ttl_seconds=idle_ttl_seconds,
273
+ session_stall_timeout_seconds=session_stall_timeout_seconds,
247
274
  username=username if password else None,
248
275
  password=password if password else None,
249
276
  base_env=base_env,
@@ -57,7 +57,7 @@ def discover_and_init(hub_config: HubConfig) -> Tuple[Manifest, List[DiscoveryRe
57
57
 
58
58
  def _record_repo(repo_entry: ManifestRepo, *, added: bool) -> None:
59
59
  repo_path = (hub_config.root / repo_entry.path).resolve()
60
- initialized = (repo_path / ".codex-autorunner" / "state.sqlite3").exists()
60
+ initialized = (repo_path / ".codex-autorunner" / "tickets").exists()
61
61
  init_error: Optional[str] = None
62
62
  if hub_config.auto_init_missing and repo_path.exists() and not initialized:
63
63
  try:
@@ -176,9 +176,7 @@ def discover_and_init(hub_config: HubConfig) -> Tuple[Manifest, List[DiscoveryRe
176
176
  absolute_path=repo_path,
177
177
  added_to_manifest=False,
178
178
  exists_on_disk=repo_path.exists(),
179
- initialized=(
180
- repo_path / ".codex-autorunner" / "state.sqlite3"
181
- ).exists(),
179
+ initialized=(repo_path / ".codex-autorunner" / "tickets").exists(),
182
180
  init_error=None,
183
181
  )
184
182
  )
@@ -0,0 +1,3 @@
1
+ from .definition import build_ticket_flow_definition
2
+
3
+ __all__ = ["build_ticket_flow_definition"]
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any, Dict, Optional
5
+
6
+ from ...core.flows.definition import EmitEventFn, FlowDefinition, StepOutcome
7
+ from ...core.flows.models import FlowEventType, FlowRunRecord
8
+ from ...core.utils import find_repo_root
9
+ from ...tickets import AgentPool, TicketRunConfig, TicketRunner
10
+
11
+
12
+ def build_ticket_flow_definition(*, agent_pool: AgentPool) -> FlowDefinition:
13
+ """Build the single-step ticket runner flow.
14
+
15
+ The flow is intentionally simple: each step executes at most one agent turn
16
+ against the current ticket, and re-schedules itself until paused or complete.
17
+ """
18
+
19
+ async def _ticket_turn_step(
20
+ record: FlowRunRecord,
21
+ input_data: Dict[str, Any],
22
+ emit_event: Optional[EmitEventFn],
23
+ ) -> StepOutcome:
24
+ # Namespace all state under `ticket_engine` to avoid collisions with other flows.
25
+ engine_state = (
26
+ record.state.get("ticket_engine")
27
+ if isinstance(record.state, dict)
28
+ else None
29
+ )
30
+ engine_state = dict(engine_state) if isinstance(engine_state, dict) else {}
31
+
32
+ repo_root = find_repo_root()
33
+ workspace_root = Path(input_data.get("workspace_root") or repo_root)
34
+ ticket_dir = Path(input_data.get("ticket_dir") or ".codex-autorunner/tickets")
35
+ runs_dir = Path(input_data.get("runs_dir") or ".codex-autorunner/runs")
36
+ max_total_turns = int(input_data.get("max_total_turns") or 25)
37
+ max_lint_retries = int(input_data.get("max_lint_retries") or 3)
38
+ max_commit_retries = int(input_data.get("max_commit_retries") or 2)
39
+ auto_commit = bool(
40
+ input_data.get("auto_commit") if "auto_commit" in input_data else True
41
+ )
42
+
43
+ runner = TicketRunner(
44
+ workspace_root=workspace_root,
45
+ run_id=str(record.id),
46
+ config=TicketRunConfig(
47
+ ticket_dir=ticket_dir,
48
+ runs_dir=runs_dir,
49
+ max_total_turns=max_total_turns,
50
+ max_lint_retries=max_lint_retries,
51
+ max_commit_retries=max_commit_retries,
52
+ auto_commit=auto_commit,
53
+ ),
54
+ agent_pool=agent_pool,
55
+ )
56
+
57
+ if emit_event is not None:
58
+ emit_event(FlowEventType.STEP_PROGRESS, {"message": "Running ticket turn"})
59
+ result = await runner.step(engine_state, emit_event=emit_event)
60
+ out_state = dict(record.state or {})
61
+ out_state["ticket_engine"] = result.state
62
+
63
+ if result.status == "completed":
64
+ return StepOutcome.complete(output=out_state)
65
+ if result.status == "paused":
66
+ return StepOutcome.pause(output=out_state)
67
+ if result.status == "failed":
68
+ return StepOutcome.fail(
69
+ error=result.reason or "Ticket engine failed", output=out_state
70
+ )
71
+ return StepOutcome.continue_to(next_steps={"ticket_turn"}, output=out_state)
72
+
73
+ return FlowDefinition(
74
+ flow_type="ticket_flow",
75
+ name="Ticket Flow",
76
+ description="Ticket-based agent workflow runner",
77
+ initial_step="ticket_turn",
78
+ input_schema={
79
+ "type": "object",
80
+ "properties": {
81
+ "workspace_root": {"type": "string"},
82
+ "ticket_dir": {"type": "string"},
83
+ "runs_dir": {"type": "string"},
84
+ "max_total_turns": {"type": "integer"},
85
+ "max_lint_retries": {"type": "integer"},
86
+ "max_commit_retries": {"type": "integer"},
87
+ "auto_commit": {"type": "boolean"},
88
+ },
89
+ },
90
+ steps={"ticket_turn": _ticket_turn_step},
91
+ )
@@ -0,0 +1,27 @@
1
+ from .agent_backend import AgentBackend, AgentEvent, AgentEventType
2
+ from .codex_backend import CodexAppServerBackend
3
+ from .opencode_backend import OpenCodeBackend
4
+ from .run_event import (
5
+ ApprovalRequested,
6
+ Completed,
7
+ Failed,
8
+ OutputDelta,
9
+ RunEvent,
10
+ Started,
11
+ ToolCall,
12
+ )
13
+
14
+ __all__ = [
15
+ "AgentBackend",
16
+ "AgentEvent",
17
+ "AgentEventType",
18
+ "CodexAppServerBackend",
19
+ "OpenCodeBackend",
20
+ "RunEvent",
21
+ "Started",
22
+ "OutputDelta",
23
+ "ToolCall",
24
+ "ApprovalRequested",
25
+ "Completed",
26
+ "Failed",
27
+ ]
@@ -0,0 +1,142 @@
1
+ import logging
2
+ from dataclasses import dataclass, field
3
+ from datetime import datetime, timezone
4
+ from enum import Enum
5
+ from typing import Any, AsyncGenerator, Dict, Optional
6
+
7
+ _logger = logging.getLogger(__name__)
8
+
9
+
10
+ def now_iso() -> str:
11
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
12
+
13
+
14
+ class AgentEventType(str, Enum):
15
+ STREAM_DELTA = "stream_delta"
16
+ TOOL_CALL = "tool_call"
17
+ TOOL_RESULT = "tool_result"
18
+ MESSAGE_COMPLETE = "message_complete"
19
+ ERROR = "error"
20
+ APPROVAL_REQUESTED = "approval_requested"
21
+ APPROVAL_GRANTED = "approval_granted"
22
+ APPROVAL_DENIED = "approval_denied"
23
+ SESSION_STARTED = "session_started"
24
+ SESSION_ENDED = "session_ended"
25
+ SESION_STARTED = "session_started" # legacy typo kept for backward tests
26
+
27
+
28
+ @dataclass
29
+ class AgentEvent:
30
+ type: str
31
+ timestamp: str
32
+ data: Dict[str, Any] = field(default_factory=dict)
33
+
34
+ @property
35
+ def event_type(self) -> AgentEventType:
36
+ try:
37
+ return AgentEventType(self.type)
38
+ except ValueError:
39
+ return AgentEventType.ERROR
40
+
41
+ @classmethod
42
+ def stream_delta(cls, content: str, delta_type: str = "text") -> "AgentEvent":
43
+ return cls(
44
+ type=AgentEventType.STREAM_DELTA.value,
45
+ timestamp=now_iso(),
46
+ data={"content": content, "delta_type": delta_type},
47
+ )
48
+
49
+ @classmethod
50
+ def tool_call(cls, tool_name: str, tool_input: Dict[str, Any]) -> "AgentEvent":
51
+ return cls(
52
+ type=AgentEventType.TOOL_CALL.value,
53
+ timestamp=now_iso(),
54
+ data={"tool_name": tool_name, "tool_input": tool_input},
55
+ )
56
+
57
+ @classmethod
58
+ def tool_result(
59
+ cls, tool_name: str, result: Any, error: Optional[str] = None
60
+ ) -> "AgentEvent":
61
+ return cls(
62
+ type=AgentEventType.TOOL_RESULT.value,
63
+ timestamp=now_iso(),
64
+ data={"tool_name": tool_name, "result": result, "error": error},
65
+ )
66
+
67
+ @classmethod
68
+ def message_complete(cls, final_message: str) -> "AgentEvent":
69
+ return cls(
70
+ type=AgentEventType.MESSAGE_COMPLETE.value,
71
+ timestamp=now_iso(),
72
+ data={"final_message": final_message},
73
+ )
74
+
75
+ @classmethod
76
+ def error(cls, error_message: str) -> "AgentEvent":
77
+ return cls(
78
+ type=AgentEventType.ERROR.value,
79
+ timestamp=now_iso(),
80
+ data={"error": error_message},
81
+ )
82
+
83
+ @classmethod
84
+ def approval_requested(
85
+ cls, request_id: str, description: str, context: Optional[Dict[str, Any]] = None
86
+ ) -> "AgentEvent":
87
+ return cls(
88
+ type=AgentEventType.APPROVAL_REQUESTED.value,
89
+ timestamp=now_iso(),
90
+ data={
91
+ "request_id": request_id,
92
+ "description": description,
93
+ "context": context or {},
94
+ },
95
+ )
96
+
97
+ @classmethod
98
+ def approval_granted(cls, request_id: str) -> "AgentEvent":
99
+ return cls(
100
+ type=AgentEventType.APPROVAL_GRANTED.value,
101
+ timestamp=now_iso(),
102
+ data={"request_id": request_id},
103
+ )
104
+
105
+ @classmethod
106
+ def approval_denied(
107
+ cls, request_id: str, reason: Optional[str] = None
108
+ ) -> "AgentEvent":
109
+ return cls(
110
+ type=AgentEventType.APPROVAL_DENIED.value,
111
+ timestamp=now_iso(),
112
+ data={"request_id": request_id, "reason": reason},
113
+ )
114
+
115
+
116
+ class AgentBackend:
117
+ async def start_session(self, target: dict, context: dict) -> str:
118
+ raise NotImplementedError
119
+
120
+ async def run_turn(
121
+ self, session_id: str, message: str
122
+ ) -> AsyncGenerator[AgentEvent, None]:
123
+ raise NotImplementedError
124
+
125
+ async def stream_events(self, session_id: str) -> AsyncGenerator[AgentEvent, None]:
126
+ raise NotImplementedError
127
+
128
+ async def run_turn_events(
129
+ self, session_id: str, message: str
130
+ ) -> AsyncGenerator[Any, None]:
131
+ raise NotImplementedError
132
+
133
+ async def interrupt(self, session_id: str) -> None:
134
+ raise NotImplementedError
135
+
136
+ async def final_messages(self, session_id: str) -> list[str]:
137
+ raise NotImplementedError
138
+
139
+ async def request_approval(
140
+ self, description: str, context: Optional[Dict[str, Any]] = None
141
+ ) -> bool:
142
+ raise NotImplementedError