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.
- codex_autorunner/__main__.py +4 -0
- codex_autorunner/agents/opencode/client.py +68 -35
- codex_autorunner/agents/opencode/logging.py +21 -5
- codex_autorunner/agents/opencode/run_prompt.py +1 -0
- codex_autorunner/agents/opencode/runtime.py +118 -30
- codex_autorunner/agents/opencode/supervisor.py +36 -48
- codex_autorunner/agents/registry.py +136 -8
- codex_autorunner/api.py +25 -0
- codex_autorunner/bootstrap.py +16 -35
- codex_autorunner/cli.py +157 -139
- codex_autorunner/core/about_car.py +44 -32
- codex_autorunner/core/adapter_utils.py +21 -0
- codex_autorunner/core/app_server_logging.py +7 -3
- codex_autorunner/core/app_server_prompts.py +27 -260
- codex_autorunner/core/app_server_threads.py +15 -26
- codex_autorunner/core/codex_runner.py +6 -0
- codex_autorunner/core/config.py +390 -100
- codex_autorunner/core/docs.py +10 -2
- codex_autorunner/core/drafts.py +82 -0
- codex_autorunner/core/engine.py +278 -262
- codex_autorunner/core/flows/__init__.py +25 -0
- codex_autorunner/core/flows/controller.py +178 -0
- codex_autorunner/core/flows/definition.py +82 -0
- codex_autorunner/core/flows/models.py +75 -0
- codex_autorunner/core/flows/runtime.py +351 -0
- codex_autorunner/core/flows/store.py +485 -0
- codex_autorunner/core/flows/transition.py +133 -0
- codex_autorunner/core/flows/worker_process.py +242 -0
- codex_autorunner/core/hub.py +15 -9
- codex_autorunner/core/locks.py +4 -0
- codex_autorunner/core/prompt.py +15 -7
- codex_autorunner/core/redaction.py +29 -0
- codex_autorunner/core/review_context.py +5 -8
- codex_autorunner/core/run_index.py +6 -0
- codex_autorunner/core/runner_process.py +5 -2
- codex_autorunner/core/state.py +0 -88
- codex_autorunner/core/static_assets.py +55 -0
- codex_autorunner/core/supervisor_utils.py +67 -0
- codex_autorunner/core/update.py +20 -11
- codex_autorunner/core/update_runner.py +2 -0
- codex_autorunner/core/utils.py +29 -2
- codex_autorunner/discovery.py +2 -4
- codex_autorunner/flows/ticket_flow/__init__.py +3 -0
- codex_autorunner/flows/ticket_flow/definition.py +91 -0
- codex_autorunner/integrations/agents/__init__.py +27 -0
- codex_autorunner/integrations/agents/agent_backend.py +142 -0
- codex_autorunner/integrations/agents/codex_backend.py +307 -0
- codex_autorunner/integrations/agents/opencode_backend.py +325 -0
- codex_autorunner/integrations/agents/run_event.py +71 -0
- codex_autorunner/integrations/app_server/client.py +576 -92
- codex_autorunner/integrations/app_server/supervisor.py +59 -33
- codex_autorunner/integrations/telegram/adapter.py +141 -167
- codex_autorunner/integrations/telegram/api_schemas.py +120 -0
- codex_autorunner/integrations/telegram/config.py +175 -0
- codex_autorunner/integrations/telegram/constants.py +16 -1
- codex_autorunner/integrations/telegram/dispatch.py +17 -0
- codex_autorunner/integrations/telegram/doctor.py +47 -0
- codex_autorunner/integrations/telegram/handlers/callbacks.py +0 -4
- codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
- codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +227 -0
- codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
- codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +133 -475
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +11 -4
- codex_autorunner/integrations/telegram/handlers/messages.py +120 -9
- codex_autorunner/integrations/telegram/helpers.py +88 -16
- codex_autorunner/integrations/telegram/outbox.py +208 -37
- codex_autorunner/integrations/telegram/progress_stream.py +3 -10
- codex_autorunner/integrations/telegram/service.py +214 -40
- codex_autorunner/integrations/telegram/state.py +100 -2
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
- codex_autorunner/integrations/telegram/transport.py +36 -3
- codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
- codex_autorunner/manifest.py +2 -0
- codex_autorunner/plugin_api.py +22 -0
- codex_autorunner/routes/__init__.py +23 -14
- codex_autorunner/routes/analytics.py +239 -0
- codex_autorunner/routes/base.py +81 -109
- codex_autorunner/routes/file_chat.py +836 -0
- codex_autorunner/routes/flows.py +980 -0
- codex_autorunner/routes/messages.py +459 -0
- codex_autorunner/routes/system.py +6 -1
- codex_autorunner/routes/usage.py +87 -0
- codex_autorunner/routes/workspace.py +271 -0
- codex_autorunner/server.py +2 -1
- codex_autorunner/static/agentControls.js +1 -0
- codex_autorunner/static/agentEvents.js +248 -0
- codex_autorunner/static/app.js +25 -22
- codex_autorunner/static/autoRefresh.js +29 -1
- codex_autorunner/static/bootstrap.js +1 -0
- codex_autorunner/static/bus.js +1 -0
- codex_autorunner/static/cache.js +1 -0
- codex_autorunner/static/constants.js +20 -4
- codex_autorunner/static/dashboard.js +162 -196
- codex_autorunner/static/diffRenderer.js +37 -0
- codex_autorunner/static/docChatCore.js +324 -0
- codex_autorunner/static/docChatStorage.js +65 -0
- codex_autorunner/static/docChatVoice.js +65 -0
- codex_autorunner/static/docEditor.js +133 -0
- codex_autorunner/static/env.js +1 -0
- codex_autorunner/static/eventSummarizer.js +166 -0
- codex_autorunner/static/fileChat.js +182 -0
- codex_autorunner/static/health.js +155 -0
- codex_autorunner/static/hub.js +41 -118
- codex_autorunner/static/index.html +787 -858
- codex_autorunner/static/liveUpdates.js +1 -0
- codex_autorunner/static/loader.js +1 -0
- codex_autorunner/static/messages.js +470 -0
- codex_autorunner/static/mobileCompact.js +2 -1
- codex_autorunner/static/settings.js +24 -211
- codex_autorunner/static/styles.css +7567 -3865
- codex_autorunner/static/tabs.js +28 -5
- codex_autorunner/static/terminal.js +14 -0
- codex_autorunner/static/terminalManager.js +34 -59
- codex_autorunner/static/ticketChatActions.js +333 -0
- codex_autorunner/static/ticketChatEvents.js +16 -0
- codex_autorunner/static/ticketChatStorage.js +16 -0
- codex_autorunner/static/ticketChatStream.js +264 -0
- codex_autorunner/static/ticketEditor.js +750 -0
- codex_autorunner/static/ticketVoice.js +9 -0
- codex_autorunner/static/tickets.js +1315 -0
- codex_autorunner/static/utils.js +32 -3
- codex_autorunner/static/voice.js +1 -0
- codex_autorunner/static/workspace.js +672 -0
- codex_autorunner/static/workspaceApi.js +53 -0
- codex_autorunner/static/workspaceFileBrowser.js +504 -0
- codex_autorunner/tickets/__init__.py +20 -0
- codex_autorunner/tickets/agent_pool.py +377 -0
- codex_autorunner/tickets/files.py +85 -0
- codex_autorunner/tickets/frontmatter.py +55 -0
- codex_autorunner/tickets/lint.py +102 -0
- codex_autorunner/tickets/models.py +95 -0
- codex_autorunner/tickets/outbox.py +232 -0
- codex_autorunner/tickets/replies.py +179 -0
- codex_autorunner/tickets/runner.py +823 -0
- codex_autorunner/tickets/spec_ingest.py +77 -0
- codex_autorunner/web/app.py +269 -91
- codex_autorunner/web/middleware.py +3 -4
- codex_autorunner/web/schemas.py +89 -109
- codex_autorunner/web/static_assets.py +1 -44
- codex_autorunner/workspace/__init__.py +40 -0
- codex_autorunner/workspace/paths.py +319 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +18 -21
- codex_autorunner-1.0.0.dist-info/RECORD +251 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/WHEEL +1 -1
- codex_autorunner/agents/execution/policy.py +0 -292
- codex_autorunner/agents/factory.py +0 -52
- codex_autorunner/agents/orchestrator.py +0 -358
- codex_autorunner/core/doc_chat.py +0 -1446
- codex_autorunner/core/snapshot.py +0 -580
- codex_autorunner/integrations/github/chatops.py +0 -268
- codex_autorunner/integrations/github/pr_flow.py +0 -1314
- codex_autorunner/routes/docs.py +0 -381
- codex_autorunner/routes/github.py +0 -327
- codex_autorunner/routes/runs.py +0 -250
- codex_autorunner/spec_ingest.py +0 -812
- codex_autorunner/static/docChatActions.js +0 -287
- codex_autorunner/static/docChatEvents.js +0 -300
- codex_autorunner/static/docChatRender.js +0 -205
- codex_autorunner/static/docChatStream.js +0 -361
- codex_autorunner/static/docs.js +0 -20
- codex_autorunner/static/docsClipboard.js +0 -69
- codex_autorunner/static/docsCrud.js +0 -257
- codex_autorunner/static/docsDocUpdates.js +0 -62
- codex_autorunner/static/docsDrafts.js +0 -16
- codex_autorunner/static/docsElements.js +0 -69
- codex_autorunner/static/docsInit.js +0 -285
- codex_autorunner/static/docsParse.js +0 -160
- codex_autorunner/static/docsSnapshot.js +0 -87
- codex_autorunner/static/docsSpecIngest.js +0 -263
- codex_autorunner/static/docsState.js +0 -127
- codex_autorunner/static/docsThreadRegistry.js +0 -44
- codex_autorunner/static/docsUi.js +0 -153
- codex_autorunner/static/docsVoice.js +0 -56
- codex_autorunner/static/github.js +0 -504
- codex_autorunner/static/logs.js +0 -678
- codex_autorunner/static/review.js +0 -157
- codex_autorunner/static/runs.js +0 -418
- codex_autorunner/static/snapshot.js +0 -124
- codex_autorunner/static/state.js +0 -94
- codex_autorunner/static/todoPreview.js +0 -27
- codex_autorunner/workspace.py +0 -16
- codex_autorunner-0.1.2.dist-info/RECORD +0 -222
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
codex_autorunner/core/update.py
CHANGED
|
@@ -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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
|
codex_autorunner/core/utils.py
CHANGED
|
@@ -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
|
-
|
|
27
|
-
|
|
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,
|
codex_autorunner/discovery.py
CHANGED
|
@@ -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" / "
|
|
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,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
|