codex-autorunner 1.1.0__py3-none-any.whl → 1.2.1__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/agents/opencode/client.py +113 -4
- codex_autorunner/agents/opencode/supervisor.py +4 -0
- codex_autorunner/agents/registry.py +17 -7
- codex_autorunner/bootstrap.py +219 -1
- codex_autorunner/core/__init__.py +17 -1
- codex_autorunner/core/about_car.py +124 -11
- codex_autorunner/core/app_server_threads.py +6 -0
- codex_autorunner/core/config.py +238 -3
- codex_autorunner/core/context_awareness.py +39 -0
- codex_autorunner/core/docs.py +0 -122
- codex_autorunner/core/filebox.py +265 -0
- codex_autorunner/core/flows/controller.py +71 -1
- codex_autorunner/core/flows/reconciler.py +4 -1
- codex_autorunner/core/flows/runtime.py +22 -0
- codex_autorunner/core/flows/store.py +61 -9
- codex_autorunner/core/flows/transition.py +23 -16
- codex_autorunner/core/flows/ux_helpers.py +18 -3
- codex_autorunner/core/flows/worker_process.py +32 -6
- codex_autorunner/core/hub.py +198 -41
- codex_autorunner/core/lifecycle_events.py +253 -0
- codex_autorunner/core/path_utils.py +2 -1
- codex_autorunner/core/pma_audit.py +224 -0
- codex_autorunner/core/pma_context.py +683 -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/agent_backend.py +2 -5
- codex_autorunner/core/ports/run_event.py +1 -4
- 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 +5 -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/ticket_linter_cli.py +17 -0
- codex_autorunner/core/ticket_manager_cli.py +154 -92
- codex_autorunner/core/time_utils.py +11 -0
- codex_autorunner/core/types.py +18 -0
- codex_autorunner/core/utils.py +34 -6
- codex_autorunner/flows/review/service.py +23 -25
- codex_autorunner/flows/ticket_flow/definition.py +43 -1
- codex_autorunner/integrations/agents/__init__.py +2 -0
- codex_autorunner/integrations/agents/backend_orchestrator.py +18 -0
- codex_autorunner/integrations/agents/codex_backend.py +19 -8
- codex_autorunner/integrations/agents/runner.py +3 -8
- codex_autorunner/integrations/agents/wiring.py +8 -0
- codex_autorunner/integrations/telegram/adapter.py +1 -1
- codex_autorunner/integrations/telegram/config.py +1 -1
- codex_autorunner/integrations/telegram/doctor.py +228 -6
- 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 +346 -58
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +202 -45
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +18 -7
- codex_autorunner/integrations/telegram/handlers/messages.py +34 -3
- codex_autorunner/integrations/telegram/helpers.py +1 -3
- codex_autorunner/integrations/telegram/runtime.py +9 -4
- codex_autorunner/integrations/telegram/service.py +30 -0
- codex_autorunner/integrations/telegram/state.py +38 -0
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +10 -4
- codex_autorunner/integrations/telegram/transport.py +10 -3
- codex_autorunner/integrations/templates/__init__.py +27 -0
- codex_autorunner/integrations/templates/scan_agent.py +312 -0
- codex_autorunner/server.py +2 -2
- codex_autorunner/static/agentControls.js +21 -5
- codex_autorunner/static/app.js +115 -11
- codex_autorunner/static/archive.js +274 -81
- codex_autorunner/static/archiveApi.js +21 -0
- codex_autorunner/static/chatUploads.js +137 -0
- codex_autorunner/static/constants.js +1 -1
- 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 +46 -81
- codex_autorunner/static/index.html +303 -24
- codex_autorunner/static/messages.js +82 -4
- codex_autorunner/static/notifications.js +288 -0
- codex_autorunner/static/pma.js +1167 -0
- codex_autorunner/static/settings.js +3 -0
- codex_autorunner/static/streamUtils.js +57 -0
- codex_autorunner/static/styles.css +9141 -6742
- codex_autorunner/static/templateReposSettings.js +225 -0
- codex_autorunner/static/terminalManager.js +22 -3
- codex_autorunner/static/ticketChatActions.js +165 -3
- codex_autorunner/static/ticketChatStream.js +17 -119
- codex_autorunner/static/ticketEditor.js +41 -13
- codex_autorunner/static/ticketTemplates.js +798 -0
- codex_autorunner/static/tickets.js +69 -19
- codex_autorunner/static/turnEvents.js +27 -0
- codex_autorunner/static/turnResume.js +33 -0
- codex_autorunner/static/utils.js +28 -0
- codex_autorunner/static/workspace.js +258 -44
- codex_autorunner/static/workspaceFileBrowser.js +6 -4
- codex_autorunner/surfaces/cli/cli.py +1465 -155
- codex_autorunner/surfaces/cli/pma_cli.py +817 -0
- codex_autorunner/surfaces/web/app.py +253 -49
- codex_autorunner/surfaces/web/routes/__init__.py +4 -0
- codex_autorunner/surfaces/web/routes/analytics.py +29 -22
- codex_autorunner/surfaces/web/routes/archive.py +197 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +297 -36
- codex_autorunner/surfaces/web/routes/filebox.py +227 -0
- codex_autorunner/surfaces/web/routes/flows.py +219 -29
- codex_autorunner/surfaces/web/routes/messages.py +70 -39
- codex_autorunner/surfaces/web/routes/pma.py +1652 -0
- codex_autorunner/surfaces/web/routes/repos.py +1 -1
- codex_autorunner/surfaces/web/routes/shared.py +0 -3
- codex_autorunner/surfaces/web/routes/templates.py +634 -0
- codex_autorunner/surfaces/web/runner_manager.py +2 -2
- codex_autorunner/surfaces/web/schemas.py +81 -18
- codex_autorunner/tickets/agent_pool.py +27 -0
- codex_autorunner/tickets/files.py +33 -16
- codex_autorunner/tickets/lint.py +50 -0
- codex_autorunner/tickets/models.py +3 -0
- codex_autorunner/tickets/outbox.py +41 -5
- codex_autorunner/tickets/runner.py +350 -69
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/METADATA +15 -19
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/RECORD +132 -101
- codex_autorunner/core/adapter_utils.py +0 -21
- codex_autorunner/core/engine.py +0 -3302
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/top_level.txt +0 -0
|
@@ -14,13 +14,14 @@ import difflib
|
|
|
14
14
|
import logging
|
|
15
15
|
from dataclasses import dataclass
|
|
16
16
|
from pathlib import Path
|
|
17
|
-
from typing import Any, AsyncIterator, Dict, Optional
|
|
17
|
+
from typing import Any, AsyncIterator, Callable, Dict, Optional
|
|
18
18
|
|
|
19
19
|
from fastapi import APIRouter, HTTPException, Request
|
|
20
20
|
from fastapi.responses import StreamingResponse
|
|
21
21
|
|
|
22
22
|
from ....agents.registry import validate_agent_id
|
|
23
23
|
from ....core import drafts as draft_utils
|
|
24
|
+
from ....core.context_awareness import CAR_AWARENESS_BLOCK, format_file_role_addendum
|
|
24
25
|
from ....core.state import now_iso
|
|
25
26
|
from ....core.utils import atomic_write, find_repo_root
|
|
26
27
|
from ....integrations.app_server.event_buffer import format_sse
|
|
@@ -140,7 +141,49 @@ def _parse_target(repo_root: Path, raw: str) -> _Target:
|
|
|
140
141
|
state_key=f"workspace_{rel_suffix.replace('/', '_')}",
|
|
141
142
|
)
|
|
142
143
|
|
|
143
|
-
raise HTTPException(status_code=400, detail="invalid target")
|
|
144
|
+
raise HTTPException(status_code=400, detail=f"invalid target: {target}")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _build_file_chat_prompt(*, target: _Target, message: str, before: str) -> str:
|
|
148
|
+
if target.kind == "ticket":
|
|
149
|
+
file_role_context = (
|
|
150
|
+
f"{format_file_role_addendum('ticket', target.rel_path)}\n"
|
|
151
|
+
"Edits here change what the ticket flow agent will do; keep YAML "
|
|
152
|
+
"frontmatter valid."
|
|
153
|
+
)
|
|
154
|
+
elif target.kind == "workspace":
|
|
155
|
+
file_role_context = (
|
|
156
|
+
f"{format_file_role_addendum('workspace', target.rel_path)}\n"
|
|
157
|
+
"These docs act as shared memory across ticket turns."
|
|
158
|
+
)
|
|
159
|
+
else:
|
|
160
|
+
file_role_context = format_file_role_addendum("other", target.rel_path)
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
f"{CAR_AWARENESS_BLOCK}\n\n"
|
|
164
|
+
"<file_role_context>\n"
|
|
165
|
+
f"{file_role_context}\n"
|
|
166
|
+
"</file_role_context>\n\n"
|
|
167
|
+
"You are editing a single file in Codex Autorunner.\n\n"
|
|
168
|
+
"<target>\n"
|
|
169
|
+
f"{target.target}\n"
|
|
170
|
+
"</target>\n\n"
|
|
171
|
+
"<path>\n"
|
|
172
|
+
f"{target.rel_path}\n"
|
|
173
|
+
"</path>\n\n"
|
|
174
|
+
"<instructions>\n"
|
|
175
|
+
"- This is a single-turn edit request. Don’t ask the user questions.\n"
|
|
176
|
+
"- You may read other files for context, but only modify the target file.\n"
|
|
177
|
+
"- If no changes are needed, explain why without editing the file.\n"
|
|
178
|
+
"- Respond with a short summary of what you did.\n"
|
|
179
|
+
"</instructions>\n\n"
|
|
180
|
+
"<user_request>\n"
|
|
181
|
+
f"{message}\n"
|
|
182
|
+
"</user_request>\n\n"
|
|
183
|
+
"<FILE_CONTENT>\n"
|
|
184
|
+
f"{before[:12000]}\n"
|
|
185
|
+
"</FILE_CONTENT>\n"
|
|
186
|
+
)
|
|
144
187
|
|
|
145
188
|
|
|
146
189
|
def _read_file(path: Path) -> str:
|
|
@@ -164,6 +207,10 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
164
207
|
router = APIRouter(prefix="/api", tags=["file-chat"])
|
|
165
208
|
_active_chats: Dict[str, asyncio.Event] = {}
|
|
166
209
|
_chat_lock = asyncio.Lock()
|
|
210
|
+
_turn_lock = asyncio.Lock()
|
|
211
|
+
_current_by_target: Dict[str, Dict[str, Any]] = {}
|
|
212
|
+
_current_by_client: Dict[str, Dict[str, Any]] = {}
|
|
213
|
+
_last_by_client: Dict[str, Dict[str, Any]] = {}
|
|
167
214
|
|
|
168
215
|
async def _get_or_create_interrupt_event(key: str) -> asyncio.Event:
|
|
169
216
|
async with _chat_lock:
|
|
@@ -175,6 +222,61 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
175
222
|
async with _chat_lock:
|
|
176
223
|
_active_chats.pop(key, None)
|
|
177
224
|
|
|
225
|
+
async def _begin_turn_state(target: _Target, client_turn_id: Optional[str]) -> None:
|
|
226
|
+
async with _turn_lock:
|
|
227
|
+
state: Dict[str, Any] = {
|
|
228
|
+
"client_turn_id": client_turn_id or "",
|
|
229
|
+
"target": target.target,
|
|
230
|
+
"status": "starting",
|
|
231
|
+
"agent": None,
|
|
232
|
+
"thread_id": None,
|
|
233
|
+
"turn_id": None,
|
|
234
|
+
}
|
|
235
|
+
_current_by_target[target.state_key] = state
|
|
236
|
+
if client_turn_id:
|
|
237
|
+
_current_by_client[client_turn_id] = state
|
|
238
|
+
|
|
239
|
+
async def _update_turn_state(target: _Target, **updates: Any) -> None:
|
|
240
|
+
async with _turn_lock:
|
|
241
|
+
state = _current_by_target.get(target.state_key)
|
|
242
|
+
if not state:
|
|
243
|
+
return
|
|
244
|
+
for key, value in updates.items():
|
|
245
|
+
if value is None:
|
|
246
|
+
continue
|
|
247
|
+
state[key] = value
|
|
248
|
+
cid = state.get("client_turn_id") or ""
|
|
249
|
+
if cid:
|
|
250
|
+
_current_by_client[cid] = state
|
|
251
|
+
|
|
252
|
+
async def _finalize_turn_state(target: _Target, result: Dict[str, Any]) -> None:
|
|
253
|
+
async with _turn_lock:
|
|
254
|
+
state = _current_by_target.pop(target.state_key, None)
|
|
255
|
+
cid = ""
|
|
256
|
+
if state:
|
|
257
|
+
cid = state.get("client_turn_id", "") or ""
|
|
258
|
+
if cid:
|
|
259
|
+
_current_by_client.pop(cid, None)
|
|
260
|
+
_last_by_client[cid] = dict(result or {})
|
|
261
|
+
|
|
262
|
+
async def _active_for_client(client_turn_id: Optional[str]) -> Dict[str, Any]:
|
|
263
|
+
if not client_turn_id:
|
|
264
|
+
return {}
|
|
265
|
+
async with _turn_lock:
|
|
266
|
+
return dict(_current_by_client.get(client_turn_id, {}))
|
|
267
|
+
|
|
268
|
+
async def _last_for_client(client_turn_id: Optional[str]) -> Dict[str, Any]:
|
|
269
|
+
if not client_turn_id:
|
|
270
|
+
return {}
|
|
271
|
+
async with _turn_lock:
|
|
272
|
+
return dict(_last_by_client.get(client_turn_id, {}))
|
|
273
|
+
|
|
274
|
+
@router.get("/file-chat/active")
|
|
275
|
+
async def file_chat_active(client_turn_id: Optional[str] = None) -> Dict[str, Any]:
|
|
276
|
+
current = await _active_for_client(client_turn_id)
|
|
277
|
+
last = await _last_for_client(client_turn_id)
|
|
278
|
+
return {"active": bool(current), "current": current, "last_result": last}
|
|
279
|
+
|
|
178
280
|
@router.post("/file-chat")
|
|
179
281
|
async def chat_file(request: Request):
|
|
180
282
|
"""Chat with a file target - optionally streams SSE events."""
|
|
@@ -185,6 +287,7 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
185
287
|
agent = body.get("agent", "codex")
|
|
186
288
|
model = body.get("model")
|
|
187
289
|
reasoning = body.get("reasoning")
|
|
290
|
+
client_turn_id = (body.get("client_turn_id") or "").strip() or None
|
|
188
291
|
|
|
189
292
|
if not message:
|
|
190
293
|
raise HTTPException(status_code=400, detail="message is required")
|
|
@@ -203,6 +306,8 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
203
306
|
raise HTTPException(status_code=409, detail="File chat already running")
|
|
204
307
|
_active_chats[target.state_key] = asyncio.Event()
|
|
205
308
|
|
|
309
|
+
await _begin_turn_state(target, client_turn_id)
|
|
310
|
+
|
|
206
311
|
if stream:
|
|
207
312
|
return StreamingResponse(
|
|
208
313
|
_stream_file_chat(
|
|
@@ -213,21 +318,47 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
213
318
|
agent=agent,
|
|
214
319
|
model=model,
|
|
215
320
|
reasoning=reasoning,
|
|
321
|
+
client_turn_id=client_turn_id,
|
|
216
322
|
),
|
|
217
323
|
media_type="text/event-stream",
|
|
218
324
|
headers=SSE_HEADERS,
|
|
219
325
|
)
|
|
220
326
|
|
|
221
327
|
try:
|
|
222
|
-
result
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
328
|
+
result: Dict[str, Any]
|
|
329
|
+
|
|
330
|
+
async def _on_meta(agent_id: str, thread_id: str, turn_id: str) -> None:
|
|
331
|
+
await _update_turn_state(
|
|
332
|
+
target,
|
|
333
|
+
agent=agent_id,
|
|
334
|
+
thread_id=thread_id,
|
|
335
|
+
turn_id=turn_id,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
result = await _execute_file_chat(
|
|
340
|
+
request,
|
|
341
|
+
repo_root,
|
|
342
|
+
target,
|
|
343
|
+
message,
|
|
344
|
+
agent=agent,
|
|
345
|
+
model=model,
|
|
346
|
+
reasoning=reasoning,
|
|
347
|
+
on_meta=_on_meta,
|
|
348
|
+
)
|
|
349
|
+
except Exception as exc:
|
|
350
|
+
await _finalize_turn_state(
|
|
351
|
+
target,
|
|
352
|
+
{
|
|
353
|
+
"status": "error",
|
|
354
|
+
"detail": str(exc),
|
|
355
|
+
"client_turn_id": client_turn_id or "",
|
|
356
|
+
},
|
|
357
|
+
)
|
|
358
|
+
raise
|
|
359
|
+
result = dict(result or {})
|
|
360
|
+
result["client_turn_id"] = client_turn_id or ""
|
|
361
|
+
await _finalize_turn_state(target, result)
|
|
231
362
|
return result
|
|
232
363
|
finally:
|
|
233
364
|
await _clear_interrupt_event(target.state_key)
|
|
@@ -241,22 +372,62 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
241
372
|
agent: str = "codex",
|
|
242
373
|
model: Optional[str] = None,
|
|
243
374
|
reasoning: Optional[str] = None,
|
|
375
|
+
client_turn_id: Optional[str] = None,
|
|
244
376
|
) -> AsyncIterator[str]:
|
|
245
377
|
yield format_sse("status", {"status": "queued"})
|
|
246
378
|
try:
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
379
|
+
|
|
380
|
+
async def _on_meta(agent_id: str, thread_id: str, turn_id: str) -> None:
|
|
381
|
+
await _update_turn_state(
|
|
382
|
+
target,
|
|
383
|
+
agent=agent_id,
|
|
384
|
+
thread_id=thread_id,
|
|
385
|
+
turn_id=turn_id,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
run_task = asyncio.create_task(
|
|
389
|
+
_execute_file_chat(
|
|
390
|
+
request,
|
|
391
|
+
repo_root,
|
|
392
|
+
target,
|
|
393
|
+
message,
|
|
394
|
+
agent=agent,
|
|
395
|
+
model=model,
|
|
396
|
+
reasoning=reasoning,
|
|
397
|
+
on_meta=_on_meta,
|
|
398
|
+
)
|
|
255
399
|
)
|
|
400
|
+
|
|
401
|
+
async def _finalize() -> None:
|
|
402
|
+
result = {"status": "error", "detail": "File chat failed"}
|
|
403
|
+
try:
|
|
404
|
+
result = await run_task
|
|
405
|
+
except Exception as exc:
|
|
406
|
+
logger.exception("file chat task failed")
|
|
407
|
+
result = {
|
|
408
|
+
"status": "error",
|
|
409
|
+
"detail": str(exc) or "File chat failed",
|
|
410
|
+
}
|
|
411
|
+
result = dict(result or {})
|
|
412
|
+
result["client_turn_id"] = client_turn_id or ""
|
|
413
|
+
await _finalize_turn_state(target, result)
|
|
414
|
+
|
|
415
|
+
asyncio.create_task(_finalize())
|
|
416
|
+
|
|
417
|
+
try:
|
|
418
|
+
result = await asyncio.shield(run_task)
|
|
419
|
+
except asyncio.CancelledError:
|
|
420
|
+
# client disconnected; turn continues in background
|
|
421
|
+
return
|
|
422
|
+
|
|
256
423
|
if result.get("status") == "ok":
|
|
257
424
|
raw_events = result.pop("raw_events", []) or []
|
|
258
425
|
for event in raw_events:
|
|
259
426
|
yield format_sse("app-server", event)
|
|
427
|
+
usage_parts = result.pop("usage_parts", []) or []
|
|
428
|
+
for usage in usage_parts:
|
|
429
|
+
yield format_sse("token_usage", usage)
|
|
430
|
+
result["client_turn_id"] = client_turn_id or ""
|
|
260
431
|
yield format_sse("update", result)
|
|
261
432
|
yield format_sse("done", {"status": "ok"})
|
|
262
433
|
elif result.get("status") == "interrupted":
|
|
@@ -283,11 +454,14 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
283
454
|
agent: str = "codex",
|
|
284
455
|
model: Optional[str] = None,
|
|
285
456
|
reasoning: Optional[str] = None,
|
|
457
|
+
on_meta: Optional[Callable[[str, str, str], Any]] = None,
|
|
458
|
+
on_usage: Optional[Callable[[Dict[str, Any]], Any]] = None,
|
|
286
459
|
) -> Dict[str, Any]:
|
|
287
460
|
supervisor = getattr(request.app.state, "app_server_supervisor", None)
|
|
288
461
|
threads = getattr(request.app.state, "app_server_threads", None)
|
|
289
462
|
opencode = getattr(request.app.state, "opencode_supervisor", None)
|
|
290
463
|
engine = getattr(request.app.state, "engine", None)
|
|
464
|
+
events = getattr(request.app.state, "app_server_events", None)
|
|
291
465
|
stall_timeout_seconds = None
|
|
292
466
|
try:
|
|
293
467
|
stall_timeout_seconds = (
|
|
@@ -303,21 +477,7 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
303
477
|
before = _read_file(target.path)
|
|
304
478
|
base_hash = _hash_content(before)
|
|
305
479
|
|
|
306
|
-
prompt = (
|
|
307
|
-
"You are editing a single file in Codex AutoRunner.\n\n"
|
|
308
|
-
f"Target: {target.target}\n"
|
|
309
|
-
f"Path: {target.rel_path}\n\n"
|
|
310
|
-
"Instructions:\n"
|
|
311
|
-
"- This run is non-interactive. Do not ask the user questions.\n"
|
|
312
|
-
"- Edit ONLY the target file.\n"
|
|
313
|
-
"- If no changes are needed, explain why without editing the file.\n"
|
|
314
|
-
"- Respond with a short summary of what you did.\n\n"
|
|
315
|
-
"User request:\n"
|
|
316
|
-
f"{message}\n\n"
|
|
317
|
-
"<FILE_CONTENT>\n"
|
|
318
|
-
f"{before[:12000]}\n"
|
|
319
|
-
"</FILE_CONTENT>\n"
|
|
320
|
-
)
|
|
480
|
+
prompt = _build_file_chat_prompt(target=target, message=message, before=before)
|
|
321
481
|
|
|
322
482
|
interrupt_event = await _get_or_create_interrupt_event(target.state_key)
|
|
323
483
|
if interrupt_event.is_set():
|
|
@@ -329,6 +489,7 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
329
489
|
agent_id = "codex"
|
|
330
490
|
|
|
331
491
|
thread_key = f"file_chat.{target.state_key}"
|
|
492
|
+
await _update_turn_state(target, status="running", agent=agent_id)
|
|
332
493
|
|
|
333
494
|
if agent_id == "opencode":
|
|
334
495
|
if opencode is None:
|
|
@@ -343,6 +504,8 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
343
504
|
thread_registry=threads,
|
|
344
505
|
thread_key=thread_key,
|
|
345
506
|
stall_timeout_seconds=stall_timeout_seconds,
|
|
507
|
+
on_meta=on_meta,
|
|
508
|
+
on_usage=on_usage,
|
|
346
509
|
)
|
|
347
510
|
else:
|
|
348
511
|
if supervisor is None:
|
|
@@ -355,10 +518,13 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
355
518
|
repo_root,
|
|
356
519
|
prompt,
|
|
357
520
|
interrupt_event,
|
|
521
|
+
agent_id=agent_id,
|
|
358
522
|
model=model,
|
|
359
523
|
reasoning=reasoning,
|
|
360
524
|
thread_registry=threads,
|
|
361
525
|
thread_key=thread_key,
|
|
526
|
+
on_meta=on_meta,
|
|
527
|
+
events=events,
|
|
362
528
|
)
|
|
363
529
|
|
|
364
530
|
if result.get("status") != "ok":
|
|
@@ -393,6 +559,7 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
393
559
|
return {
|
|
394
560
|
"status": "ok",
|
|
395
561
|
"target": target.target,
|
|
562
|
+
"agent": agent_id,
|
|
396
563
|
"agent_message": agent_message,
|
|
397
564
|
"message": response_text,
|
|
398
565
|
"has_draft": True,
|
|
@@ -400,6 +567,8 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
400
567
|
"content": after,
|
|
401
568
|
"base_hash": base_hash,
|
|
402
569
|
"created_at": drafts[target.state_key]["created_at"],
|
|
570
|
+
"thread_id": result.get("thread_id"),
|
|
571
|
+
"turn_id": result.get("turn_id"),
|
|
403
572
|
**(
|
|
404
573
|
{"raw_events": result.get("raw_events")}
|
|
405
574
|
if result.get("raw_events")
|
|
@@ -410,9 +579,12 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
410
579
|
return {
|
|
411
580
|
"status": "ok",
|
|
412
581
|
"target": target.target,
|
|
582
|
+
"agent": agent_id,
|
|
413
583
|
"agent_message": agent_message,
|
|
414
584
|
"message": response_text,
|
|
415
585
|
"has_draft": False,
|
|
586
|
+
"thread_id": result.get("thread_id"),
|
|
587
|
+
"turn_id": result.get("turn_id"),
|
|
416
588
|
**(
|
|
417
589
|
{"raw_events": result.get("raw_events")}
|
|
418
590
|
if result.get("raw_events")
|
|
@@ -428,8 +600,11 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
428
600
|
*,
|
|
429
601
|
model: Optional[str] = None,
|
|
430
602
|
reasoning: Optional[str] = None,
|
|
603
|
+
agent_id: str = "codex",
|
|
431
604
|
thread_registry: Optional[Any] = None,
|
|
432
605
|
thread_key: Optional[str] = None,
|
|
606
|
+
on_meta: Optional[Callable[[str, str, str], Any]] = None,
|
|
607
|
+
events: Optional[Any] = None,
|
|
433
608
|
) -> Dict[str, Any]:
|
|
434
609
|
client = await supervisor.get_client(repo_root)
|
|
435
610
|
|
|
@@ -463,6 +638,18 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
463
638
|
sandbox_policy="dangerFullAccess",
|
|
464
639
|
**turn_kwargs,
|
|
465
640
|
)
|
|
641
|
+
if events is not None:
|
|
642
|
+
try:
|
|
643
|
+
await events.register_turn(thread_id, handle.turn_id)
|
|
644
|
+
except Exception:
|
|
645
|
+
logger.debug("file chat register_turn failed", exc_info=True)
|
|
646
|
+
if on_meta is not None:
|
|
647
|
+
try:
|
|
648
|
+
maybe = on_meta(agent_id, thread_id, handle.turn_id)
|
|
649
|
+
if asyncio.iscoroutine(maybe):
|
|
650
|
+
await maybe
|
|
651
|
+
except Exception:
|
|
652
|
+
logger.debug("file chat meta callback failed", exc_info=True)
|
|
466
653
|
|
|
467
654
|
turn_task = asyncio.create_task(handle.wait(timeout=None))
|
|
468
655
|
timeout_task = asyncio.create_task(asyncio.sleep(FILE_CHAT_TIMEOUT_SECONDS))
|
|
@@ -495,6 +682,9 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
495
682
|
"agent_message": agent_message,
|
|
496
683
|
"message": output,
|
|
497
684
|
"raw_events": raw_events,
|
|
685
|
+
"thread_id": thread_id,
|
|
686
|
+
"turn_id": getattr(handle, "turn_id", None),
|
|
687
|
+
"agent": agent_id,
|
|
498
688
|
}
|
|
499
689
|
|
|
500
690
|
async def _execute_opencode(
|
|
@@ -508,9 +698,12 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
508
698
|
thread_registry: Optional[Any] = None,
|
|
509
699
|
thread_key: Optional[str] = None,
|
|
510
700
|
stall_timeout_seconds: Optional[float] = None,
|
|
701
|
+
on_meta: Optional[Callable[[str, str, str], Any]] = None,
|
|
702
|
+
on_usage: Optional[Callable[[Dict[str, Any]], Any]] = None,
|
|
511
703
|
) -> Dict[str, Any]:
|
|
512
704
|
from ....agents.opencode.runtime import (
|
|
513
705
|
PERMISSION_ALLOW,
|
|
706
|
+
build_turn_id,
|
|
514
707
|
collect_opencode_output,
|
|
515
708
|
extract_session_id,
|
|
516
709
|
parse_message_response,
|
|
@@ -529,9 +722,32 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
529
722
|
if thread_registry is not None and thread_key:
|
|
530
723
|
thread_registry.set_thread_id(thread_key, session_id)
|
|
531
724
|
|
|
725
|
+
turn_id = build_turn_id(session_id)
|
|
726
|
+
if on_meta is not None:
|
|
727
|
+
try:
|
|
728
|
+
maybe = on_meta("opencode", session_id, turn_id)
|
|
729
|
+
if asyncio.iscoroutine(maybe):
|
|
730
|
+
await maybe
|
|
731
|
+
except Exception:
|
|
732
|
+
logger.debug("file chat opencode meta failed", exc_info=True)
|
|
733
|
+
|
|
532
734
|
model_payload = split_model_id(model)
|
|
533
735
|
await supervisor.mark_turn_started(repo_root)
|
|
534
736
|
|
|
737
|
+
usage_parts: list[Dict[str, Any]] = []
|
|
738
|
+
|
|
739
|
+
async def _part_handler(
|
|
740
|
+
part_type: str, part: Any, turn_id_arg: Optional[str] | None
|
|
741
|
+
) -> None:
|
|
742
|
+
if part_type == "usage" and on_usage is not None:
|
|
743
|
+
usage_parts.append(part)
|
|
744
|
+
try:
|
|
745
|
+
maybe = on_usage(part)
|
|
746
|
+
if asyncio.iscoroutine(maybe):
|
|
747
|
+
await maybe
|
|
748
|
+
except Exception:
|
|
749
|
+
logger.debug("file chat usage handler failed", exc_info=True)
|
|
750
|
+
|
|
535
751
|
ready_event = asyncio.Event()
|
|
536
752
|
output_task = asyncio.create_task(
|
|
537
753
|
collect_opencode_output(
|
|
@@ -543,6 +759,7 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
543
759
|
question_policy="auto_first_option",
|
|
544
760
|
should_stop=interrupt_event.is_set,
|
|
545
761
|
ready_event=ready_event,
|
|
762
|
+
part_handler=_part_handler,
|
|
546
763
|
stall_timeout_seconds=stall_timeout_seconds,
|
|
547
764
|
)
|
|
548
765
|
)
|
|
@@ -593,11 +810,17 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
593
810
|
if output_result.error:
|
|
594
811
|
raise FileChatError(output_result.error)
|
|
595
812
|
agent_message = _parse_agent_message(output_result.text)
|
|
596
|
-
|
|
813
|
+
result = {
|
|
597
814
|
"status": "ok",
|
|
598
815
|
"agent_message": agent_message,
|
|
599
816
|
"message": output_result.text,
|
|
817
|
+
"thread_id": session_id,
|
|
818
|
+
"turn_id": turn_id,
|
|
819
|
+
"agent": "opencode",
|
|
600
820
|
}
|
|
821
|
+
if usage_parts:
|
|
822
|
+
result["usage_parts"] = usage_parts
|
|
823
|
+
return result
|
|
601
824
|
|
|
602
825
|
def _parse_agent_message(output: str) -> str:
|
|
603
826
|
text = (output or "").strip()
|
|
@@ -693,6 +916,37 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
693
916
|
"content": _read_file(resolved.path),
|
|
694
917
|
}
|
|
695
918
|
|
|
919
|
+
@router.get("/file-chat/turns/{turn_id}/events")
|
|
920
|
+
async def stream_file_chat_turn_events(
|
|
921
|
+
turn_id: str, request: Request, thread_id: str, agent: str = "codex"
|
|
922
|
+
):
|
|
923
|
+
agent_id = (agent or "").strip().lower()
|
|
924
|
+
if agent_id == "codex":
|
|
925
|
+
events = getattr(request.app.state, "app_server_events", None)
|
|
926
|
+
if events is None:
|
|
927
|
+
raise HTTPException(status_code=404, detail="Events unavailable")
|
|
928
|
+
if not thread_id:
|
|
929
|
+
raise HTTPException(status_code=400, detail="thread_id is required")
|
|
930
|
+
return StreamingResponse(
|
|
931
|
+
events.stream(thread_id, turn_id),
|
|
932
|
+
media_type="text/event-stream",
|
|
933
|
+
headers=SSE_HEADERS,
|
|
934
|
+
)
|
|
935
|
+
if agent_id == "opencode":
|
|
936
|
+
supervisor = getattr(request.app.state, "opencode_supervisor", None)
|
|
937
|
+
if supervisor is None:
|
|
938
|
+
raise HTTPException(status_code=404, detail="OpenCode unavailable")
|
|
939
|
+
from ....agents.opencode.harness import OpenCodeHarness
|
|
940
|
+
|
|
941
|
+
harness = OpenCodeHarness(supervisor)
|
|
942
|
+
repo_root = _resolve_repo_root(request)
|
|
943
|
+
return StreamingResponse(
|
|
944
|
+
harness.stream_events(repo_root, thread_id, turn_id),
|
|
945
|
+
media_type="text/event-stream",
|
|
946
|
+
headers=SSE_HEADERS,
|
|
947
|
+
)
|
|
948
|
+
raise HTTPException(status_code=404, detail="Unknown agent")
|
|
949
|
+
|
|
696
950
|
@router.post("/file-chat/interrupt")
|
|
697
951
|
async def interrupt_file_chat(request: Request):
|
|
698
952
|
body = await request.json()
|
|
@@ -715,6 +969,7 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
715
969
|
agent = body.get("agent", "codex")
|
|
716
970
|
model = body.get("model")
|
|
717
971
|
reasoning = body.get("reasoning")
|
|
972
|
+
client_turn_id = (body.get("client_turn_id") or "").strip() or None
|
|
718
973
|
|
|
719
974
|
if not message:
|
|
720
975
|
raise HTTPException(status_code=400, detail="message is required")
|
|
@@ -729,6 +984,7 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
729
984
|
status_code=409, detail="Ticket chat already running"
|
|
730
985
|
)
|
|
731
986
|
_active_chats[target.state_key] = asyncio.Event()
|
|
987
|
+
await _begin_turn_state(target, client_turn_id)
|
|
732
988
|
|
|
733
989
|
if stream:
|
|
734
990
|
return StreamingResponse(
|
|
@@ -740,13 +996,14 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
740
996
|
agent=agent,
|
|
741
997
|
model=model,
|
|
742
998
|
reasoning=reasoning,
|
|
999
|
+
client_turn_id=client_turn_id,
|
|
743
1000
|
),
|
|
744
1001
|
media_type="text/event-stream",
|
|
745
1002
|
headers=SSE_HEADERS,
|
|
746
1003
|
)
|
|
747
1004
|
|
|
748
1005
|
try:
|
|
749
|
-
|
|
1006
|
+
result = await _execute_file_chat(
|
|
750
1007
|
request,
|
|
751
1008
|
repo_root,
|
|
752
1009
|
target,
|
|
@@ -755,6 +1012,10 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
755
1012
|
model=model,
|
|
756
1013
|
reasoning=reasoning,
|
|
757
1014
|
)
|
|
1015
|
+
result = dict(result or {})
|
|
1016
|
+
result["client_turn_id"] = client_turn_id or ""
|
|
1017
|
+
await _finalize_turn_state(target, result)
|
|
1018
|
+
return result
|
|
758
1019
|
finally:
|
|
759
1020
|
await _clear_interrupt_event(target.state_key)
|
|
760
1021
|
|