From da8b7f82a02e2a57aaf349235e79c853bde5eec7 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 28 Apr 2026 17:36:18 +0100 Subject: [PATCH] Finish Epiphany live specialist MVP --- notes/epiphany-fork-implementation-plan.md | 16 +- notes/fresh-workspace-handoff.md | 20 +- state/evidence.jsonl | 1 + state/map.yaml | 8 +- tools/epiphany_mvp_live_specialist.py | 213 ++++++++++++++++++ .../core/src/tools/handlers/agent_jobs.rs | 22 +- .../codex/codex-rs/tools/src/tool_config.rs | 11 +- .../codex-rs/tools/src/tool_config_tests.rs | 23 ++ .../codex-rs/tools/src/tool_registry_plan.rs | 17 +- .../tools/src/tool_registry_plan_tests.rs | 30 +++ 10 files changed, 329 insertions(+), 32 deletions(-) create mode 100644 tools/epiphany_mvp_live_specialist.py diff --git a/notes/epiphany-fork-implementation-plan.md b/notes/epiphany-fork-implementation-plan.md index de616b0e..123ac349 100644 --- a/notes/epiphany-fork-implementation-plan.md +++ b/notes/epiphany-fork-implementation-plan.md @@ -79,6 +79,7 @@ The landed machine now has: - explicit Phase 6 role launch/read-back through `thread/epiphany/roleLaunch` and read-only `thread/epiphany/roleResult`, limited to fixed modeling/checkpoint and verification/review templates over the existing job-control seam - first Phase 6 dogfood operator view through `tools/epiphany_mvp_status.py` - first auditable Phase 6 dogfood runner through `tools/epiphany_mvp_dogfood.py`, producing local status snapshots, raw app-server transcript, final status artifacts, vanilla-reference output, and comparison notes +- first auditable Phase 6 live-specialist runner through `tools/epiphany_mvp_live_specialist.py`, proving `roleLaunch -> agent_jobs worker -> report_agent_job_result -> roleResult` without manual backend completion - live Phase 6 reorientation app-server smoke coverage in `tools/epiphany_phase6_reorient_smoke.py` - live Phase 6 reorient-launch app-server smoke coverage in `tools/epiphany_phase6_reorient_launch_smoke.py` - live Phase 6 MVP status smoke coverage in `tools/epiphany_mvp_status_smoke.py` @@ -148,8 +149,8 @@ The next unknowns are: - how the landed watcher-backed invalidation telemetry should be consumed without turning freshness into a secret worker - how far the read-only CRRC recommendation should go before explicit client/operator action takes over -- whether future bounded CRRC consumers should keep reusing the landed `agent_jobs` backend through the explicit job-control seam or start defining a second backend contract -- what Phase 6 should prove before specialist scheduling begins +- how much specialist ergonomics can improve before crossing into broad ambient scheduling +- what concrete operator friction appears when the CLI MVP is tested on real work ## MVP Cutline @@ -191,9 +192,12 @@ launch modeling/checkpoint or verification/review workers, and read their findings back without reading Rust. The first auditable dogfood pass then added `tools/epiphany_mvp_dogfood.py`, rendered modeling/verification findings in the status view, and fixed CRRC's repeat-acceptance recommendation after -`reorientAccept`. The next MVP question is human operator testing, not another -imagined organ: use the landed loop and artifact bundle, then fix only concrete -blockers. +`reorientAccept`. The live-specialist runner then proved the real worker path by +launching a modeling/checkpoint specialist, letting it inspect the smoke +workspace, report through `report_agent_job_result`, and return a completed +`checkpoint-ready` finding through `roleResult`. The next MVP question is human +operator testing, not another imagined organ: use the landed loop and artifact +bundle, then fix only concrete blockers. ## Phase 6 Direction @@ -201,7 +205,7 @@ Phase 6 should grow observable harness state outward from the typed spine. Useful candidates: -1. Put the dogfooded MVP loop in front of a human operator through status/artifact review, then fix concrete usability blockers. +1. Put the CLI MVP in front of a human operator through status/artifact review, then fix concrete usability blockers. 2. Keep accepted worker findings review-gated; do not convert acceptance into automatic promotion of arbitrary worker output. 3. Add targeted operator-view fields only when dogfooding exposes a real gap. diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 57a08a0a..e7b6e75f 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -31,6 +31,8 @@ Do not trust this file for the exact live HEAD. Always check git. - Phase 6 has read-only `thread/epiphany/scene`, `thread/epiphany/jobs`, `thread/epiphany/roles`, `thread/epiphany/freshness`, `thread/epiphany/context`, `thread/epiphany/pressure`, `thread/epiphany/reorient`, `thread/epiphany/crrc`, `thread/epiphany/reorientResult`, and `thread/epiphany/roleResult`; durable `jobBindings` now act as a thin Epiphany-owned launcher seam with launcher id, authority scope, and backend kind/job id, `thread/epiphany/jobLaunch` / `thread/epiphany/jobInterrupt` / `thread/epiphany/roleLaunch` now provide explicit bounded authority over that seam, jobs can overlay it onto live runtime `agent_jobs` progress for loaded threads, and `thread/epiphany/jobsUpdated` now translates live `agent_job_progress:{json}` background events into changed launcher-bound notifications without polling or scheduling. Freshness carries watcher-backed invalidation inputs, roles project implementation/modeling/verification/reorientation ownership from existing signals without becoming a scheduler, `roleLaunch` can launch only fixed modeling/checkpoint or verification/review specialists, `roleResult` reads those outputs back as reviewable findings without mutation, reorient turns checkpoint plus freshness/pressure/watcher signals into a read-only resume-versus-regather verdict, `thread/epiphany/reorientLaunch` is the explicit runtime consumer over that verdict, `thread/epiphany/reorientResult` reads the launched worker output back as a reviewable finding without mutation or promotion, `thread/epiphany/reorientAccept` explicitly banks completed findings into accepted observation/evidence plus optional scratch/checkpoint state, and `thread/epiphany/crrc` recommends the next explicit CRRC action without launching, accepting, compacting, scheduling, or mutating. - `tools/epiphany_mvp_status.py` is the first dogfood operator view. It starts or reads a thread through app-server and prints scene, pressure, reorient, jobs, roles, modeling/verification role result read-backs, reorient result, and CRRC recommendation as text or JSON. - `tools/epiphany_mvp_dogfood.py` is the first auditable dogfood runner. It drives a bounded state/role/CRRC/reorientation loop and writes local artifacts under `.epiphany-dogfood/mvp-loop`, including raw app-server transcript, rendered snapshots, final status, vanilla-reference prompt/output, and comparison notes. +- `tools/epiphany_mvp_live_specialist.py` is the first auditable live-specialist runner. It launches a real role specialist through `thread/epiphany/roleLaunch`, lets the spawned worker report through `report_agent_job_result`, reads it back through `thread/epiphany/roleResult`, and writes local artifacts under `.epiphany-dogfood/live-specialist`. +- Internal `agent_job:` workers now get the reporting tool independent of the user-facing CSV spawn feature, and ephemeral worker sessions can initialize the sqlite state runtime on demand so specialist reports land in the shared backend. - Durable in-flight investigation checkpointing is now landed in authoritative typed state, writable through `thread/epiphany/update` or accepted `thread/epiphany/promote`, rendered into the prompt, and reflected through scene/context. - The repo is an Epiphany fork of Codex, not a Codex preset. - `vendor/codex` is tracked directly, not a submodule. @@ -93,6 +95,7 @@ The current spine: - live reorient-guided worker launch smoke through `tools/epiphany_phase6_reorient_launch_smoke.py` - live MVP operator status smoke through `tools/epiphany_mvp_status_smoke.py` - live job-control app-server smoke through `tools/epiphany_phase6_job_control_smoke.py` +- live specialist MVP pass through `tools/epiphany_mvp_live_specialist.py` The exact current control flow is documented in `notes/epiphany-current-algorithmic-map.md`. @@ -268,10 +271,17 @@ produced `.epiphany-dogfood/mvp-loop` artifacts, ran a read-only Vanilla Codex reference, and fixed the concrete blocker where CRRC recommended accepting the same reorientation result again after `reorientAccept`. -When the user asks to continue, put the testable MVP in front of a human -operator through `tools/epiphany_mvp_status.py` and the dogfood artifact bundle. -Fix concrete blockers in that operator loop; do not build a broad hidden -scheduler, marketplace, or GUI-as-source-of-truth. +The first live-specialist pass has also run. It produced +`.epiphany-dogfood/live-specialist` artifacts and proved the real worker path: +`roleLaunch` created a bound `agent_jobs` row, the spawned modeling worker +inspected the smoke workspace, called `report_agent_job_result`, and +`roleResult` projected a completed `checkpoint-ready` finding. + +The CLI MVP is now testable. Put it in front of a human operator through +`tools/epiphany_mvp_status.py`, `.epiphany-dogfood/mvp-loop`, and +`.epiphany-dogfood/live-specialist`. Fix concrete blockers in that operator +loop; do not build a broad hidden scheduler, marketplace, or +GUI-as-source-of-truth. Live `thread/epiphany/scene`, `thread/epiphany/jobs`, `thread/epiphany/roles`, `thread/epiphany/freshness`, `thread/epiphany/context`, @@ -283,7 +293,7 @@ guardrails, not the next organs. - automatic watcher-driven semantic invalidation - automatic observation promotion -- specialist-agent scheduling +- broad specialist-agent scheduling beyond explicit roleLaunch - GUI-as-source-of-truth - automatic runtime CRRC execution acting on the landed recommendation/verdict and launch/read-back path - Epiphany-owned long-running job execution beyond the current runtime `agent_jobs` seam diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 0169d5a9..6e805e45 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -56,3 +56,4 @@ {"ts": "2026-04-28T13:55:36+00:00", "type": "implementation", "status": "ok", "note": "Landed read-only thread/epiphany/roles as the harness-native MVP role ownership surface. It projects implementation, modeling/checkpoint, verification/review, and reorientation lanes from typed state plus jobs/pressure/reorient/CRRC/result signals without scheduling or mutation; the MVP status tool now consumes that surface. Focused protocol/app-server tests, schema fixtures, app-server build, and live MVP status smoke passed."} {"ts": "2026-04-28T14:48:10+00:00", "type": "implementation", "status": "ok", "note": "Landed explicit thread/epiphany/roleLaunch and read-only thread/epiphany/roleResult for fixed modeling/checkpoint and verification/review specialists over the existing job-control seam. Launch mutates only jobBindings; result projects reviewable findings without mutation. Focused protocol/app-server tests, schema fixtures, app-server build, and tools/epiphany_phase6_role_smoke.py passed."} {"ts": "2026-04-28T15:32:47+00:00", "type": "implementation", "status": "ok", "note": "Ran the first auditable Epiphany MVP dogfood pass through state, modeling/verification roleLaunch/roleResult, CRRC drift, reorientLaunch/reorientResult/reorientAccept, final status rendering, and a read-only Vanilla Codex reference. Dogfood artifacts live under .epiphany-dogfood/mvp-loop. The pass exposed and fixed a concrete operator-loop blocker: after reorientAccept, CRRC no longer recommends accepting the same completed reorientation finding again; it recognizes the accepted evidence and recommends regatherManually for accepted regather findings."} +{"ts": "2026-04-28T16:33:43+00:00", "type": "implementation", "status": "ok", "note": "Landed live-specialist MVP execution: internal agent_job workers now expose report_agent_job_result independently of the CSV front door, ephemeral workers can initialize sqlite state runtime on demand, and tools/epiphany_mvp_live_specialist.py proved roleLaunch -> agent_jobs worker -> report_agent_job_result -> roleResult with auditable artifacts under .epiphany-dogfood/live-specialist."} diff --git a/state/map.yaml b/state/map.yaml index 88eaa0b4..f80bd0d5 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -90,6 +90,8 @@ accepted_design: - `thread/epiphany/reorientAccept` is the first explicit acceptance write for completed reorient-worker findings: it appends accepted observation/evidence records, can optionally update scratch and investigation checkpoint state, and emits `stateUpdated` with source `reorientAccept` without accepting pending work or launching follow-up work. - After `thread/epiphany/reorientAccept`, CRRC recognizes the accepted reorientation finding and does not recommend accepting the same completed result again. - `tools/epiphany_mvp_dogfood.py` is the auditable MVP dogfood runner: it drives state, role launch/result, CRRC drift, reorient launch/result/accept, rendered status snapshots, and raw app-server transcript into a local artifact bundle for comparison against a vanilla Codex reference. + - `tools/epiphany_mvp_live_specialist.py` is the auditable live-specialist MVP runner: it launches a real modeling/checkpoint or verification/review specialist through `thread/epiphany/roleLaunch`, lets the spawned worker report through `report_agent_job_result`, reads the finding back through `thread/epiphany/roleResult`, and writes transcript/status artifacts without manually completing backend rows. + - Internal `agent_job:` workers expose `report_agent_job_result` even when the user-facing CSV spawn feature is disabled, and ephemeral worker sessions can initialize the sqlite state runtime on demand so role specialists can report into the shared `agent_jobs` backend. - Epiphany safety doctrine is to separate cognition from actuation, keep authority explicit, make interruption sacred, stage capability release, and treat operator misuse as a normal design input rather than a rude hypothetical. - Epiphany is built in the open, but publication mode is not one-size-fits-all: use FOSS where viable, source-available licensing where economics or capability risk demand it, and commercial terms where organizations extract real enterprise value. - `thread/epiphany/jobLaunch` and `thread/epiphany/jobInterrupt` are the first bounded authority surfaces over the thin job seam: they mutate durable `job_bindings`, create or cancel the current `agent_jobs` backend binding, and preserve explicit launcher identity and interruption without turning `thread/epiphany/jobs` into a scheduler. @@ -112,6 +114,7 @@ landed_baseline: - Phase 6 acceptance: explicit reorient-worker finding acceptance through `thread/epiphany/reorientAccept`. - Phase 6 dogfood view: first CLI operator status over scene/pressure/reorient/jobs/result/CRRC. - Phase 6 dogfood runner: auditable scripted MVP loop over state, role results, CRRC, reorientation acceptance, status snapshots, and transcript artifacts. + - Phase 6 live specialist runner: auditable real worker loop over roleLaunch, agent_jobs worker execution, report_agent_job_result, roleResult, status rendering, and transcript artifacts. - Phase 6 role ownership: read-only `thread/epiphany/roles` over implementation, modeling/checkpoint, verification/review, and reorientation lanes. - Phase 6 role specialists: explicit `thread/epiphany/roleLaunch` and read-only `thread/epiphany/roleResult` templates for modeling/checkpoint and verification/review. guardrails: @@ -129,6 +132,7 @@ landed_baseline: - Use `tools/epiphany_phase6_reorient_launch_smoke.py` before modifying the read-only CRRC coordinator recommendation surface. - Use `tools/epiphany_mvp_status_smoke.py` before modifying the MVP operator status view. - Use `tools/epiphany_phase6_role_smoke.py` before modifying explicit role launch/read-back behavior. + - Use `tools/epiphany_mvp_live_specialist.py` before claiming live specialist execution still works end to end. - Use focused scene/jobs/context/pressure/reorient/protocol/schema checks for reflection-only changes. - Use `tools/epiphany_prepare_compaction.py` before and after imminent-compaction persistence passes. - Use `CARGO_TARGET_DIR=C:\Users\Meta\.cargo-target-codex` for Codex Rust work on this Windows machine. @@ -147,5 +151,5 @@ rejected_paths: reason: persistent state that accumulates every local truth becomes memory cruft and weakens rehydration current_status: - summary: Phase 1 durable state, Phase 2 prompt integration, Phase 3 typed read, Phase 4 retrieval/indexing, Phase 5 update/distill/propose/promote/read/notification, and Phase 6 read-only scene, jobs, roles, freshness, targeted context, pressure, reorient, CRRC, reorient launch/result/accept, explicit modeling/checkpoint and verification/review role launch/result templates, first dogfood MVP status, and auditable MVP dogfood runner are landed. The dogfood runner exercised state, role launch/result, CRRC drift, reorient launch/result/accept, and rendered status snapshots; it exposed and fixed the repeat-acceptance CRRC recommendation after `reorientAccept`. Implementation remains main-agent work; reorientation remains on the CRRC-specific launch/result path. Persistent state remains distilled; evidence is a belief ledger, not an activity feed. - next_action: Put the testable MVP in front of a human operator using the dogfood artifact bundle and `tools/epiphany_mvp_status.py`; fix only concrete blockers in the operator loop. Automatic broad specialist scheduling, GUI-as-source-of-truth, a specialist marketplace, autonomous specialist quality, and a second job backend remain future work. + summary: Phase 1 durable state, Phase 2 prompt integration, Phase 3 typed read, Phase 4 retrieval/indexing, Phase 5 update/distill/propose/promote/read/notification, and Phase 6 read-only scene, jobs, roles, freshness, targeted context, pressure, reorient, CRRC, reorient launch/result/accept, explicit modeling/checkpoint and verification/review role launch/result templates, first dogfood MVP status, auditable MVP dogfood runner, and live specialist MVP runner are landed. The live specialist runner proved a real modeling specialist can launch, inspect, report through `report_agent_job_result`, and be read back through `roleResult`; the earlier deterministic dogfood runner exercised state, role launch/result, CRRC drift, reorient launch/result/accept, rendered status snapshots, and Vanilla Codex comparison. Implementation remains main-agent work; reorientation remains on the CRRC-specific launch/result path. Persistent state remains distilled; evidence is a belief ledger, not an activity feed. + next_action: Treat the CLI MVP as testable: run `tools/epiphany_mvp_status.py`, inspect `.epiphany-dogfood/mvp-loop` and `.epiphany-dogfood/live-specialist`, and iterate only on concrete operator-loop blockers. Automatic broad specialist scheduling, GUI-as-source-of-truth, a specialist marketplace, autonomous specialist quality, and a second job backend remain future work. diff --git a/tools/epiphany_mvp_live_specialist.py b/tools/epiphany_mvp_live_specialist.py new file mode 100644 index 00000000..e80603c2 --- /dev/null +++ b/tools/epiphany_mvp_live_specialist.py @@ -0,0 +1,213 @@ +from __future__ import annotations + +import argparse +import json +import os +import shutil +import sys +import time +from pathlib import Path +from typing import Any + +from epiphany_mvp_status import DEFAULT_APP_SERVER +from epiphany_mvp_status import collect_status +from epiphany_mvp_status import render_status +from epiphany_phase5_smoke import AppServerClient +from epiphany_phase5_smoke import ROOT +from epiphany_phase5_smoke import require +from epiphany_phase6_reorient_smoke import prepare_workspace +from epiphany_phase6_reorient_smoke import reorient_patch + + +DEFAULT_ARTIFACT_DIR = ROOT / ".epiphany-dogfood" / "live-specialist" +DEFAULT_CODEX_HOME = Path(os.environ.get("CODEX_HOME", Path.home() / ".codex")) +DEFAULT_ROLES = ("modeling",) +TERMINAL_STATUSES = {"completed", "failed", "cancelled"} + + +def reset_artifact_dir(path: Path) -> None: + root = (ROOT / ".epiphany-dogfood").resolve() + resolved = path.resolve() + if resolved == root or root not in resolved.parents: + raise ValueError(f"refusing to delete non-dogfood artifact dir: {path}") + if path.exists(): + shutil.rmtree(path) + path.mkdir(parents=True, exist_ok=True) + + +def write_json(path: Path, value: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + + +def write_text(path: Path, value: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(value, encoding="utf-8") + + +def launch_role( + client: AppServerClient, + *, + thread_id: str, + role_id: str, + expected_revision: int, + max_runtime_seconds: int, +) -> dict[str, Any]: + launch = client.send( + "thread/epiphany/roleLaunch", + { + "threadId": thread_id, + "roleId": role_id, + "expectedRevision": expected_revision, + "maxRuntimeSeconds": max_runtime_seconds, + }, + ) + assert launch is not None + require(launch["roleId"] == role_id, f"{role_id} launch should echo role id") + require( + launch["changedFields"] == ["jobBindings"], + f"{role_id} launch should only mutate job bindings", + ) + return launch + + +def wait_for_role_result( + client: AppServerClient, + *, + thread_id: str, + role_id: str, + timeout_seconds: int, + poll_seconds: float, +) -> tuple[dict[str, Any], list[str]]: + deadline = time.time() + timeout_seconds + seen_statuses: list[str] = [] + result: dict[str, Any] | None = None + while time.time() < deadline: + response = client.send( + "thread/epiphany/roleResult", + {"threadId": thread_id, "roleId": role_id}, + ) + assert response is not None + result = response + status = response.get("status", "") + seen_statuses.append(status) + if status in TERMINAL_STATUSES: + return response, seen_statuses + time.sleep(poll_seconds) + + assert result is not None + return result, seen_statuses + + +def run_live_specialist(args: argparse.Namespace) -> dict[str, Any]: + app_server = args.app_server.resolve() + if not app_server.exists(): + raise FileNotFoundError(f"codex app-server binary not found: {app_server}") + + codex_home = args.codex_home.resolve() + if not codex_home.exists(): + raise FileNotFoundError(f"codex home not found: {codex_home}") + + artifact_dir = args.artifact_dir.resolve() + reset_artifact_dir(artifact_dir) + workspace = artifact_dir / "workspace" + transcript_path = artifact_dir / "epiphany-transcript.jsonl" + stderr_path = artifact_dir / "epiphany-server.stderr.log" + prepare_workspace(workspace) + + role_results: list[dict[str, Any]] = [] + + with AppServerClient(app_server, codex_home, transcript_path, stderr_path) as client: + client.send( + "initialize", + { + "clientInfo": { + "name": "epiphany-mvp-live-specialist", + "title": "Epiphany MVP Live Specialist", + "version": "0.1.0", + }, + "capabilities": {"experimentalApi": True}, + }, + ) + client.send("initialized", expect_response=False) + started = client.send("thread/start", {"cwd": str(workspace), "ephemeral": True}) + assert started is not None + thread_id = started["thread"]["id"] + + update = client.send( + "thread/epiphany/update", + {"threadId": thread_id, "expectedRevision": 0, "patch": reorient_patch()}, + ) + assert update is not None + revision = update["revision"] + + for role_id in args.roles: + launch = launch_role( + client, + thread_id=thread_id, + role_id=role_id, + expected_revision=revision, + max_runtime_seconds=args.max_runtime_seconds, + ) + revision = launch["revision"] + result, seen_statuses = wait_for_role_result( + client, + thread_id=thread_id, + role_id=role_id, + timeout_seconds=args.timeout_seconds, + poll_seconds=args.poll_seconds, + ) + role_results.append( + { + "roleId": role_id, + "launch": launch, + "seenStatuses": seen_statuses, + "result": result, + } + ) + + final_status = collect_status(client, thread_id=thread_id, cwd=workspace, ephemeral=True) + final_rendered = render_status(final_status) + + summary = { + "objective": "Run a real Epiphany role specialist through roleLaunch, agent_jobs, report_agent_job_result, and roleResult.", + "artifactDir": str(artifact_dir), + "codexHome": str(codex_home), + "threadId": thread_id, + "workspace": str(workspace), + "roles": role_results, + "finalStatus": final_status, + "artifactManifest": [ + "live-specialist-summary.json", + "epiphany-final-status.json", + "epiphany-final-status.txt", + "epiphany-transcript.jsonl", + "epiphany-server.stderr.log", + ], + } + + write_json(artifact_dir / "live-specialist-summary.json", summary) + write_json(artifact_dir / "epiphany-final-status.json", final_status) + write_text(artifact_dir / "epiphany-final-status.txt", final_rendered) + return summary + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Run an auditable live Epiphany specialist through the real agent_jobs worker path." + ) + parser.add_argument("--app-server", type=Path, default=DEFAULT_APP_SERVER) + parser.add_argument("--artifact-dir", type=Path, default=DEFAULT_ARTIFACT_DIR) + parser.add_argument("--codex-home", type=Path, default=DEFAULT_CODEX_HOME) + parser.add_argument("--roles", nargs="+", choices=["modeling", "verification"], default=list(DEFAULT_ROLES)) + parser.add_argument("--max-runtime-seconds", type=int, default=180) + parser.add_argument("--timeout-seconds", type=int, default=240) + parser.add_argument("--poll-seconds", type=float, default=5.0) + args = parser.parse_args() + result = run_live_specialist(args) + print(json.dumps(result, indent=2, ensure_ascii=False)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/vendor/codex/codex-rs/core/src/tools/handlers/agent_jobs.rs b/vendor/codex/codex-rs/core/src/tools/handlers/agent_jobs.rs index cd6d5f4a..e7707bbd 100644 --- a/vendor/codex/codex-rs/core/src/tools/handlers/agent_jobs.rs +++ b/vendor/codex/codex-rs/core/src/tools/handlers/agent_jobs.rs @@ -236,7 +236,7 @@ mod spawn_agents_on_csv { )); } - let db = required_state_db(&session)?; + let db = required_state_db(&session).await?; let input_path = turn.resolve_path(Some(args.csv_path)); let input_path_display = input_path.display().to_string(); let csv_content = tokio::fs::read_to_string(&input_path) @@ -478,7 +478,7 @@ mod report_agent_job_result { "result must be a JSON object".to_string(), )); } - let db = required_state_db(&session)?; + let db = required_state_db(&session).await?; let reporting_thread_id = session.conversation_id.to_string(); let accepted = db .report_agent_job_item_result( @@ -511,11 +511,23 @@ mod report_agent_job_result { } } -fn required_state_db( +async fn required_state_db( session: &Arc, ) -> Result, FunctionCallError> { - session.state_db().ok_or_else(|| { - FunctionCallError::Fatal("sqlite state db is unavailable for this session".to_string()) + if let Some(state_db) = session.state_db() { + return Ok(state_db); + } + + let config = session.get_config().await; + codex_state::StateRuntime::init( + config.codex_home.to_path_buf(), + config.model_provider_id.clone(), + ) + .await + .map_err(|err| { + FunctionCallError::Fatal(format!( + "sqlite state db is unavailable for this session and fallback init failed: {err}" + )) }) } diff --git a/vendor/codex/codex-rs/tools/src/tool_config.rs b/vendor/codex/codex-rs/tools/src/tool_config.rs index 20c812ce..c359374e 100644 --- a/vendor/codex/codex-rs/tools/src/tool_config.rs +++ b/vendor/codex/codex-rs/tools/src/tool_config.rs @@ -195,12 +195,11 @@ impl ToolsConfig { None => include_apply_patch_tool.then_some(ApplyPatchToolType::Freeform), }; - let agent_jobs_worker_tools = include_agent_jobs - && matches!( - session_source, - SessionSource::SubAgent(SubAgentSource::Other(label)) - if label.starts_with("agent_job:") - ); + let agent_jobs_worker_tools = matches!( + session_source, + SessionSource::SubAgent(SubAgentSource::Other(label)) + if label.starts_with("agent_job:") + ); Self { available_models: available_models.to_vec(), diff --git a/vendor/codex/codex-rs/tools/src/tool_config_tests.rs b/vendor/codex/codex-rs/tools/src/tool_config_tests.rs index 25c78a05..e1b9d828 100644 --- a/vendor/codex/codex-rs/tools/src/tool_config_tests.rs +++ b/vendor/codex/codex-rs/tools/src/tool_config_tests.rs @@ -157,6 +157,29 @@ fn subagents_keep_request_user_input_mode_config_and_agent_jobs_workers_opt_in_b assert!(tools_config.agent_jobs_worker_tools); } +#[test] +fn agent_job_workers_get_report_tool_even_when_csv_front_door_disabled() { + let model_info = model_info(); + let features = Features::with_defaults(); + + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + image_generation_tool_auth_allowed: true, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::SubAgent(SubAgentSource::Other( + "agent_job:test".to_string(), + )), + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + + assert!(!tools_config.agent_jobs_tools); + assert!(tools_config.agent_jobs_worker_tools); +} + #[test] fn image_generation_requires_feature_and_supported_model() { let supported_model_info = model_info(); diff --git a/vendor/codex/codex-rs/tools/src/tool_registry_plan.rs b/vendor/codex/codex-rs/tools/src/tool_registry_plan.rs index d3b075f5..69fd794c 100644 --- a/vendor/codex/codex-rs/tools/src/tool_registry_plan.rs +++ b/vendor/codex/codex-rs/tools/src/tool_registry_plan.rs @@ -484,14 +484,15 @@ pub fn build_tool_registry_plan( config.code_mode_enabled, ); plan.register_handler("spawn_agents_on_csv", ToolHandlerKind::AgentJobs); - if config.agent_jobs_worker_tools { - plan.push_spec( - create_report_agent_job_result_tool(), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - plan.register_handler("report_agent_job_result", ToolHandlerKind::AgentJobs); - } + } + + if config.agent_jobs_worker_tools { + plan.push_spec( + create_report_agent_job_result_tool(), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + plan.register_handler("report_agent_job_result", ToolHandlerKind::AgentJobs); } if let Some(mcp_tools) = params.mcp_tools { diff --git a/vendor/codex/codex-rs/tools/src/tool_registry_plan_tests.rs b/vendor/codex/codex-rs/tools/src/tool_registry_plan_tests.rs index 0b94ef64..b55e53e0 100644 --- a/vendor/codex/codex-rs/tools/src/tool_registry_plan_tests.rs +++ b/vendor/codex/codex-rs/tools/src/tool_registry_plan_tests.rs @@ -511,6 +511,36 @@ fn test_build_specs_agent_job_worker_tools_enabled() { ); } +#[test] +fn test_build_specs_agent_job_worker_report_tool_without_csv_front_door() { + let model_info = model_info(); + let mut features = Features::with_defaults(); + features.normalize_dependencies(); + features.enable(Feature::Sqlite); + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + image_generation_tool_auth_allowed: true, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::SubAgent(SubAgentSource::Other( + "agent_job:test".to_string(), + )), + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*deferred_mcp_tools*/ None, + &[], + ); + + assert_contains_tool_names(&tools, &["report_agent_job_result"]); + assert_lacks_tool_name(&tools, "spawn_agents_on_csv"); +} + #[test] fn request_user_input_description_reflects_default_mode_feature_flag() { let model_info = model_info();