codex-autorunner 1.0.0__py3-none-any.whl → 1.2.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/__init__.py +12 -1
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/client.py +113 -4
- codex_autorunner/agents/opencode/constants.py +3 -0
- codex_autorunner/agents/opencode/harness.py +6 -1
- codex_autorunner/agents/opencode/runtime.py +59 -18
- codex_autorunner/agents/opencode/supervisor.py +4 -0
- codex_autorunner/agents/registry.py +36 -7
- codex_autorunner/bootstrap.py +226 -4
- codex_autorunner/cli.py +5 -1174
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +20 -0
- codex_autorunner/core/about_car.py +119 -1
- codex_autorunner/core/app_server_ids.py +59 -0
- codex_autorunner/core/app_server_threads.py +17 -2
- codex_autorunner/core/app_server_utils.py +165 -0
- codex_autorunner/core/archive.py +349 -0
- codex_autorunner/core/codex_runner.py +6 -2
- codex_autorunner/core/config.py +433 -4
- codex_autorunner/core/context_awareness.py +38 -0
- codex_autorunner/core/docs.py +0 -122
- codex_autorunner/core/drafts.py +58 -4
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/filebox.py +265 -0
- codex_autorunner/core/flows/controller.py +96 -2
- codex_autorunner/core/flows/models.py +13 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +134 -0
- codex_autorunner/core/flows/runtime.py +57 -4
- codex_autorunner/core/flows/store.py +142 -7
- codex_autorunner/core/flows/transition.py +27 -15
- codex_autorunner/core/flows/ux_helpers.py +272 -0
- codex_autorunner/core/flows/worker_process.py +32 -6
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +291 -20
- codex_autorunner/core/lifecycle_events.py +253 -0
- codex_autorunner/core/notifications.py +14 -2
- codex_autorunner/core/path_utils.py +2 -1
- codex_autorunner/core/pma_audit.py +224 -0
- codex_autorunner/core/pma_context.py +496 -0
- codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
- codex_autorunner/core/pma_lifecycle.py +527 -0
- codex_autorunner/core/pma_queue.py +367 -0
- codex_autorunner/core/pma_safety.py +221 -0
- codex_autorunner/core/pma_state.py +115 -0
- codex_autorunner/core/ports/__init__.py +28 -0
- codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +13 -8
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/{integrations/agents → core/ports}/run_event.py +23 -6
- codex_autorunner/core/prompt.py +0 -80
- codex_autorunner/core/prompts.py +56 -172
- codex_autorunner/core/redaction.py +0 -4
- codex_autorunner/core/review_context.py +11 -9
- codex_autorunner/core/runner_controller.py +35 -33
- codex_autorunner/core/runner_state.py +147 -0
- codex_autorunner/core/runtime.py +829 -0
- codex_autorunner/core/sqlite_utils.py +13 -4
- codex_autorunner/core/state.py +7 -10
- codex_autorunner/core/state_roots.py +62 -0
- codex_autorunner/core/supervisor_protocol.py +15 -0
- codex_autorunner/core/templates/__init__.py +39 -0
- codex_autorunner/core/templates/git_mirror.py +234 -0
- codex_autorunner/core/templates/provenance.py +56 -0
- codex_autorunner/core/templates/scan_cache.py +120 -0
- codex_autorunner/core/text_delta_coalescer.py +54 -0
- codex_autorunner/core/ticket_linter_cli.py +218 -0
- codex_autorunner/core/ticket_manager_cli.py +494 -0
- codex_autorunner/core/time_utils.py +11 -0
- codex_autorunner/core/types.py +18 -0
- codex_autorunner/core/update.py +4 -5
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +125 -15
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +37 -34
- codex_autorunner/flows/ticket_flow/definition.py +52 -3
- codex_autorunner/integrations/agents/__init__.py +11 -19
- codex_autorunner/integrations/agents/backend_orchestrator.py +302 -0
- codex_autorunner/integrations/agents/codex_adapter.py +90 -0
- codex_autorunner/integrations/agents/codex_backend.py +177 -25
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +305 -32
- codex_autorunner/integrations/agents/runner.py +86 -0
- codex_autorunner/integrations/agents/wiring.py +279 -0
- codex_autorunner/integrations/app_server/client.py +7 -60
- codex_autorunner/integrations/app_server/env.py +2 -107
- codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
- codex_autorunner/integrations/telegram/adapter.py +65 -0
- codex_autorunner/integrations/telegram/config.py +46 -0
- codex_autorunner/integrations/telegram/constants.py +1 -1
- codex_autorunner/integrations/telegram/doctor.py +228 -6
- codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
- codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +1496 -71
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +206 -48
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +20 -3
- codex_autorunner/integrations/telegram/handlers/messages.py +27 -1
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +22 -1
- codex_autorunner/integrations/telegram/runtime.py +9 -4
- codex_autorunner/integrations/telegram/service.py +45 -10
- codex_autorunner/integrations/telegram/state.py +38 -0
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +338 -43
- codex_autorunner/integrations/telegram/transport.py +13 -4
- codex_autorunner/integrations/templates/__init__.py +27 -0
- codex_autorunner/integrations/templates/scan_agent.py +312 -0
- codex_autorunner/routes/__init__.py +37 -76
- codex_autorunner/routes/agents.py +2 -137
- codex_autorunner/routes/analytics.py +2 -238
- codex_autorunner/routes/app_server.py +2 -131
- codex_autorunner/routes/base.py +2 -596
- codex_autorunner/routes/file_chat.py +4 -833
- codex_autorunner/routes/flows.py +4 -977
- codex_autorunner/routes/messages.py +4 -456
- codex_autorunner/routes/repos.py +2 -196
- codex_autorunner/routes/review.py +2 -147
- codex_autorunner/routes/sessions.py +2 -175
- codex_autorunner/routes/settings.py +2 -168
- codex_autorunner/routes/shared.py +2 -275
- codex_autorunner/routes/system.py +4 -193
- codex_autorunner/routes/usage.py +2 -86
- codex_autorunner/routes/voice.py +2 -119
- codex_autorunner/routes/workspace.py +2 -270
- codex_autorunner/server.py +4 -4
- codex_autorunner/static/agentControls.js +61 -16
- codex_autorunner/static/app.js +126 -14
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +7 -7
- codex_autorunner/static/chatUploads.js +137 -0
- codex_autorunner/static/dashboard.js +224 -171
- codex_autorunner/static/docChatCore.js +185 -13
- codex_autorunner/static/fileChat.js +68 -40
- codex_autorunner/static/fileboxUi.js +159 -0
- codex_autorunner/static/hub.js +114 -131
- codex_autorunner/static/index.html +375 -49
- codex_autorunner/static/messages.js +568 -87
- codex_autorunner/static/notifications.js +255 -0
- codex_autorunner/static/pma.js +1167 -0
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +128 -6
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/streamUtils.js +57 -0
- codex_autorunner/static/styles.css +9798 -6143
- codex_autorunner/static/tabs.js +152 -11
- codex_autorunner/static/templateReposSettings.js +225 -0
- codex_autorunner/static/terminal.js +18 -0
- codex_autorunner/static/ticketChatActions.js +165 -3
- codex_autorunner/static/ticketChatStream.js +17 -119
- codex_autorunner/static/ticketEditor.js +137 -15
- codex_autorunner/static/ticketTemplates.js +798 -0
- codex_autorunner/static/tickets.js +821 -98
- codex_autorunner/static/turnEvents.js +27 -0
- codex_autorunner/static/turnResume.js +33 -0
- codex_autorunner/static/utils.js +39 -0
- codex_autorunner/static/workspace.js +389 -82
- codex_autorunner/static/workspaceFileBrowser.js +15 -13
- codex_autorunner/surfaces/__init__.py +5 -0
- codex_autorunner/surfaces/cli/__init__.py +6 -0
- codex_autorunner/surfaces/cli/cli.py +2534 -0
- codex_autorunner/surfaces/cli/codex_cli.py +20 -0
- codex_autorunner/surfaces/cli/pma_cli.py +817 -0
- codex_autorunner/surfaces/telegram/__init__.py +3 -0
- codex_autorunner/surfaces/web/__init__.py +1 -0
- codex_autorunner/surfaces/web/app.py +2223 -0
- codex_autorunner/surfaces/web/hub_jobs.py +192 -0
- codex_autorunner/surfaces/web/middleware.py +587 -0
- codex_autorunner/surfaces/web/pty_session.py +370 -0
- codex_autorunner/surfaces/web/review.py +6 -0
- codex_autorunner/surfaces/web/routes/__init__.py +82 -0
- codex_autorunner/surfaces/web/routes/agents.py +138 -0
- codex_autorunner/surfaces/web/routes/analytics.py +284 -0
- codex_autorunner/surfaces/web/routes/app_server.py +132 -0
- codex_autorunner/surfaces/web/routes/archive.py +357 -0
- codex_autorunner/surfaces/web/routes/base.py +615 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +1117 -0
- codex_autorunner/surfaces/web/routes/filebox.py +227 -0
- codex_autorunner/surfaces/web/routes/flows.py +1354 -0
- codex_autorunner/surfaces/web/routes/messages.py +490 -0
- codex_autorunner/surfaces/web/routes/pma.py +1652 -0
- codex_autorunner/surfaces/web/routes/repos.py +197 -0
- codex_autorunner/surfaces/web/routes/review.py +148 -0
- codex_autorunner/surfaces/web/routes/sessions.py +176 -0
- codex_autorunner/surfaces/web/routes/settings.py +169 -0
- codex_autorunner/surfaces/web/routes/shared.py +277 -0
- codex_autorunner/surfaces/web/routes/system.py +196 -0
- codex_autorunner/surfaces/web/routes/templates.py +634 -0
- codex_autorunner/surfaces/web/routes/usage.py +89 -0
- codex_autorunner/surfaces/web/routes/voice.py +120 -0
- codex_autorunner/surfaces/web/routes/workspace.py +271 -0
- codex_autorunner/surfaces/web/runner_manager.py +25 -0
- codex_autorunner/surfaces/web/schemas.py +469 -0
- codex_autorunner/surfaces/web/static_assets.py +490 -0
- codex_autorunner/surfaces/web/static_refresh.py +86 -0
- codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
- codex_autorunner/tickets/__init__.py +8 -1
- codex_autorunner/tickets/agent_pool.py +53 -4
- codex_autorunner/tickets/files.py +37 -16
- codex_autorunner/tickets/lint.py +50 -0
- codex_autorunner/tickets/models.py +6 -1
- codex_autorunner/tickets/outbox.py +50 -2
- codex_autorunner/tickets/runner.py +396 -57
- codex_autorunner/web/__init__.py +5 -1
- codex_autorunner/web/app.py +2 -1949
- codex_autorunner/web/hub_jobs.py +2 -191
- codex_autorunner/web/middleware.py +2 -586
- codex_autorunner/web/pty_session.py +2 -369
- codex_autorunner/web/runner_manager.py +2 -24
- codex_autorunner/web/schemas.py +2 -376
- codex_autorunner/web/static_assets.py +4 -441
- codex_autorunner/web/static_refresh.py +2 -85
- codex_autorunner/web/terminal_sessions.py +2 -77
- codex_autorunner/workspace/paths.py +49 -33
- codex_autorunner-1.2.0.dist-info/METADATA +150 -0
- codex_autorunner-1.2.0.dist-info/RECORD +339 -0
- codex_autorunner/core/adapter_utils.py +0 -21
- codex_autorunner/core/engine.py +0 -2653
- codex_autorunner/core/static_assets.py +0 -55
- codex_autorunner-1.0.0.dist-info/METADATA +0 -246
- codex_autorunner-1.0.0.dist-info/RECORD +0 -251
- /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
CAR_AWARENESS_BLOCK = """<injected context>
|
|
6
|
+
You are operating inside a Codex Autorunner (CAR) managed repo.
|
|
7
|
+
|
|
8
|
+
CAR’s durable control-plane lives under `.codex-autorunner/`:
|
|
9
|
+
- `.codex-autorunner/ABOUT_CAR.md` — short repo-local briefing (ticket/workspace conventions + helper scripts).
|
|
10
|
+
- `.codex-autorunner/tickets/` — ordered ticket queue (`TICKET-###*.md`) used by the ticket flow runner.
|
|
11
|
+
- `.codex-autorunner/workspace/` — shared context docs:
|
|
12
|
+
- `active_context.md` — current “north star” context; kept fresh for ongoing work.
|
|
13
|
+
- `spec.md` — longer spec / acceptance criteria when needed.
|
|
14
|
+
- `decisions.md` — prior decisions / tradeoffs when relevant.
|
|
15
|
+
|
|
16
|
+
Intent signals: if the user mentions tickets, “dispatch”, “resume”, workspace docs, or `.codex-autorunner/`, they are likely referring to CAR artifacts/workflow rather than generic repo files.
|
|
17
|
+
|
|
18
|
+
Use the above as orientation. If you need the operational details (exact helper commands, what CAR auto-generates), read `.codex-autorunner/ABOUT_CAR.md`.
|
|
19
|
+
</injected context>"""
|
|
20
|
+
|
|
21
|
+
ROLE_ADDENDUM_START = "<role addendum>"
|
|
22
|
+
ROLE_ADDENDUM_END = "</role addendum>"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def format_file_role_addendum(
|
|
26
|
+
kind: Literal["ticket", "workspace", "other"],
|
|
27
|
+
rel_path: str,
|
|
28
|
+
) -> str:
|
|
29
|
+
"""Format a short role-specific addendum for prompts."""
|
|
30
|
+
if kind == "ticket":
|
|
31
|
+
text = f"This target is a CAR ticket at `{rel_path}`."
|
|
32
|
+
elif kind == "workspace":
|
|
33
|
+
text = f"This target is a CAR workspace doc at `{rel_path}`."
|
|
34
|
+
elif kind == "other":
|
|
35
|
+
text = f"This target file is `{rel_path}`."
|
|
36
|
+
else:
|
|
37
|
+
raise ValueError(f"Unsupported role addendum kind: {kind}")
|
|
38
|
+
return f"{ROLE_ADDENDUM_START}\n{text}\n{ROLE_ADDENDUM_END}"
|
codex_autorunner/core/docs.py
CHANGED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
from typing import List, Tuple
|
|
4
|
-
|
|
5
|
-
from .config import Config
|
|
6
|
-
|
|
7
|
-
_TODO_LINE_RE = re.compile(r"^\s*[-*]\s*\[(?P<state>[ xX])\]\s*(?P<text>.*)$")
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def _iter_meaningful_lines(content: str):
|
|
11
|
-
in_code_fence = False
|
|
12
|
-
in_html_comment = False
|
|
13
|
-
html_comment_pattern = re.compile(r"<!--.*?-->", re.DOTALL)
|
|
14
|
-
|
|
15
|
-
for line in content.splitlines():
|
|
16
|
-
stripped = line.strip()
|
|
17
|
-
|
|
18
|
-
if stripped.startswith("```"):
|
|
19
|
-
in_code_fence = not in_code_fence
|
|
20
|
-
continue
|
|
21
|
-
|
|
22
|
-
if in_code_fence:
|
|
23
|
-
continue
|
|
24
|
-
|
|
25
|
-
if line.lstrip().startswith("<!--"):
|
|
26
|
-
if "-->" in line:
|
|
27
|
-
if html_comment_pattern.search(line):
|
|
28
|
-
continue
|
|
29
|
-
else:
|
|
30
|
-
in_html_comment = True
|
|
31
|
-
continue
|
|
32
|
-
|
|
33
|
-
if in_html_comment:
|
|
34
|
-
if "-->" in line:
|
|
35
|
-
in_html_comment = False
|
|
36
|
-
continue
|
|
37
|
-
|
|
38
|
-
yield line
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def parse_todos(content: str) -> Tuple[List[str], List[str]]:
|
|
42
|
-
outstanding: List[str] = []
|
|
43
|
-
done: List[str] = []
|
|
44
|
-
if not content:
|
|
45
|
-
return outstanding, done
|
|
46
|
-
|
|
47
|
-
for line in _iter_meaningful_lines(content):
|
|
48
|
-
match = _TODO_LINE_RE.match(line)
|
|
49
|
-
if match:
|
|
50
|
-
state = match.group("state")
|
|
51
|
-
text = match.group("text").strip()
|
|
52
|
-
if state in (" ",):
|
|
53
|
-
outstanding.append(text)
|
|
54
|
-
elif state in ("x", "X"):
|
|
55
|
-
done.append(text)
|
|
56
|
-
return outstanding, done
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
_TODO_CHECKBOX_RE = re.compile(r"^\s*[-*]\s*\[(?P<state>[ xX])\]\s+\S")
|
|
60
|
-
_TODO_BULLET_RE = re.compile(r"^\s*[-*]\s+")
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def validate_todo_markdown(content: str) -> List[str]:
|
|
64
|
-
"""
|
|
65
|
-
Validate that TODO content contains tasks as markdown checkboxes.
|
|
66
|
-
|
|
67
|
-
Rules:
|
|
68
|
-
- If the file has any non-heading, non-empty content, it must include at least one checkbox line.
|
|
69
|
-
- Any bullet line must be a checkbox bullet (no plain '-' bullets for tasks).
|
|
70
|
-
"""
|
|
71
|
-
errors: List[str] = []
|
|
72
|
-
if content is None:
|
|
73
|
-
return ["TODO is missing"]
|
|
74
|
-
lines = list(_iter_meaningful_lines(content))
|
|
75
|
-
meaningful = [
|
|
76
|
-
line for line in lines if line.strip() and not line.lstrip().startswith("#")
|
|
77
|
-
]
|
|
78
|
-
if not meaningful:
|
|
79
|
-
return []
|
|
80
|
-
checkbox_lines = [line for line in meaningful if _TODO_CHECKBOX_RE.match(line)]
|
|
81
|
-
if not checkbox_lines:
|
|
82
|
-
errors.append(
|
|
83
|
-
"TODO must contain at least one markdown checkbox task line like `- [ ] ...`."
|
|
84
|
-
)
|
|
85
|
-
bullet_lines = [line for line in meaningful if _TODO_BULLET_RE.match(line)]
|
|
86
|
-
non_checkbox_bullets = [
|
|
87
|
-
line for line in bullet_lines if not _TODO_CHECKBOX_RE.match(line)
|
|
88
|
-
]
|
|
89
|
-
if non_checkbox_bullets:
|
|
90
|
-
sample = non_checkbox_bullets[0].strip()
|
|
91
|
-
errors.append(
|
|
92
|
-
"TODO contains non-checkbox bullet(s); use `- [ ] ...` instead. "
|
|
93
|
-
f"Example: `{sample}`"
|
|
94
|
-
)
|
|
95
|
-
return errors
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
class DocsManager:
|
|
99
|
-
def __init__(self, config: Config):
|
|
100
|
-
self.config = config
|
|
101
|
-
|
|
102
|
-
def read_doc(self, key: str) -> str:
|
|
103
|
-
try:
|
|
104
|
-
path = self.config.doc_path(key)
|
|
105
|
-
except KeyError:
|
|
106
|
-
return ""
|
|
107
|
-
return path.read_text(encoding="utf-8") if path.exists() else ""
|
|
108
|
-
|
|
109
|
-
def todos(self) -> Tuple[List[str], List[str]]:
|
|
110
|
-
# Legacy helper retained for backward compatibility; newer configs may not
|
|
111
|
-
# have a TODO doc at all.
|
|
112
|
-
try:
|
|
113
|
-
todo_path: Path = self.config.doc_path("todo")
|
|
114
|
-
except KeyError:
|
|
115
|
-
return [], []
|
|
116
|
-
if not todo_path.exists():
|
|
117
|
-
return [], []
|
|
118
|
-
return parse_todos(todo_path.read_text(encoding="utf-8"))
|
|
119
|
-
|
|
120
|
-
def todos_done(self) -> bool:
|
|
121
|
-
outstanding, _ = self.todos()
|
|
122
|
-
return len(outstanding) == 0
|
codex_autorunner/core/drafts.py
CHANGED
|
@@ -2,12 +2,18 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import hashlib
|
|
4
4
|
import json
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import datetime, timezone
|
|
5
7
|
from pathlib import Path
|
|
6
8
|
from typing import Any, Dict, Optional
|
|
7
9
|
|
|
8
10
|
from .utils import atomic_write
|
|
9
11
|
|
|
10
12
|
FILE_CHAT_STATE_NAME = "file_chat_state.json"
|
|
13
|
+
FILE_CHAT_STATE_CORRUPT_SUFFIX = ".corrupt"
|
|
14
|
+
FILE_CHAT_STATE_NOTICE_SUFFIX = ".corrupt.json"
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
11
17
|
|
|
12
18
|
|
|
13
19
|
def state_path(repo_root: Path) -> Path:
|
|
@@ -23,12 +29,19 @@ def load_state(repo_root: Path) -> Dict[str, Any]:
|
|
|
23
29
|
if not path.exists():
|
|
24
30
|
return {"drafts": {}}
|
|
25
31
|
try:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
raw = path.read_text(encoding="utf-8")
|
|
33
|
+
except OSError as exc:
|
|
34
|
+
logger.warning("Failed to read file chat state at %s: %s", path, exc)
|
|
29
35
|
return {"drafts": {}}
|
|
30
|
-
|
|
36
|
+
try:
|
|
37
|
+
data = json.loads(raw)
|
|
38
|
+
except json.JSONDecodeError as exc:
|
|
39
|
+
_handle_corrupt_state(path, str(exc))
|
|
40
|
+
return {"drafts": {}}
|
|
41
|
+
if not isinstance(data, dict):
|
|
42
|
+
_handle_corrupt_state(path, f"Expected JSON object, got {type(data).__name__}")
|
|
31
43
|
return {"drafts": {}}
|
|
44
|
+
return data
|
|
32
45
|
|
|
33
46
|
|
|
34
47
|
def save_state(repo_root: Path, state: Dict[str, Any]) -> None:
|
|
@@ -80,3 +93,44 @@ def invalidate_drafts_for_path(repo_root: Path, rel_path: str) -> list[str]:
|
|
|
80
93
|
if removed_keys:
|
|
81
94
|
save_drafts(repo_root, drafts)
|
|
82
95
|
return removed_keys
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _stamp() -> str:
|
|
99
|
+
return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _notice_path(path: Path) -> Path:
|
|
103
|
+
return path.with_name(f"{path.name}{FILE_CHAT_STATE_NOTICE_SUFFIX}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _handle_corrupt_state(path: Path, detail: str) -> None:
|
|
107
|
+
stamp = _stamp()
|
|
108
|
+
backup_path = path.with_name(f"{path.name}{FILE_CHAT_STATE_CORRUPT_SUFFIX}.{stamp}")
|
|
109
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
try:
|
|
111
|
+
path.replace(backup_path)
|
|
112
|
+
backup_value = str(backup_path)
|
|
113
|
+
except OSError:
|
|
114
|
+
backup_value = ""
|
|
115
|
+
notice = {
|
|
116
|
+
"status": "corrupt",
|
|
117
|
+
"message": "Draft state reset due to corrupted file_chat_state.json.",
|
|
118
|
+
"detail": detail,
|
|
119
|
+
"detected_at": stamp,
|
|
120
|
+
"backup_path": backup_value,
|
|
121
|
+
}
|
|
122
|
+
notice_path = _notice_path(path)
|
|
123
|
+
try:
|
|
124
|
+
atomic_write(notice_path, json.dumps(notice, indent=2) + "\n")
|
|
125
|
+
except Exception:
|
|
126
|
+
logger.warning("Failed to write draft corruption notice at %s", notice_path)
|
|
127
|
+
try:
|
|
128
|
+
atomic_write(path, json.dumps({"drafts": {}}, indent=2) + "\n")
|
|
129
|
+
except Exception:
|
|
130
|
+
logger.warning("Failed to reset draft state at %s", path)
|
|
131
|
+
logger.warning(
|
|
132
|
+
"Corrupted file chat state detected; backup=%s notice=%s detail=%s",
|
|
133
|
+
backup_value or "unavailable",
|
|
134
|
+
notice_path,
|
|
135
|
+
detail,
|
|
136
|
+
)
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, Iterable, List, Tuple
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class FileBoxEntry:
|
|
12
|
+
name: str
|
|
13
|
+
box: str # "inbox" | "outbox"
|
|
14
|
+
size: int | None
|
|
15
|
+
modified_at: str | None
|
|
16
|
+
source: str # "filebox", "pma", "telegram"
|
|
17
|
+
path: Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
BOXES = ("inbox", "outbox")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def filebox_root(repo_root: Path) -> Path:
|
|
24
|
+
return Path(repo_root) / ".codex-autorunner" / "filebox"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def inbox_dir(repo_root: Path) -> Path:
|
|
28
|
+
return filebox_root(repo_root) / "inbox"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def outbox_dir(repo_root: Path) -> Path:
|
|
32
|
+
return filebox_root(repo_root) / "outbox"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def outbox_pending_dir(repo_root: Path) -> Path:
|
|
36
|
+
# Preserves Telegram pending semantics while keeping everything under the shared FileBox.
|
|
37
|
+
return outbox_dir(repo_root) / "pending"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def outbox_sent_dir(repo_root: Path) -> Path:
|
|
41
|
+
return outbox_dir(repo_root) / "sent"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def ensure_structure(repo_root: Path) -> None:
|
|
45
|
+
for path in (
|
|
46
|
+
inbox_dir(repo_root),
|
|
47
|
+
outbox_dir(repo_root),
|
|
48
|
+
outbox_pending_dir(repo_root),
|
|
49
|
+
outbox_sent_dir(repo_root),
|
|
50
|
+
):
|
|
51
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def sanitize_filename(name: str) -> str:
|
|
55
|
+
base = Path(name or "").name
|
|
56
|
+
if not base or base in {".", ".."}:
|
|
57
|
+
raise ValueError("Missing filename")
|
|
58
|
+
# Reject any path separators or traversal segments up-front.
|
|
59
|
+
if name != base or "/" in name or "\\" in name:
|
|
60
|
+
raise ValueError("Invalid filename")
|
|
61
|
+
parts = Path(base).parts
|
|
62
|
+
if any(part in {"", ".", ".."} for part in parts):
|
|
63
|
+
raise ValueError("Invalid filename")
|
|
64
|
+
return base
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _legacy_paths(repo_root: Path, box: str) -> List[Tuple[str, Path]]:
|
|
68
|
+
root = Path(repo_root)
|
|
69
|
+
paths: List[Tuple[str, Path]] = []
|
|
70
|
+
if box not in BOXES:
|
|
71
|
+
return paths
|
|
72
|
+
|
|
73
|
+
# PMA legacy paths
|
|
74
|
+
pma_dir = root / ".codex-autorunner" / "pma" / box
|
|
75
|
+
paths.append(("pma", pma_dir))
|
|
76
|
+
|
|
77
|
+
# Telegram legacy paths (topic-scoped). We merge inbox and outbox/pending|sent.
|
|
78
|
+
telegram_root = root / ".codex-autorunner" / "uploads" / "telegram-files"
|
|
79
|
+
if telegram_root.exists():
|
|
80
|
+
for topic in telegram_root.iterdir():
|
|
81
|
+
if not topic.is_dir():
|
|
82
|
+
continue
|
|
83
|
+
if box == "inbox":
|
|
84
|
+
paths.append(("telegram", topic / "inbox"))
|
|
85
|
+
elif box == "outbox":
|
|
86
|
+
paths.append(("telegram", topic / "outbox" / "pending"))
|
|
87
|
+
paths.append(("telegram", topic / "outbox" / "sent"))
|
|
88
|
+
return paths
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _gather_files(entries: Iterable[Tuple[str, Path]], box: str) -> List[FileBoxEntry]:
|
|
92
|
+
collected: List[FileBoxEntry] = []
|
|
93
|
+
for source, folder in entries:
|
|
94
|
+
if not folder.exists():
|
|
95
|
+
continue
|
|
96
|
+
try:
|
|
97
|
+
for path in folder.iterdir():
|
|
98
|
+
try:
|
|
99
|
+
if not path.is_file():
|
|
100
|
+
continue
|
|
101
|
+
stat = path.stat()
|
|
102
|
+
collected.append(
|
|
103
|
+
FileBoxEntry(
|
|
104
|
+
name=path.name,
|
|
105
|
+
box=box,
|
|
106
|
+
size=stat.st_size if stat else None,
|
|
107
|
+
modified_at=_format_mtime(stat.st_mtime) if stat else None,
|
|
108
|
+
source=source,
|
|
109
|
+
path=path,
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
except OSError:
|
|
113
|
+
continue
|
|
114
|
+
except OSError:
|
|
115
|
+
continue
|
|
116
|
+
return collected
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _dedupe(entries: List[FileBoxEntry]) -> List[FileBoxEntry]:
|
|
120
|
+
# Prefer primary filebox entries over legacy duplicates.
|
|
121
|
+
deduped: Dict[Tuple[str, str], FileBoxEntry] = {}
|
|
122
|
+
for entry in entries:
|
|
123
|
+
key = (entry.box, entry.name)
|
|
124
|
+
existing = deduped.get(key)
|
|
125
|
+
if existing is None:
|
|
126
|
+
deduped[key] = entry
|
|
127
|
+
continue
|
|
128
|
+
if existing.source != "filebox" and entry.source == "filebox":
|
|
129
|
+
deduped[key] = entry
|
|
130
|
+
return list(deduped.values())
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _format_mtime(ts: float | None) -> str | None:
|
|
134
|
+
if ts is None:
|
|
135
|
+
return None
|
|
136
|
+
try:
|
|
137
|
+
return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat()
|
|
138
|
+
except Exception:
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def list_filebox(
|
|
143
|
+
repo_root: Path, *, include_legacy: bool = True
|
|
144
|
+
) -> Dict[str, List[FileBoxEntry]]:
|
|
145
|
+
ensure_structure(repo_root)
|
|
146
|
+
results: Dict[str, List[FileBoxEntry]] = {}
|
|
147
|
+
for box in BOXES:
|
|
148
|
+
primaries = _gather_files([("filebox", _box_dir(repo_root, box))], box)
|
|
149
|
+
legacy = (
|
|
150
|
+
_gather_files(_legacy_paths(repo_root, box), box) if include_legacy else []
|
|
151
|
+
)
|
|
152
|
+
results[box] = _dedupe(primaries + legacy)
|
|
153
|
+
return results
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _box_dir(repo_root: Path, box: str) -> Path:
|
|
157
|
+
if box == "inbox":
|
|
158
|
+
return inbox_dir(repo_root)
|
|
159
|
+
if box == "outbox":
|
|
160
|
+
return outbox_dir(repo_root)
|
|
161
|
+
raise ValueError("Invalid filebox")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _target_path(repo_root: Path, box: str, filename: str) -> Path:
|
|
165
|
+
"""Return a resolved path within the FileBox, rejecting traversal attempts."""
|
|
166
|
+
|
|
167
|
+
safe_name = sanitize_filename(filename)
|
|
168
|
+
target_dir = _box_dir(repo_root, box)
|
|
169
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
170
|
+
|
|
171
|
+
root = target_dir.resolve()
|
|
172
|
+
candidate = (root / safe_name).resolve()
|
|
173
|
+
try:
|
|
174
|
+
candidate.relative_to(root)
|
|
175
|
+
except ValueError as exc:
|
|
176
|
+
raise ValueError("Invalid filename") from exc
|
|
177
|
+
if candidate.parent != root:
|
|
178
|
+
# Disallow sneaky path tricks that resolve inside nested folders.
|
|
179
|
+
raise ValueError("Invalid filename")
|
|
180
|
+
return candidate
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def save_file(repo_root: Path, box: str, filename: str, data: bytes) -> Path:
|
|
184
|
+
if box not in BOXES:
|
|
185
|
+
raise ValueError("Invalid box")
|
|
186
|
+
ensure_structure(repo_root)
|
|
187
|
+
path = _target_path(repo_root, box, filename)
|
|
188
|
+
path.write_bytes(data)
|
|
189
|
+
return path
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def resolve_file(repo_root: Path, box: str, filename: str) -> FileBoxEntry | None:
|
|
193
|
+
if box not in BOXES:
|
|
194
|
+
return None
|
|
195
|
+
safe_name = sanitize_filename(filename)
|
|
196
|
+
paths: List[Tuple[str, Path]] = [("filebox", _box_dir(repo_root, box))]
|
|
197
|
+
paths.extend(_legacy_paths(repo_root, box))
|
|
198
|
+
candidates = _gather_files(paths, box)
|
|
199
|
+
for entry in candidates:
|
|
200
|
+
if entry.name == safe_name:
|
|
201
|
+
return entry
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def delete_file(repo_root: Path, box: str, filename: str) -> bool:
|
|
206
|
+
if box not in BOXES:
|
|
207
|
+
return False
|
|
208
|
+
safe_name = sanitize_filename(filename)
|
|
209
|
+
paths: List[Tuple[str, Path]] = [("filebox", _box_dir(repo_root, box))]
|
|
210
|
+
paths.extend(_legacy_paths(repo_root, box))
|
|
211
|
+
candidates = _gather_files(paths, box)
|
|
212
|
+
removed = False
|
|
213
|
+
for entry in candidates:
|
|
214
|
+
if entry.name != safe_name:
|
|
215
|
+
continue
|
|
216
|
+
try:
|
|
217
|
+
entry.path.unlink()
|
|
218
|
+
removed = True
|
|
219
|
+
except OSError:
|
|
220
|
+
continue
|
|
221
|
+
return removed
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def migrate_legacy(repo_root: Path) -> int:
|
|
225
|
+
"""
|
|
226
|
+
Opportunistically copy legacy PMA/Telegram files into the shared FileBox.
|
|
227
|
+
Returns the number of files copied.
|
|
228
|
+
"""
|
|
229
|
+
copied = 0
|
|
230
|
+
ensure_structure(repo_root)
|
|
231
|
+
for box in BOXES:
|
|
232
|
+
target_dir = _box_dir(repo_root, box)
|
|
233
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
234
|
+
for _source, folder in _legacy_paths(repo_root, box):
|
|
235
|
+
if not folder.exists():
|
|
236
|
+
continue
|
|
237
|
+
for path in folder.iterdir():
|
|
238
|
+
try:
|
|
239
|
+
if not path.is_file():
|
|
240
|
+
continue
|
|
241
|
+
dest = target_dir / path.name
|
|
242
|
+
if dest.exists():
|
|
243
|
+
continue
|
|
244
|
+
shutil.copy2(path, dest)
|
|
245
|
+
copied += 1
|
|
246
|
+
except OSError:
|
|
247
|
+
continue
|
|
248
|
+
return copied
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
__all__ = [
|
|
252
|
+
"BOXES",
|
|
253
|
+
"FileBoxEntry",
|
|
254
|
+
"delete_file",
|
|
255
|
+
"filebox_root",
|
|
256
|
+
"inbox_dir",
|
|
257
|
+
"list_filebox",
|
|
258
|
+
"migrate_legacy",
|
|
259
|
+
"outbox_dir",
|
|
260
|
+
"outbox_pending_dir",
|
|
261
|
+
"outbox_sent_dir",
|
|
262
|
+
"resolve_file",
|
|
263
|
+
"sanitize_filename",
|
|
264
|
+
"save_file",
|
|
265
|
+
]
|
|
@@ -4,11 +4,31 @@ import uuid
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Any, AsyncGenerator, Callable, Dict, Optional, Set
|
|
6
6
|
|
|
7
|
+
from ..lifecycle_events import LifecycleEventEmitter
|
|
8
|
+
from ..utils import find_repo_root
|
|
7
9
|
from .definition import FlowDefinition
|
|
8
10
|
from .models import FlowEvent, FlowRunRecord, FlowRunStatus
|
|
9
11
|
from .runtime import FlowRuntime
|
|
10
12
|
from .store import FlowStore
|
|
11
13
|
|
|
14
|
+
|
|
15
|
+
def _find_hub_root(repo_root: Optional[Path] = None) -> Optional[Path]:
|
|
16
|
+
if repo_root is None:
|
|
17
|
+
repo_root = find_repo_root()
|
|
18
|
+
if repo_root is None:
|
|
19
|
+
return None
|
|
20
|
+
current = repo_root
|
|
21
|
+
for _ in range(5):
|
|
22
|
+
manifest_path = current / ".codex-autorunner" / "manifest.yml"
|
|
23
|
+
if manifest_path.exists():
|
|
24
|
+
return current
|
|
25
|
+
parent = current.parent
|
|
26
|
+
if parent == current:
|
|
27
|
+
break
|
|
28
|
+
current = parent
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
12
32
|
_logger = logging.getLogger(__name__)
|
|
13
33
|
|
|
14
34
|
|
|
@@ -18,13 +38,24 @@ class FlowController:
|
|
|
18
38
|
definition: FlowDefinition,
|
|
19
39
|
db_path: Path,
|
|
20
40
|
artifacts_root: Path,
|
|
41
|
+
durable: bool = False,
|
|
42
|
+
hub_root: Optional[Path] = None,
|
|
21
43
|
):
|
|
22
44
|
self.definition = definition
|
|
23
45
|
self.db_path = db_path
|
|
24
46
|
self.artifacts_root = artifacts_root
|
|
25
|
-
self.store = FlowStore(db_path)
|
|
47
|
+
self.store = FlowStore(db_path, durable=durable)
|
|
26
48
|
self._event_listeners: Set[Callable[[FlowEvent], None]] = set()
|
|
49
|
+
self._lifecycle_event_listeners: Set[Callable[[str, str, str, dict], None]] = (
|
|
50
|
+
set()
|
|
51
|
+
)
|
|
27
52
|
self._lock = asyncio.Lock()
|
|
53
|
+
self._lifecycle_emitter: Optional[LifecycleEventEmitter] = None
|
|
54
|
+
if hub_root is None:
|
|
55
|
+
hub_root = _find_hub_root(db_path.parent.parent if db_path else None)
|
|
56
|
+
if hub_root is not None:
|
|
57
|
+
self._lifecycle_emitter = LifecycleEventEmitter(hub_root)
|
|
58
|
+
self.add_lifecycle_event_listener(self._emit_to_lifecycle_store)
|
|
28
59
|
|
|
29
60
|
def initialize(self) -> None:
|
|
30
61
|
self.artifacts_root.mkdir(parents=True, exist_ok=True)
|
|
@@ -70,6 +101,7 @@ class FlowController:
|
|
|
70
101
|
definition=self.definition,
|
|
71
102
|
store=self.store,
|
|
72
103
|
emit_event=self._emit_event,
|
|
104
|
+
emit_lifecycle_event=self._emit_lifecycle,
|
|
73
105
|
)
|
|
74
106
|
return await runtime.run_flow(run_id=run_id, initial_state=initial_state)
|
|
75
107
|
|
|
@@ -103,7 +135,33 @@ class FlowController:
|
|
|
103
135
|
cleared = self.store.set_stop_requested(run_id, False)
|
|
104
136
|
if not cleared:
|
|
105
137
|
raise RuntimeError(f"Failed to clear stop flag for run {run_id}")
|
|
106
|
-
|
|
138
|
+
if record.status == FlowRunStatus.COMPLETED:
|
|
139
|
+
return cleared
|
|
140
|
+
state = dict(record.state or {})
|
|
141
|
+
engine = state.get("ticket_engine")
|
|
142
|
+
if isinstance(engine, dict):
|
|
143
|
+
engine = dict(engine)
|
|
144
|
+
if engine.get("reason_code") == "max_turns":
|
|
145
|
+
engine["total_turns"] = 0
|
|
146
|
+
engine["status"] = "running"
|
|
147
|
+
engine.pop("reason", None)
|
|
148
|
+
engine.pop("reason_details", None)
|
|
149
|
+
engine.pop("reason_code", None)
|
|
150
|
+
state["ticket_engine"] = engine
|
|
151
|
+
state.pop("reason_summary", None)
|
|
152
|
+
|
|
153
|
+
updated = self.store.update_flow_run_status(
|
|
154
|
+
run_id=run_id,
|
|
155
|
+
status=FlowRunStatus.RUNNING,
|
|
156
|
+
state=state,
|
|
157
|
+
)
|
|
158
|
+
if updated:
|
|
159
|
+
return updated
|
|
160
|
+
|
|
161
|
+
updated = self.store.get_flow_run(run_id)
|
|
162
|
+
if not updated:
|
|
163
|
+
raise RuntimeError(f"Failed to get record for run {run_id}")
|
|
164
|
+
return updated
|
|
107
165
|
|
|
108
166
|
def get_status(self, run_id: str) -> Optional[FlowRunRecord]:
|
|
109
167
|
return self.store.get_flow_run(run_id)
|
|
@@ -150,6 +208,42 @@ class FlowController:
|
|
|
150
208
|
def remove_event_listener(self, listener: Callable[[FlowEvent], None]) -> None:
|
|
151
209
|
self._event_listeners.discard(listener)
|
|
152
210
|
|
|
211
|
+
def add_lifecycle_event_listener(
|
|
212
|
+
self, listener: Callable[[str, str, str, dict], None]
|
|
213
|
+
) -> None:
|
|
214
|
+
self._lifecycle_event_listeners.add(listener)
|
|
215
|
+
|
|
216
|
+
def remove_lifecycle_event_listener(
|
|
217
|
+
self, listener: Callable[[str, str, str, dict], None]
|
|
218
|
+
) -> None:
|
|
219
|
+
self._lifecycle_event_listeners.discard(listener)
|
|
220
|
+
|
|
221
|
+
def _emit_lifecycle(
|
|
222
|
+
self, event_type: str, repo_id: str, run_id: str, data: Dict[str, Any]
|
|
223
|
+
) -> None:
|
|
224
|
+
for listener in self._lifecycle_event_listeners:
|
|
225
|
+
try:
|
|
226
|
+
listener(event_type, repo_id, run_id, data)
|
|
227
|
+
except Exception as e:
|
|
228
|
+
_logger.exception("Error in lifecycle event listener: %s", e)
|
|
229
|
+
|
|
230
|
+
def _emit_to_lifecycle_store(
|
|
231
|
+
self, event_type: str, repo_id: str, run_id: str, data: Dict[str, Any]
|
|
232
|
+
) -> None:
|
|
233
|
+
if self._lifecycle_emitter is None:
|
|
234
|
+
return
|
|
235
|
+
try:
|
|
236
|
+
if event_type == "flow_paused":
|
|
237
|
+
self._lifecycle_emitter.emit_flow_paused(repo_id, run_id, data=data)
|
|
238
|
+
elif event_type == "flow_completed":
|
|
239
|
+
self._lifecycle_emitter.emit_flow_completed(repo_id, run_id, data=data)
|
|
240
|
+
elif event_type == "flow_failed":
|
|
241
|
+
self._lifecycle_emitter.emit_flow_failed(repo_id, run_id, data=data)
|
|
242
|
+
elif event_type == "flow_stopped":
|
|
243
|
+
self._lifecycle_emitter.emit_flow_stopped(repo_id, run_id, data=data)
|
|
244
|
+
except Exception as exc:
|
|
245
|
+
_logger.exception("Error emitting to lifecycle store: %s", exc)
|
|
246
|
+
|
|
153
247
|
def _emit_event(self, event: FlowEvent) -> None:
|
|
154
248
|
for listener in self._event_listeners:
|
|
155
249
|
try:
|
|
@@ -32,13 +32,26 @@ class FlowEventType(str, Enum):
|
|
|
32
32
|
STEP_COMPLETED = "step_completed"
|
|
33
33
|
STEP_FAILED = "step_failed"
|
|
34
34
|
AGENT_STREAM_DELTA = "agent_stream_delta"
|
|
35
|
+
AGENT_MESSAGE_COMPLETE = "agent_message_complete"
|
|
36
|
+
AGENT_FAILED = "agent_failed"
|
|
35
37
|
APP_SERVER_EVENT = "app_server_event"
|
|
38
|
+
TOOL_CALL = "tool_call"
|
|
39
|
+
TOOL_RESULT = "tool_result"
|
|
40
|
+
APPROVAL_REQUESTED = "approval_requested"
|
|
36
41
|
TOKEN_USAGE = "token_usage"
|
|
37
42
|
FLOW_STARTED = "flow_started"
|
|
38
43
|
FLOW_STOPPED = "flow_stopped"
|
|
39
44
|
FLOW_RESUMED = "flow_resumed"
|
|
40
45
|
FLOW_COMPLETED = "flow_completed"
|
|
41
46
|
FLOW_FAILED = "flow_failed"
|
|
47
|
+
RUN_STARTED = "run_started"
|
|
48
|
+
RUN_FINISHED = "run_finished"
|
|
49
|
+
RUN_STATE_CHANGED = "run_state_changed"
|
|
50
|
+
RUN_NO_PROGRESS = "run_no_progress"
|
|
51
|
+
PLAN_UPDATED = "plan_updated"
|
|
52
|
+
DIFF_UPDATED = "diff_updated"
|
|
53
|
+
RUN_TIMEOUT = "run_timeout"
|
|
54
|
+
RUN_CANCELLED = "run_cancelled"
|
|
42
55
|
|
|
43
56
|
|
|
44
57
|
class FlowRunRecord(BaseModel):
|