codex-autorunner 0.1.1__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codex_autorunner/__main__.py +4 -0
- codex_autorunner/agents/__init__.py +20 -0
- codex_autorunner/agents/base.py +2 -2
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/__init__.py +4 -0
- codex_autorunner/agents/opencode/agent_config.py +104 -0
- codex_autorunner/agents/opencode/client.py +305 -28
- codex_autorunner/agents/opencode/harness.py +71 -20
- codex_autorunner/agents/opencode/logging.py +225 -0
- codex_autorunner/agents/opencode/run_prompt.py +261 -0
- codex_autorunner/agents/opencode/runtime.py +1202 -132
- codex_autorunner/agents/opencode/supervisor.py +194 -68
- codex_autorunner/agents/registry.py +258 -0
- codex_autorunner/agents/types.py +2 -2
- codex_autorunner/api.py +25 -0
- codex_autorunner/bootstrap.py +19 -40
- codex_autorunner/cli.py +234 -151
- codex_autorunner/core/about_car.py +44 -32
- codex_autorunner/core/adapter_utils.py +21 -0
- codex_autorunner/core/app_server_events.py +15 -6
- codex_autorunner/core/app_server_logging.py +55 -15
- codex_autorunner/core/app_server_prompts.py +28 -259
- codex_autorunner/core/app_server_threads.py +15 -26
- codex_autorunner/core/circuit_breaker.py +183 -0
- codex_autorunner/core/codex_runner.py +6 -0
- codex_autorunner/core/config.py +555 -133
- codex_autorunner/core/docs.py +54 -9
- codex_autorunner/core/drafts.py +82 -0
- codex_autorunner/core/engine.py +828 -274
- codex_autorunner/core/exceptions.py +60 -0
- 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 +21 -13
- codex_autorunner/core/locks.py +118 -1
- codex_autorunner/core/logging_utils.py +9 -6
- codex_autorunner/core/path_utils.py +123 -0
- codex_autorunner/core/prompt.py +15 -7
- codex_autorunner/core/redaction.py +29 -0
- codex_autorunner/core/retry.py +61 -0
- codex_autorunner/core/review.py +888 -0
- codex_autorunner/core/review_context.py +161 -0
- codex_autorunner/core/run_index.py +223 -0
- codex_autorunner/core/runner_controller.py +44 -1
- codex_autorunner/core/runner_process.py +30 -1
- codex_autorunner/core/sqlite_utils.py +32 -0
- codex_autorunner/core/state.py +273 -44
- codex_autorunner/core/static_assets.py +55 -0
- codex_autorunner/core/supervisor_utils.py +67 -0
- codex_autorunner/core/text_delta_coalescer.py +43 -0
- codex_autorunner/core/update.py +20 -11
- codex_autorunner/core/update_runner.py +2 -0
- codex_autorunner/core/usage.py +107 -75
- codex_autorunner/core/utils.py +167 -3
- codex_autorunner/discovery.py +3 -3
- 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 +708 -153
- codex_autorunner/integrations/app_server/supervisor.py +59 -33
- codex_autorunner/integrations/telegram/adapter.py +474 -185
- codex_autorunner/integrations/telegram/api_schemas.py +120 -0
- codex_autorunner/integrations/telegram/config.py +239 -1
- codex_autorunner/integrations/telegram/constants.py +19 -1
- codex_autorunner/integrations/telegram/dispatch.py +44 -8
- codex_autorunner/integrations/telegram/doctor.py +47 -0
- codex_autorunner/integrations/telegram/handlers/approvals.py +12 -10
- codex_autorunner/integrations/telegram/handlers/callbacks.py +15 -1
- codex_autorunner/integrations/telegram/handlers/commands/__init__.py +29 -0
- codex_autorunner/integrations/telegram/handlers/commands/approvals.py +173 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +2595 -0
- codex_autorunner/integrations/telegram/handlers/commands/files.py +1408 -0
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +227 -0
- codex_autorunner/integrations/telegram/handlers/commands/formatting.py +81 -0
- codex_autorunner/integrations/telegram/handlers/commands/github.py +1688 -0
- codex_autorunner/integrations/telegram/handlers/commands/shared.py +190 -0
- codex_autorunner/integrations/telegram/handlers/commands/voice.py +112 -0
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +2043 -0
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +954 -5689
- codex_autorunner/integrations/telegram/handlers/{commands.py → commands_spec.py} +11 -4
- codex_autorunner/integrations/telegram/handlers/messages.py +374 -49
- codex_autorunner/integrations/telegram/handlers/questions.py +389 -0
- codex_autorunner/integrations/telegram/handlers/selections.py +6 -4
- codex_autorunner/integrations/telegram/handlers/utils.py +171 -0
- codex_autorunner/integrations/telegram/helpers.py +90 -18
- codex_autorunner/integrations/telegram/notifications.py +126 -35
- codex_autorunner/integrations/telegram/outbox.py +214 -43
- codex_autorunner/integrations/telegram/progress_stream.py +42 -19
- codex_autorunner/integrations/telegram/runtime.py +24 -13
- codex_autorunner/integrations/telegram/service.py +500 -129
- codex_autorunner/integrations/telegram/state.py +1278 -330
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
- codex_autorunner/integrations/telegram/transport.py +37 -4
- codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
- codex_autorunner/integrations/telegram/types.py +22 -2
- codex_autorunner/integrations/telegram/voice.py +14 -15
- codex_autorunner/manifest.py +2 -0
- codex_autorunner/plugin_api.py +22 -0
- codex_autorunner/routes/__init__.py +25 -14
- codex_autorunner/routes/agents.py +18 -78
- codex_autorunner/routes/analytics.py +239 -0
- codex_autorunner/routes/base.py +142 -113
- 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/repos.py +17 -0
- codex_autorunner/routes/review.py +148 -0
- codex_autorunner/routes/sessions.py +16 -8
- codex_autorunner/routes/settings.py +22 -0
- codex_autorunner/routes/shared.py +33 -3
- codex_autorunner/routes/system.py +22 -1
- codex_autorunner/routes/usage.py +87 -0
- codex_autorunner/routes/voice.py +5 -13
- codex_autorunner/routes/workspace.py +271 -0
- codex_autorunner/server.py +2 -1
- codex_autorunner/static/agentControls.js +9 -1
- codex_autorunner/static/agentEvents.js +248 -0
- codex_autorunner/static/app.js +27 -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 -150
- 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 +67 -126
- codex_autorunner/static/index.html +788 -807
- codex_autorunner/static/liveUpdates.js +59 -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 -205
- codex_autorunner/static/styles.css +7577 -3758
- codex_autorunner/static/tabs.js +28 -5
- codex_autorunner/static/terminal.js +14 -0
- codex_autorunner/static/terminalManager.js +53 -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 +21 -7
- 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/voice/capture.py +7 -7
- codex_autorunner/voice/service.py +51 -9
- codex_autorunner/web/app.py +419 -199
- codex_autorunner/web/hub_jobs.py +13 -2
- codex_autorunner/web/middleware.py +47 -13
- codex_autorunner/web/pty_session.py +26 -13
- codex_autorunner/web/schemas.py +114 -109
- codex_autorunner/web/static_assets.py +55 -42
- codex_autorunner/web/static_refresh.py +86 -0
- codex_autorunner/workspace/__init__.py +40 -0
- codex_autorunner/workspace/paths.py +319 -0
- {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +20 -21
- codex_autorunner-1.0.0.dist-info/RECORD +251 -0
- {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/WHEEL +1 -1
- codex_autorunner/core/doc_chat.py +0 -1415
- 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 -118
- codex_autorunner/spec_ingest.py +0 -788
- codex_autorunner/static/docChatActions.js +0 -279
- 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 -274
- 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 -442
- codex_autorunner/static/logs.js +0 -640
- codex_autorunner/static/runs.js +0 -409
- codex_autorunner/static/snapshot.js +0 -124
- codex_autorunner/static/state.js +0 -86
- codex_autorunner/static/todoPreview.js +0 -27
- codex_autorunner/workspace.py +0 -16
- codex_autorunner-0.1.1.dist-info/RECORD +0 -191
- {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -3,117 +3,31 @@ from __future__ import annotations
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Callable, Optional
|
|
5
5
|
|
|
6
|
-
from .config import
|
|
7
|
-
AppServerAutorunnerPromptConfig,
|
|
8
|
-
AppServerDocChatPromptConfig,
|
|
9
|
-
AppServerSpecIngestPromptConfig,
|
|
10
|
-
Config,
|
|
11
|
-
)
|
|
6
|
+
from .config import AppServerAutorunnerPromptConfig, Config
|
|
12
7
|
|
|
13
8
|
TRUNCATION_MARKER = "...[truncated]"
|
|
14
9
|
|
|
15
10
|
|
|
16
|
-
DOC_CHAT_APP_SERVER_TEMPLATE = """You are an autonomous coding assistant helping maintain the work docs for this repository.
|
|
17
|
-
|
|
18
|
-
Instructions:
|
|
19
|
-
- Use the base doc content below. Drafts (if present) are the authoritative base.
|
|
20
|
-
- You may inspect the repo and update the work docs listed when needed.
|
|
21
|
-
- If you update docs, edit the files directly. If no changes are needed, do not edit files.
|
|
22
|
-
- Respond with a short summary of what you did or found.
|
|
23
|
-
|
|
24
|
-
Work docs (paths):
|
|
25
|
-
- TODO: {todo_path}
|
|
26
|
-
- PROGRESS: {progress_path}
|
|
27
|
-
- OPINIONS: {opinions_path}
|
|
28
|
-
- SPEC: {spec_path}
|
|
29
|
-
- SUMMARY: {summary_path}
|
|
30
|
-
|
|
31
|
-
{user_viewing_block}
|
|
32
|
-
|
|
33
|
-
User request:
|
|
34
|
-
{message}
|
|
35
|
-
|
|
36
|
-
{docs_context_block}
|
|
37
|
-
{recent_summary_block}
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
SPEC_INGEST_APP_SERVER_TEMPLATE = """You are preparing work docs (TODO/PROGRESS/OPINIONS) from the SPEC.
|
|
42
|
-
|
|
43
|
-
SPEC path: {spec_path}
|
|
44
|
-
TODO path: {todo_path}
|
|
45
|
-
PROGRESS path: {progress_path}
|
|
46
|
-
OPINIONS path: {opinions_path}
|
|
47
|
-
|
|
48
|
-
Instructions:
|
|
49
|
-
- Read the SPEC and existing docs from disk.
|
|
50
|
-
- Edit the TODO, PROGRESS, and OPINIONS files directly to reflect the SPEC.
|
|
51
|
-
- The TODO must be a Markdown checklist. Every task MUST be a checkbox line:
|
|
52
|
-
- Use `- [ ] <task>` for open items and `- [x] <task>` for completed items.
|
|
53
|
-
- Do NOT use plain bullets like `- task` or paragraphs for tasks.
|
|
54
|
-
- Do NOT output a patch block. Just edit the files.
|
|
55
|
-
- Output a short summary prefixed with "Agent: " explaining what you did.
|
|
56
|
-
|
|
57
|
-
User request:
|
|
58
|
-
{message}
|
|
59
|
-
|
|
60
|
-
{spec_excerpt_block}
|
|
61
|
-
"""
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
SNAPSHOT_APP_SERVER_TEMPLATE = """You are generating a compact Markdown repo snapshot meant to be pasted into another LLM chat.
|
|
65
|
-
|
|
66
|
-
Snapshot path: {snapshot_path}
|
|
67
|
-
|
|
68
|
-
Instructions:
|
|
69
|
-
- Analyze the provided context and the repository.
|
|
70
|
-
- Write the snapshot content directly to the snapshot path.
|
|
71
|
-
- Keep the file concise and high-signal.
|
|
72
|
-
|
|
73
|
-
Required output format (keep headings exactly):
|
|
74
|
-
# Repo Snapshot
|
|
75
|
-
|
|
76
|
-
## What this repo is
|
|
77
|
-
- 3–6 bullets.
|
|
78
|
-
|
|
79
|
-
## Architecture overview
|
|
80
|
-
- Components and responsibilities.
|
|
81
|
-
- Data/control flow (high level).
|
|
82
|
-
- How things actually work
|
|
83
|
-
|
|
84
|
-
## Key files and modules
|
|
85
|
-
- Bullet list of important paths with 1-line notes.
|
|
86
|
-
|
|
87
|
-
## Extension points and sharp edges
|
|
88
|
-
- Config/state/concurrency hazards, limits, sharp edges.
|
|
89
|
-
|
|
90
|
-
Inputs:
|
|
91
|
-
{seed_context}
|
|
92
|
-
|
|
93
|
-
{changes_block}
|
|
94
|
-
{previous_snapshot_block}
|
|
95
|
-
"""
|
|
96
|
-
|
|
97
|
-
|
|
98
11
|
AUTORUNNER_APP_SERVER_TEMPLATE = """You are an autonomous coding assistant operating on a git repository.
|
|
99
12
|
|
|
100
|
-
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
-
|
|
104
|
-
|
|
105
|
-
|
|
13
|
+
Workspace docs (optional; read from disk when useful):
|
|
14
|
+
- Active context: {active_context_path}
|
|
15
|
+
- Decisions: {decisions_path}
|
|
16
|
+
- Spec: {spec_path}
|
|
17
|
+
|
|
18
|
+
Tickets:
|
|
19
|
+
- The authoritative work items are ticket files under `.codex-autorunner/tickets/`.
|
|
20
|
+
- Pick the next not-done ticket, implement it, and update the ticket file (`done: true`) when complete.
|
|
106
21
|
|
|
107
22
|
Instructions:
|
|
108
|
-
-
|
|
109
|
-
- Prefer
|
|
110
|
-
-
|
|
111
|
-
- Make actual edits in the repo as needed.
|
|
23
|
+
- This run is non-interactive. Do not ask the user questions. If unsure, make reasonable assumptions and proceed.
|
|
24
|
+
- Prefer small, safe diffs and keep work focused on the current ticket.
|
|
25
|
+
- You may create new tickets only when needed to break down the current work.
|
|
112
26
|
|
|
113
27
|
User request:
|
|
114
28
|
{message}
|
|
115
29
|
|
|
116
|
-
{
|
|
30
|
+
{workspace_spec_block}
|
|
117
31
|
{prev_run_block}
|
|
118
32
|
"""
|
|
119
33
|
|
|
@@ -167,144 +81,6 @@ def _shrink_prompt(
|
|
|
167
81
|
return prompt
|
|
168
82
|
|
|
169
83
|
|
|
170
|
-
def build_doc_chat_prompt(
|
|
171
|
-
config: Config,
|
|
172
|
-
*,
|
|
173
|
-
message: str,
|
|
174
|
-
recent_summary: Optional[str],
|
|
175
|
-
docs: dict[str, dict[str, str]],
|
|
176
|
-
context_doc: Optional[str] = None,
|
|
177
|
-
) -> str:
|
|
178
|
-
prompt_cfg: AppServerDocChatPromptConfig = config.app_server.prompts.doc_chat
|
|
179
|
-
doc_paths = {
|
|
180
|
-
"todo": _display_path(config.root, config.doc_path("todo")),
|
|
181
|
-
"progress": _display_path(config.root, config.doc_path("progress")),
|
|
182
|
-
"opinions": _display_path(config.root, config.doc_path("opinions")),
|
|
183
|
-
"spec": _display_path(config.root, config.doc_path("spec")),
|
|
184
|
-
"summary": _display_path(config.root, config.doc_path("summary")),
|
|
185
|
-
}
|
|
186
|
-
message_text = truncate_text(message, prompt_cfg.message_max_chars)
|
|
187
|
-
doc_blocks = []
|
|
188
|
-
for key, path in doc_paths.items():
|
|
189
|
-
payload = docs.get(key, {})
|
|
190
|
-
source = payload.get("source") or "disk"
|
|
191
|
-
content = truncate_text(
|
|
192
|
-
str(payload.get("content") or ""), prompt_cfg.target_excerpt_max_chars
|
|
193
|
-
)
|
|
194
|
-
if not content.strip():
|
|
195
|
-
content = "(empty)"
|
|
196
|
-
label = f"{key.upper()} [{path}] ({source.upper()})"
|
|
197
|
-
doc_blocks.append(f"{label}\n{content}")
|
|
198
|
-
docs_context = "\n\n".join(doc_blocks)
|
|
199
|
-
recent_text = truncate_text(
|
|
200
|
-
recent_summary or "", prompt_cfg.recent_summary_max_chars
|
|
201
|
-
)
|
|
202
|
-
user_viewing = ""
|
|
203
|
-
if context_doc:
|
|
204
|
-
user_viewing = f"The user is currently looking at {context_doc.upper()}."
|
|
205
|
-
|
|
206
|
-
sections = {
|
|
207
|
-
"message": message_text,
|
|
208
|
-
"docs_context": docs_context,
|
|
209
|
-
"recent_summary": recent_text,
|
|
210
|
-
"user_viewing": user_viewing,
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
def render() -> str:
|
|
214
|
-
return DOC_CHAT_APP_SERVER_TEMPLATE.format(
|
|
215
|
-
todo_path=doc_paths["todo"],
|
|
216
|
-
progress_path=doc_paths["progress"],
|
|
217
|
-
opinions_path=doc_paths["opinions"],
|
|
218
|
-
spec_path=doc_paths["spec"],
|
|
219
|
-
summary_path=doc_paths["summary"],
|
|
220
|
-
message=sections["message"],
|
|
221
|
-
user_viewing_block=_optional_block(
|
|
222
|
-
"USER_VIEWING", sections["user_viewing"]
|
|
223
|
-
),
|
|
224
|
-
docs_context_block=_optional_block("DOC_BASES", sections["docs_context"]),
|
|
225
|
-
recent_summary_block=_optional_block(
|
|
226
|
-
"RECENT_RUN_SUMMARY", sections["recent_summary"]
|
|
227
|
-
),
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
return _shrink_prompt(
|
|
231
|
-
max_chars=prompt_cfg.max_chars,
|
|
232
|
-
render=render,
|
|
233
|
-
sections=sections,
|
|
234
|
-
order=["recent_summary", "docs_context", "message"],
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
def build_spec_ingest_prompt(
|
|
239
|
-
config: Config,
|
|
240
|
-
*,
|
|
241
|
-
message: str,
|
|
242
|
-
spec_path: Optional[Path] = None,
|
|
243
|
-
) -> str:
|
|
244
|
-
prompt_cfg: AppServerSpecIngestPromptConfig = config.app_server.prompts.spec_ingest
|
|
245
|
-
doc_paths = {
|
|
246
|
-
"todo": _display_path(config.root, config.doc_path("todo")),
|
|
247
|
-
"progress": _display_path(config.root, config.doc_path("progress")),
|
|
248
|
-
"opinions": _display_path(config.root, config.doc_path("opinions")),
|
|
249
|
-
}
|
|
250
|
-
spec_target = spec_path or config.doc_path("spec")
|
|
251
|
-
spec_path_str = _display_path(config.root, spec_target)
|
|
252
|
-
message_text = truncate_text(message, prompt_cfg.message_max_chars)
|
|
253
|
-
spec_excerpt = truncate_text(
|
|
254
|
-
spec_target.read_text(encoding="utf-8"),
|
|
255
|
-
prompt_cfg.spec_excerpt_max_chars,
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
sections = {
|
|
259
|
-
"message": message_text,
|
|
260
|
-
"spec_excerpt": spec_excerpt,
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
def render() -> str:
|
|
264
|
-
return SPEC_INGEST_APP_SERVER_TEMPLATE.format(
|
|
265
|
-
spec_path=spec_path_str,
|
|
266
|
-
todo_path=doc_paths["todo"],
|
|
267
|
-
progress_path=doc_paths["progress"],
|
|
268
|
-
opinions_path=doc_paths["opinions"],
|
|
269
|
-
message=sections["message"],
|
|
270
|
-
spec_excerpt_block=_optional_block(
|
|
271
|
-
"SPEC_EXCERPT", sections["spec_excerpt"]
|
|
272
|
-
),
|
|
273
|
-
)
|
|
274
|
-
|
|
275
|
-
return _shrink_prompt(
|
|
276
|
-
max_chars=prompt_cfg.max_chars,
|
|
277
|
-
render=render,
|
|
278
|
-
sections=sections,
|
|
279
|
-
order=["spec_excerpt", "message"],
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
def build_app_server_snapshot_prompt(
|
|
284
|
-
config: Config,
|
|
285
|
-
*,
|
|
286
|
-
seed_context: str,
|
|
287
|
-
previous_snapshot: Optional[str] = None,
|
|
288
|
-
changes: Optional[str] = None,
|
|
289
|
-
) -> str:
|
|
290
|
-
snapshot_path = config.doc_path("snapshot")
|
|
291
|
-
previous_block = ""
|
|
292
|
-
if previous_snapshot:
|
|
293
|
-
previous_block = (
|
|
294
|
-
f"<PREVIOUS_SNAPSHOT>\n{previous_snapshot.strip()}\n</PREVIOUS_SNAPSHOT>"
|
|
295
|
-
)
|
|
296
|
-
changes_block = ""
|
|
297
|
-
if changes:
|
|
298
|
-
changes_block = f"<CHANGES_SINCE_LAST_SNAPSHOT>\n{changes.strip()}\n</CHANGES_SINCE_LAST_SNAPSHOT>"
|
|
299
|
-
|
|
300
|
-
return SNAPSHOT_APP_SERVER_TEMPLATE.format(
|
|
301
|
-
snapshot_path=snapshot_path,
|
|
302
|
-
seed_context=seed_context,
|
|
303
|
-
changes_block=changes_block,
|
|
304
|
-
previous_snapshot_block=previous_block,
|
|
305
|
-
)
|
|
306
|
-
|
|
307
|
-
|
|
308
84
|
def build_autorunner_prompt(
|
|
309
85
|
config: Config,
|
|
310
86
|
*,
|
|
@@ -313,35 +89,36 @@ def build_autorunner_prompt(
|
|
|
313
89
|
) -> str:
|
|
314
90
|
prompt_cfg: AppServerAutorunnerPromptConfig = config.app_server.prompts.autorunner
|
|
315
91
|
doc_paths = {
|
|
316
|
-
"
|
|
317
|
-
"
|
|
318
|
-
"opinions": _display_path(config.root, config.doc_path("opinions")),
|
|
92
|
+
"active_context": _display_path(config.root, config.doc_path("active_context")),
|
|
93
|
+
"decisions": _display_path(config.root, config.doc_path("decisions")),
|
|
319
94
|
"spec": _display_path(config.root, config.doc_path("spec")),
|
|
320
|
-
"summary": _display_path(config.root, config.doc_path("summary")),
|
|
321
95
|
}
|
|
96
|
+
|
|
322
97
|
message_text = truncate_text(message, prompt_cfg.message_max_chars)
|
|
323
|
-
|
|
324
|
-
|
|
98
|
+
spec_excerpt = truncate_text(
|
|
99
|
+
(
|
|
100
|
+
config.doc_path("spec").read_text(encoding="utf-8")
|
|
101
|
+
if config.doc_path("spec").exists()
|
|
102
|
+
else ""
|
|
103
|
+
),
|
|
325
104
|
prompt_cfg.todo_excerpt_max_chars,
|
|
326
105
|
)
|
|
327
106
|
prev_run_text = truncate_text(prev_run_summary or "", prompt_cfg.prev_run_max_chars)
|
|
328
107
|
|
|
329
108
|
sections = {
|
|
330
109
|
"message": message_text,
|
|
331
|
-
"
|
|
110
|
+
"workspace_spec": spec_excerpt,
|
|
332
111
|
"prev_run": prev_run_text,
|
|
333
112
|
}
|
|
334
113
|
|
|
335
114
|
def render() -> str:
|
|
336
115
|
return AUTORUNNER_APP_SERVER_TEMPLATE.format(
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
opinions_path=doc_paths["opinions"],
|
|
116
|
+
active_context_path=doc_paths["active_context"],
|
|
117
|
+
decisions_path=doc_paths["decisions"],
|
|
340
118
|
spec_path=doc_paths["spec"],
|
|
341
|
-
summary_path=doc_paths["summary"],
|
|
342
119
|
message=sections["message"],
|
|
343
|
-
|
|
344
|
-
"
|
|
120
|
+
workspace_spec_block=_optional_block(
|
|
121
|
+
"WORKSPACE_SPEC", sections["workspace_spec"]
|
|
345
122
|
),
|
|
346
123
|
prev_run_block=_optional_block("PREV_RUN_SUMMARY", sections["prev_run"]),
|
|
347
124
|
)
|
|
@@ -350,13 +127,11 @@ def build_autorunner_prompt(
|
|
|
350
127
|
max_chars=prompt_cfg.max_chars,
|
|
351
128
|
render=render,
|
|
352
129
|
sections=sections,
|
|
353
|
-
order=["prev_run", "
|
|
130
|
+
order=["prev_run", "workspace_spec", "message"],
|
|
354
131
|
)
|
|
355
132
|
|
|
356
133
|
|
|
357
134
|
APP_SERVER_PROMPT_BUILDERS = {
|
|
358
|
-
"doc_chat": build_doc_chat_prompt,
|
|
359
|
-
"spec_ingest": build_spec_ingest_prompt,
|
|
360
135
|
"autorunner": build_autorunner_prompt,
|
|
361
136
|
}
|
|
362
137
|
|
|
@@ -364,13 +139,7 @@ APP_SERVER_PROMPT_BUILDERS = {
|
|
|
364
139
|
__all__ = [
|
|
365
140
|
"AUTORUNNER_APP_SERVER_TEMPLATE",
|
|
366
141
|
"APP_SERVER_PROMPT_BUILDERS",
|
|
367
|
-
"DOC_CHAT_APP_SERVER_TEMPLATE",
|
|
368
|
-
"SPEC_INGEST_APP_SERVER_TEMPLATE",
|
|
369
|
-
"SNAPSHOT_APP_SERVER_TEMPLATE",
|
|
370
142
|
"TRUNCATION_MARKER",
|
|
371
143
|
"build_autorunner_prompt",
|
|
372
|
-
"build_doc_chat_prompt",
|
|
373
|
-
"build_spec_ingest_prompt",
|
|
374
|
-
"build_app_server_snapshot_prompt",
|
|
375
144
|
"truncate_text",
|
|
376
145
|
]
|
|
@@ -12,19 +12,15 @@ APP_SERVER_THREADS_FILENAME = ".codex-autorunner/app_server_threads.json"
|
|
|
12
12
|
APP_SERVER_THREADS_VERSION = 1
|
|
13
13
|
APP_SERVER_THREADS_CORRUPT_SUFFIX = ".corrupt"
|
|
14
14
|
APP_SERVER_THREADS_NOTICE_SUFFIX = ".corrupt.json"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
FEATURE_KEYS = DOC_CHAT_KEYS | {
|
|
26
|
-
"spec_ingest",
|
|
27
|
-
"spec_ingest.opencode",
|
|
15
|
+
FILE_CHAT_KEY = "file_chat"
|
|
16
|
+
FILE_CHAT_OPENCODE_KEY = "file_chat.opencode"
|
|
17
|
+
FILE_CHAT_PREFIX = "file_chat."
|
|
18
|
+
FILE_CHAT_OPENCODE_PREFIX = "file_chat.opencode."
|
|
19
|
+
|
|
20
|
+
# Static keys that can be reset/managed via the UI.
|
|
21
|
+
FEATURE_KEYS = {
|
|
22
|
+
FILE_CHAT_KEY,
|
|
23
|
+
FILE_CHAT_OPENCODE_KEY,
|
|
28
24
|
"autorunner",
|
|
29
25
|
"autorunner.opencode",
|
|
30
26
|
}
|
|
@@ -43,6 +39,10 @@ def normalize_feature_key(raw: str) -> str:
|
|
|
43
39
|
key = key.replace("/", ".").replace(":", ".")
|
|
44
40
|
if key in FEATURE_KEYS:
|
|
45
41
|
return key
|
|
42
|
+
# Allow per-target file chat threads (e.g. file_chat.ticket.1, file_chat.workspace.spec).
|
|
43
|
+
for prefix in (FILE_CHAT_PREFIX, FILE_CHAT_OPENCODE_PREFIX):
|
|
44
|
+
if key.startswith(prefix) and len(key) > len(prefix):
|
|
45
|
+
return key
|
|
46
46
|
raise ValueError(f"invalid feature key: {raw}")
|
|
47
47
|
|
|
48
48
|
|
|
@@ -84,20 +84,9 @@ class AppServerThreadRegistry:
|
|
|
84
84
|
|
|
85
85
|
def feature_map(self) -> dict[str, object]:
|
|
86
86
|
threads = self.load()
|
|
87
|
-
doc_chat_thread = threads.get(DOC_CHAT_KEY)
|
|
88
|
-
doc_chat_opencode_thread = threads.get(DOC_CHAT_OPENCODE_KEY)
|
|
89
87
|
payload: dict[str, object] = {
|
|
90
|
-
"
|
|
91
|
-
|
|
92
|
-
for kind in DOC_CHAT_KINDS
|
|
93
|
-
},
|
|
94
|
-
"doc_chat_opencode": {
|
|
95
|
-
kind: doc_chat_opencode_thread
|
|
96
|
-
or threads.get(f"{DOC_CHAT_OPENCODE_PREFIX}{kind}")
|
|
97
|
-
for kind in DOC_CHAT_KINDS
|
|
98
|
-
},
|
|
99
|
-
"spec_ingest": threads.get("spec_ingest"),
|
|
100
|
-
"spec_ingest_opencode": threads.get("spec_ingest.opencode"),
|
|
88
|
+
"file_chat": threads.get(FILE_CHAT_KEY),
|
|
89
|
+
"file_chat_opencode": threads.get(FILE_CHAT_OPENCODE_KEY),
|
|
101
90
|
"autorunner": threads.get("autorunner"),
|
|
102
91
|
"autorunner_opencode": threads.get("autorunner.opencode"),
|
|
103
92
|
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import AsyncIterator, Optional
|
|
10
|
+
|
|
11
|
+
from .exceptions import CircuitOpenError
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CircuitState(str, Enum):
|
|
17
|
+
CLOSED = "closed"
|
|
18
|
+
OPEN = "open"
|
|
19
|
+
HALF_OPEN = "half_open"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class CircuitBreakerConfig:
|
|
24
|
+
"""Configuration for circuit breaker behavior."""
|
|
25
|
+
|
|
26
|
+
failure_threshold: int = 5
|
|
27
|
+
"""Number of failures before opening circuit."""
|
|
28
|
+
|
|
29
|
+
timeout_seconds: int = 60
|
|
30
|
+
"""Seconds to wait before attempting recovery (half-open state)."""
|
|
31
|
+
|
|
32
|
+
half_open_attempts: int = 1
|
|
33
|
+
"""Number of successful calls needed to close circuit from half-open state."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class CircuitBreakerState:
|
|
38
|
+
"""Internal state tracking for circuit breaker."""
|
|
39
|
+
|
|
40
|
+
failure_count: int = 0
|
|
41
|
+
state: CircuitState = CircuitState.CLOSED
|
|
42
|
+
last_failure_time: Optional[datetime] = None
|
|
43
|
+
success_count: int = 0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CircuitBreaker:
|
|
47
|
+
"""
|
|
48
|
+
Circuit breaker pattern implementation for external service resilience.
|
|
49
|
+
|
|
50
|
+
Opens after N consecutive failures, closes after success or timeout.
|
|
51
|
+
Prevents cascading failures and provides fast-fail for degraded services.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
service_name: str,
|
|
57
|
+
*,
|
|
58
|
+
config: Optional[CircuitBreakerConfig] = None,
|
|
59
|
+
logger: Optional[logging.Logger] = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
self._service_name = service_name
|
|
62
|
+
self._config = config or CircuitBreakerConfig()
|
|
63
|
+
self._logger = logger or logging.getLogger(__name__)
|
|
64
|
+
self._state = CircuitBreakerState()
|
|
65
|
+
self._lock = asyncio.Lock()
|
|
66
|
+
|
|
67
|
+
@asynccontextmanager
|
|
68
|
+
async def call(self) -> AsyncIterator[None]:
|
|
69
|
+
"""
|
|
70
|
+
Context manager that raises CircuitOpenError if circuit is open.
|
|
71
|
+
|
|
72
|
+
Tracks failures/successes to manage circuit state transitions.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
CircuitOpenError: If circuit is open.
|
|
76
|
+
|
|
77
|
+
Example:
|
|
78
|
+
async with circuit_breaker.call():
|
|
79
|
+
result = await external_service.request()
|
|
80
|
+
"""
|
|
81
|
+
is_open = False
|
|
82
|
+
async with self._lock:
|
|
83
|
+
if self._should_open_circuit():
|
|
84
|
+
self._open_circuit()
|
|
85
|
+
is_open = True
|
|
86
|
+
elif self._should_half_open_circuit():
|
|
87
|
+
self._half_open_circuit()
|
|
88
|
+
elif self._state.state == CircuitState.OPEN:
|
|
89
|
+
is_open = True
|
|
90
|
+
|
|
91
|
+
if is_open:
|
|
92
|
+
raise CircuitOpenError(
|
|
93
|
+
self._service_name,
|
|
94
|
+
message=f"Circuit breaker OPEN for {self._service_name}. "
|
|
95
|
+
f"Last failure: {self._state.last_failure_time}",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
yield
|
|
100
|
+
except Exception as exc:
|
|
101
|
+
self._logger.debug(
|
|
102
|
+
"Exception caught by circuit breaker for %s: %s",
|
|
103
|
+
self._service_name,
|
|
104
|
+
exc,
|
|
105
|
+
)
|
|
106
|
+
await self._record_failure()
|
|
107
|
+
raise
|
|
108
|
+
else:
|
|
109
|
+
await self._record_success()
|
|
110
|
+
|
|
111
|
+
def _should_open_circuit(self) -> bool:
|
|
112
|
+
"""Check if circuit should open based on failure count."""
|
|
113
|
+
return (
|
|
114
|
+
self._state.state == CircuitState.CLOSED
|
|
115
|
+
and self._state.failure_count >= self._config.failure_threshold
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def _should_half_open_circuit(self) -> bool:
|
|
119
|
+
"""Check if circuit should transition to half-open state."""
|
|
120
|
+
if self._state.state != CircuitState.OPEN:
|
|
121
|
+
return False
|
|
122
|
+
if self._state.last_failure_time is None:
|
|
123
|
+
return False
|
|
124
|
+
return datetime.utcnow() >= self._state.last_failure_time + timedelta(
|
|
125
|
+
seconds=self._config.timeout_seconds
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def _open_circuit(self) -> None:
|
|
129
|
+
"""Open circuit and log the transition."""
|
|
130
|
+
self._state.state = CircuitState.OPEN
|
|
131
|
+
self._state.last_failure_time = datetime.utcnow()
|
|
132
|
+
self._logger.warning(
|
|
133
|
+
"Circuit breaker OPEN for %s after %d failures",
|
|
134
|
+
self._service_name,
|
|
135
|
+
self._state.failure_count,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def _half_open_circuit(self) -> None:
|
|
139
|
+
"""Transition to half-open state and allow one test call."""
|
|
140
|
+
self._state.state = CircuitState.HALF_OPEN
|
|
141
|
+
self._state.success_count = 0
|
|
142
|
+
self._logger.info(
|
|
143
|
+
"Circuit breaker HALF_OPEN for %s (testing recovery)",
|
|
144
|
+
self._service_name,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
async def _record_failure(self) -> None:
|
|
148
|
+
"""Record a failure and update state accordingly."""
|
|
149
|
+
async with self._lock:
|
|
150
|
+
self._state.failure_count += 1
|
|
151
|
+
self._state.last_failure_time = datetime.utcnow()
|
|
152
|
+
|
|
153
|
+
if self._state.state == CircuitState.HALF_OPEN:
|
|
154
|
+
self._state.state = CircuitState.OPEN
|
|
155
|
+
self._logger.warning(
|
|
156
|
+
"Circuit breaker OPEN for %s (failure in half-open state)",
|
|
157
|
+
self._service_name,
|
|
158
|
+
)
|
|
159
|
+
elif self._state.state == CircuitState.CLOSED:
|
|
160
|
+
self._logger.debug(
|
|
161
|
+
"Circuit breaker recorded failure for %s (count: %d/%d)",
|
|
162
|
+
self._service_name,
|
|
163
|
+
self._state.failure_count,
|
|
164
|
+
self._config.failure_threshold,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
async def _record_success(self) -> None:
|
|
168
|
+
"""Record a success and update state accordingly."""
|
|
169
|
+
async with self._lock:
|
|
170
|
+
if self._state.state == CircuitState.CLOSED:
|
|
171
|
+
self._state.failure_count = 0
|
|
172
|
+
elif self._state.state == CircuitState.HALF_OPEN:
|
|
173
|
+
self._state.success_count += 1
|
|
174
|
+
if self._state.success_count >= self._config.half_open_attempts:
|
|
175
|
+
self._state.state = CircuitState.CLOSED
|
|
176
|
+
self._state.failure_count = 0
|
|
177
|
+
self._state.success_count = 0
|
|
178
|
+
self._logger.info(
|
|
179
|
+
"Circuit breaker CLOSED for %s (recovery successful)",
|
|
180
|
+
self._service_name,
|
|
181
|
+
)
|
|
182
|
+
else:
|
|
183
|
+
pass
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
# DEPRECATED: This module implements a Codex CLI subprocess runner.
|
|
2
|
+
# The primary execution path now uses the Codex app-server via OpenCode runtime.
|
|
3
|
+
# This file is kept for potential future CLI-as-backend support but is currently
|
|
4
|
+
# not referenced by the main engine. See src/codex_autorunner/core/engine.py for
|
|
5
|
+
# the current execution path (_run_codex_app_server_async).
|
|
6
|
+
|
|
1
7
|
import asyncio
|
|
2
8
|
import subprocess
|
|
3
9
|
from pathlib import Path
|