Add Epiphany role specialist launch
This commit is contained in:
parent
a36e2b4a4b
commit
0e7f8a7685
16 changed files with 1790 additions and 56 deletions
|
|
@ -32,6 +32,7 @@ The spine exists in twenty-four live paths:
|
|||
- reorientation policy: app-server declares experimental read-only `thread/epiphany/reorient`, reuses the same live/stored thread view plus mapped pressure/freshness/watcher signals, and derives a bounded `resume`/`regather` verdict with checkpoint-path/frontier reasons without mutating, compacting, scheduling, notifying, or continuing work in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:365), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4514), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4394), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:11424).
|
||||
- CRRC coordinator recommendation: app-server declares experimental read-only `thread/epiphany/crrc`, reuses the same pressure/freshness/watcher/reorientation mapping plus the fixed `reorient-worker` binding and result read-back helper, and returns one bounded recommendation such as continue, prepare checkpoint, launch worker, wait, review, accept, or regather manually without mutating, launching, accepting, compacting, notifying, scheduling, or silently continuing work in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:370), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4661), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4546).
|
||||
- role ownership projection: app-server declares experimental read-only `thread/epiphany/roles`, reuses the same state/jobs/pressure/reorient/result/CRRC signals as the coordinator loop, and projects implementation, modeling/checkpoint, verification/review, and reorientation lanes with owner roles, status, linked jobs, authority scopes, and recommended scene actions without launching, scheduling, accepting, compacting, notifying, or mutating state in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:350), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4161), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4310).
|
||||
- role specialist launch/read-back: app-server declares experimental `thread/epiphany/roleLaunch` and read-only `thread/epiphany/roleResult`. `roleLaunch` accepts only fixed modeling/checkpoint and verification/review role ids, builds bounded specialist payloads from authoritative Epiphany state, routes them through the existing `epiphany_launch_job` job-control seam, and emits the normal `jobLaunch` state update for `jobBindings` only. `roleResult` resolves the fixed or requested binding through the current `agent_jobs` backend and projects structured output as a reviewable finding with verdict, summary, next safe move, inspected files, frontier/evidence ids, raw result, and errors without mutation, promotion, scheduling, or hidden continuation. Implementation stays owned by the main coding agent, and reorientation stays on the CRRC-specific `reorientLaunch` / `reorientResult` path.
|
||||
- MVP operator view: `tools/epiphany_mvp_status.py` launches app-server over stdio, starts or reads one thread, gathers scene, pressure, reorient, jobs, roles, reorient result, and CRRC responses, and renders them as text or JSON so the loop can be dogfooded without reading Rust.
|
||||
- reorientation launch: app-server now declares experimental `thread/epiphany/reorientLaunch`, requires a loaded thread plus durable investigation checkpoint, consumes the live reorientation verdict, and launches one fixed `reorient-worker` binding through the existing job-control seam with explicit resume-versus-regather scope, checkpoint-derived payload, and structured-output expectations instead of pretending CRRC suddenly grew a scheduler in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:370), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4575), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4519).
|
||||
- reorientation result read-back: app-server now declares experimental read-only `thread/epiphany/reorientResult`, defaults to the fixed `reorient-worker` binding, resolves that binding through the current `agent_jobs` backend when available, reads the single backend item result, and projects a reviewable finding with mode, summary, next safe move, checkpoint-validity flag, inspected files, frontier ids, evidence ids, raw result, and any job/item errors without mutating state, promoting evidence, or scheduling follow-up work in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:374), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4610), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4684).
|
||||
|
|
@ -1309,7 +1310,7 @@ These are deliberately not shipped yet:
|
|||
- no automatic Compact-Rehydrate-Reorient-Continue coordinator; current code preserves and rehydrates valid Epiphany snapshots across resume/rollback/compaction, exposes context pressure, carries a durable investigation checkpoint packet, returns a read-only reorientation verdict, can explicitly launch one bounded reorientation worker, read that worker's result back through a reviewable surface, and explicitly accept completed findings into typed state, but runtime launch/acceptance policy and next-step execution are still explicit rather than ambient scheduler behavior.
|
||||
- no automatic watcher-driven graph or semantic invalidation.
|
||||
- no code-intelligence graph.
|
||||
- no specialist-agent scheduler.
|
||||
- no broad specialist-agent scheduler or marketplace; only fixed manual modeling/checkpoint and verification/review role templates exist.
|
||||
- no Epiphany-owned long-running job executor beyond the current runtime `agent_jobs` seam.
|
||||
- no GUI implementation beyond the read-only scene, jobs, freshness, context, and pressure projection surfaces.
|
||||
- no durable retrieval-summary write from `thread/epiphany/retrieve`.
|
||||
|
|
@ -1329,6 +1330,8 @@ stored Epiphany state
|
|||
-> reflected as a read-only resume/regather verdict from checkpoint + freshness + watcher + pressure
|
||||
-> reflected as a read-only CRRC recommendation over verdict + worker/result state
|
||||
-> reflected as read-only MVP role ownership over implementation, modeling/checkpoint, verification/review, and reorientation lanes
|
||||
-> fixed modeling/checkpoint and verification/review role workers can be launched explicitly
|
||||
-> fixed role worker results can be read back for review without mutation
|
||||
-> can be consumed explicitly by one bounded reorient-worker launch surface
|
||||
-> retrieval/indexing can inform future work
|
||||
-> explicit distillation can draft observation/evidence patches
|
||||
|
|
@ -1338,9 +1341,7 @@ stored Epiphany state
|
|||
-> successful writes emit thread/epiphany/stateUpdated
|
||||
```
|
||||
|
||||
The current remaining missing organ is not the red pen, not the first chewing motion, not the first state-change bell, not the first client-shaped scene, not the first job/progress reflection, not the first live bound-job notification, not the first freshness lens, not the first watcher-backed invalidation read, not the first targeted graph/evidence context read, not the first context-pressure gauge, not the first durable investigation checkpoint packet, not the first bounded reorientation verdict, not the first thin launcher seam, not the first explicit launch/interrupt authority surface over that seam, not the first runtime consumer that can act on the reorientation verdict without becoming a hidden tyrant, not the first read-back surface for that worker's findings, not the first explicit acceptance write for completed findings, not the first read-only CRRC coordinator recommendation, and no longer the first dogfood-facing operator view over the landed loop. Those exist. Proposal now has the first bits of map memory, focus, and selection hygiene: it can auto-select a bounded evidence-backed observation cluster when ids are omitted, requires accepting recent evidence behind selected observations, prioritizes stronger selected observations for proposal wording, reuses existing architecture nodes by concrete code-ref/path/id checks before creating new path nodes, can rescue unanchored graph nodes through strict unique semantic overlap, follows graph links and incident edges into the frontier, and reports match-kind-aware churn pressure from the actual proposal shape. Distillation now also has the first source-output-aware teeth for tool/model summaries. Promotion now notices risky deltas instead of just checking the shape of the envelope, treats graph expansion as risky even if the patch tries to mumble "low pressure" while dragging a new wall into the blueprint, token-matches strong verifier kinds so `contest` cannot sneak past as `test`, and validates investigation checkpoints against known evidence before the packet can be inked. Successful update/promote writes now emit `thread/epiphany/stateUpdated` with `source: "update"` or `source: "promote"`, event-level revision, and typed changed fields so clients no longer need to poll blindly, infer the cause from timing, or diff the whole state to know which top-level sections moved. Runtime `agent_job_progress` background events now also emit `thread/epiphany/jobsUpdated` with changed bound-job payloads so clients no longer need to poll `thread/epiphany/jobs` just to watch one real owner move, the bound jobs now reflect launcher id, authority scope, and backend kind/job id instead of treating raw runtime pointers as the architecture, `jobLaunch` / `jobInterrupt` can create or tear down those bindings without pretending reflection is authority, `reorientLaunch` can launch one fixed `reorient-worker` job from the checkpoint verdict without turning the verdict itself into a writer, `reorientResult` can read the worker's structured output back as a reviewable finding without promoting it, `reorientAccept` can explicitly bank a completed finding into typed observation/evidence plus optional scratch/checkpoint state, and `crrc` can recommend the next explicit action over that loop without doing the action itself. The scene, jobs, freshness, context, pressure, reorient, crrc, reorient-result, and MVP status surfaces give clients compact or bounded read-only views without making the GUI/client authoritative, scene/context surface the checkpoint packet back out without becoming writers, freshness surfaces watcher telemetry without becoming a hidden worker, reorient turns those signals into an explicit wakeup verdict that one explicit launch surface can consume on purpose, crrc tells the operator whether to continue, checkpoint, launch, wait, review, accept, or regather manually, and the status tool prints the answer without requiring a Rust spelunk. The remaining missing organs are a second backend if `agent_jobs` stops being enough and eventually role-scoped specialist ownership without silently auto-promoting anything.
|
||||
|
||||
Update: first role-scoped specialist ownership is now landed as read-only `thread/epiphany/roles`. The next missing organ is explicit role-scoped launch/read-back templates for modeling/checkpoint and verification/review, still manual and review-gated.
|
||||
The current remaining missing organ is not the red pen, not the first chewing motion, not the first state-change bell, not the first client-shaped scene, not the first job/progress reflection, not the first live bound-job notification, not the first freshness lens, not the first watcher-backed invalidation read, not the first targeted graph/evidence context read, not the first context-pressure gauge, not the first durable investigation checkpoint packet, not the first bounded reorientation verdict, not the first thin launcher seam, not the first explicit launch/interrupt authority surface over that seam, not the first runtime consumer that can act on the reorientation verdict without becoming a hidden tyrant, not the first read-back surface for that worker's findings, not the first explicit acceptance write for completed findings, not the first read-only CRRC coordinator recommendation, not the first role ownership board, not the first fixed role specialist launch/read-back templates, and no longer the first dogfood-facing operator view over the landed loop. Those exist. Proposal now has the first bits of map memory, focus, and selection hygiene: it can auto-select a bounded evidence-backed observation cluster when ids are omitted, requires accepting recent evidence behind selected observations, prioritizes stronger selected observations for proposal wording, reuses existing architecture nodes by concrete code-ref/path/id checks before creating new path nodes, can rescue unanchored graph nodes through strict unique semantic overlap, follows graph links and incident edges into the frontier, and reports match-kind-aware churn pressure from the actual proposal shape. Distillation now also has the first source-output-aware teeth for tool/model summaries. Promotion now notices risky deltas instead of just checking the shape of the envelope, treats graph expansion as risky even if the patch tries to mumble "low pressure" while dragging a new wall into the blueprint, token-matches strong verifier kinds so `contest` cannot sneak past as `test`, and validates investigation checkpoints against known evidence before the packet can be inked. Successful update/promote writes now emit `thread/epiphany/stateUpdated` with `source: "update"` or `source: "promote"`, event-level revision, and typed changed fields so clients no longer need to poll blindly, infer the cause from timing, or diff the whole state to know which top-level sections moved. Runtime `agent_job_progress` background events now also emit `thread/epiphany/jobsUpdated` with changed bound-job payloads so clients no longer need to poll `thread/epiphany/jobs` just to watch one real owner move, the bound jobs now reflect launcher id, authority scope, and backend kind/job id instead of treating raw runtime pointers as the architecture, `jobLaunch` / `jobInterrupt` can create or tear down those bindings without pretending reflection is authority, `reorientLaunch` can launch one fixed `reorient-worker` job from the checkpoint verdict without turning the verdict itself into a writer, `reorientResult` can read the worker's structured output back as a reviewable finding without promoting it, `reorientAccept` can explicitly bank a completed finding into typed observation/evidence plus optional scratch/checkpoint state, `roleLaunch` can launch fixed modeling/checkpoint and verification/review specialists over that same bounded seam, `roleResult` can read their structured outputs back without mutation, and `crrc` can recommend the next explicit action over that loop without doing the action itself. The scene, jobs, roles, role-result, freshness, context, pressure, reorient, crrc, reorient-result, and MVP status surfaces give clients compact or bounded views without making the GUI/client authoritative. The remaining missing organs are a second backend if `agent_jobs` stops being enough, automatic scheduling if dogfood proves it is needed, and richer product ergonomics after the MVP loop has been tested by a human.
|
||||
|
||||
```text
|
||||
model/tool observations
|
||||
|
|
@ -1385,7 +1386,9 @@ Current tests cover the landed flows at useful seams:
|
|||
- live app-server smoke for the first bounded reorient-launch runtime consumer plus CRRC coordinator recommendations in `tools/epiphany_phase6_reorient_launch_smoke.py`.
|
||||
- live app-server smoke for the first MVP operator status view in `tools/epiphany_mvp_status_smoke.py`.
|
||||
- focused app-server mapper coverage for `thread/epiphany/roles`, plus MVP status smoke coverage that consumes the roles surface.
|
||||
- focused app-server/protocol coverage for `thread/epiphany/roleLaunch` and `thread/epiphany/roleResult`, plus live role smoke coverage that launches modeling and verification workers and reads completed results without state mutation.
|
||||
- live app-server smoke for the first Phase 6 job-control boundary in `tools/epiphany_phase6_job_control_smoke.py`.
|
||||
- live app-server smoke for explicit Phase 6 role launch/read-back in `tools/epiphany_phase6_role_smoke.py`.
|
||||
|
||||
Useful commands:
|
||||
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ The landed machine now has:
|
|||
- explicit Phase 6 reorient-worker finding acceptance through `thread/epiphany/reorientAccept`
|
||||
- read-only Phase 6 CRRC coordinator recommendation through `thread/epiphany/crrc`
|
||||
- read-only Phase 6 role ownership through `thread/epiphany/roles`, projecting implementation, modeling/checkpoint, verification/review, and reorientation lanes from typed state plus jobs/CRRC/result signals
|
||||
- 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`
|
||||
- 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`
|
||||
|
|
@ -106,6 +107,8 @@ These boundaries are more important than the individual method names:
|
|||
- `thread/epiphany/reorient` is a bounded policy verdict, not an automatic runtime coordinator, scheduler, compactor, or hidden state writer.
|
||||
- `thread/epiphany/crrc` is a read-only coordinator recommendation over existing signals, not a scheduler, launch button, acceptance gate, compactor, or hidden state writer.
|
||||
- `thread/epiphany/roles` is a read-only role ownership projection, not a specialist scheduler, marketplace, launcher, acceptance gate, or second job backend.
|
||||
- `thread/epiphany/roleLaunch` is a bounded authority surface for fixed modeling/checkpoint and verification/review templates, not a broad scheduler or specialist marketplace.
|
||||
- `thread/epiphany/roleResult` is a read-only result projection, not a promotion gate, state writer, scheduler, or hidden continuation trigger.
|
||||
- The GUI may render and steer typed state, but it must not manufacture canonical understanding.
|
||||
- The app-server remains a host seam; Epiphany-owned machinery should live in `epiphany-core` where practical.
|
||||
- Qdrant is the preferred persistent semantic backend; BM25 remains the bootstrap/fallback/control path.
|
||||
|
|
@ -175,15 +178,18 @@ marketplace, broad ambient scheduling, automatic promotion of every tool
|
|||
observation, a GUI-first workflow, or a second job backend unless the current
|
||||
`agent_jobs` seam blocks the product loop.
|
||||
|
||||
The read-back, acceptance, coordinator, first dogfood-view, and first
|
||||
harness-native role ownership blockers are now landed as
|
||||
The read-back, acceptance, coordinator, first dogfood-view, first
|
||||
harness-native role ownership, and first fixed role specialist launch/result
|
||||
blockers are now landed as
|
||||
`thread/epiphany/reorientResult`, `thread/epiphany/reorientAccept`,
|
||||
`thread/epiphany/crrc`, `tools/epiphany_mvp_status.py`, and
|
||||
`thread/epiphany/roles`. A human can now ask the harness what it believes, what
|
||||
it recommends, and which role lane owns the next visible work without reading
|
||||
Rust. The next MVP blocker is making modeling/checkpoint and verification/review
|
||||
launch/read-back templates explicit over the landed job-control seam, while
|
||||
keeping scheduling manual and review-gated.
|
||||
`thread/epiphany/roles`, `thread/epiphany/roleLaunch`, and
|
||||
`thread/epiphany/roleResult`. A human can now ask the harness what it believes,
|
||||
what it recommends, which role lane owns the next visible work, explicitly
|
||||
launch modeling/checkpoint or verification/review workers, and read their
|
||||
findings back without reading Rust. The next MVP question is dogfood, not
|
||||
another imagined organ: use the landed loop on real bounded coding work and fix
|
||||
only concrete blockers.
|
||||
|
||||
## Phase 6 Direction
|
||||
|
||||
|
|
@ -191,7 +197,7 @@ Phase 6 should grow observable harness state outward from the typed spine.
|
|||
|
||||
Useful candidates:
|
||||
|
||||
1. Add the smallest explicit role-scoped specialist launch/read-back templates for modeling/checkpoint maintenance and verification/review over the landed job-control seam, without building a marketplace.
|
||||
1. Dogfood the landed MVP loop on real bounded coding work through state, status, roles, roleLaunch/roleResult, and CRRC.
|
||||
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.
|
||||
|
||||
|
|
@ -258,6 +264,12 @@ For app-server protocol changes, expect to run the relevant protocol tests,
|
|||
regenerate stable schema fixtures when needed, and verify the generated tree is
|
||||
intentional.
|
||||
|
||||
Before modifying explicit role launch/read-back behavior, run:
|
||||
|
||||
```powershell
|
||||
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_role_smoke.py'
|
||||
```
|
||||
|
||||
On this Windows machine, use:
|
||||
|
||||
```powershell
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ Do not trust this file for the exact live HEAD. Always check git.
|
|||
|
||||
- Do not copy exact branch or HEAD from this note. Run `git status --short --branch` and `git log --oneline -5`.
|
||||
- Phase 1 through Phase 5 are complete enough.
|
||||
- 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`, and `thread/epiphany/reorientResult`; 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` 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, reorient turns checkpoint plus freshness/pressure/watcher signals into a read-only resume-versus-regather verdict, `thread/epiphany/reorientLaunch` is the first 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.
|
||||
- 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, reorient result, and CRRC recommendation as text or JSON.
|
||||
- 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.
|
||||
|
|
@ -64,6 +64,8 @@ The current spine:
|
|||
- verifier-backed promotion through `thread/epiphany/promote`
|
||||
- successful-write notification through `thread/epiphany/stateUpdated`
|
||||
- explicit launch/interrupt authority through `thread/epiphany/jobLaunch` and `thread/epiphany/jobInterrupt`
|
||||
- explicit modeling/checkpoint and verification/review specialist launch through `thread/epiphany/roleLaunch`
|
||||
- read-only modeling/checkpoint and verification/review specialist result read-back through `thread/epiphany/roleResult`
|
||||
- bounded reorient-guided worker launch through `thread/epiphany/reorientLaunch`
|
||||
- read-only reorient-worker result read-back through `thread/epiphany/reorientResult`
|
||||
- explicit reorient-worker finding acceptance through `thread/epiphany/reorientAccept`
|
||||
|
|
@ -106,8 +108,9 @@ The exact current control flow is documented in
|
|||
- `thread/epiphany/reorient` is read-only.
|
||||
- `thread/epiphany/crrc` is read-only.
|
||||
- `thread/epiphany/roles` is read-only.
|
||||
- `thread/epiphany/roleResult` is read-only.
|
||||
- `thread/epiphany/reorientResult` is read-only.
|
||||
- Durable typed state writes go through `thread/epiphany/update`, accepted `thread/epiphany/promote`, `thread/epiphany/reorientAccept`, or the bounded `thread/epiphany/jobLaunch`, `thread/epiphany/jobInterrupt`, and `thread/epiphany/reorientLaunch` authority surfaces when they mutate `jobBindings`.
|
||||
- Durable typed state writes go through `thread/epiphany/update`, accepted `thread/epiphany/promote`, `thread/epiphany/reorientAccept`, or the bounded `thread/epiphany/jobLaunch`, `thread/epiphany/jobInterrupt`, `thread/epiphany/reorientLaunch`, and `thread/epiphany/roleLaunch` authority surfaces when they mutate `jobBindings`.
|
||||
- `thread/epiphany/index` writes the retrieval catalog, not durable Epiphany understanding.
|
||||
- GUI/client surfaces reflect and steer typed state; they do not become the source of truth.
|
||||
- Do not restart Phase 5 hardening without a concrete regression.
|
||||
|
|
@ -184,6 +187,13 @@ For the first MVP operator status view, run:
|
|||
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_mvp_status_smoke.py'
|
||||
```
|
||||
|
||||
For explicit modeling/checkpoint and verification/review role launch/read-back,
|
||||
run:
|
||||
|
||||
```powershell
|
||||
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_role_smoke.py'
|
||||
```
|
||||
|
||||
For Codex Rust work on this Windows machine:
|
||||
|
||||
```powershell
|
||||
|
|
@ -251,11 +261,10 @@ still explicit even though read-only job ownership/progress reflection now has a
|
|||
real runtime seam, the thin launcher boundary is landed, and one explicit
|
||||
reorient-guided launch surface can consume the verdict on purpose.
|
||||
|
||||
When the user asks to continue, the next likely organ is the smallest explicit
|
||||
role-scoped specialist launch/read-back template layer for modeling/checkpoint
|
||||
maintenance and verification/review over the landed job-control seam. Keep it
|
||||
manual and review-gated; do not build a broad hidden scheduler, marketplace, or
|
||||
GUI-as-source-of-truth.
|
||||
When the user asks to continue, dogfood the landed MVP loop on real bounded
|
||||
coding work through the state, status, roles, roleLaunch/roleResult, and CRRC
|
||||
surfaces. Fix concrete blockers that prevent a human from testing that 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`,
|
||||
|
|
|
|||
|
|
@ -54,3 +54,4 @@
|
|||
{"ts": "2026-04-28T11:08:09+00:00", "type": "implementation", "status": "ok", "note": "Landed tools/epiphany_mvp_status.py as the first dogfood operator view over scene, pressure, reorient, jobs, reorient-result, and CRRC recommendation; epiphany_mvp_status_smoke.py verifies cold-start missing-state rendering and CRRC regather recommendation through the live app-server."}
|
||||
{"ts": "2026-04-28T11:13:43+00:00", "type": "implementation", "status": "ok", "note": "Extended the MVP status view with derived implementation, modeling/checkpoint, verification/review, and reorientation lanes over existing app-server signals; the status smoke now asserts the four role lanes and rendered operator section."}
|
||||
{"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."}
|
||||
|
|
|
|||
|
|
@ -53,17 +53,17 @@ data_flow:
|
|||
|
||||
active_subgoals:
|
||||
- Treat Phase 5 update/distill/propose/promote/read/notification as complete enough unless a concrete regression appears.
|
||||
- Preserve read-only boundaries for retrieve, distill, propose, scene, jobs, roles, freshness, context, pressure, reorient, crrc, and reorientResult.
|
||||
- Preserve read-only boundaries for retrieve, distill, propose, scene, jobs, roles, roleResult, freshness, context, pressure, reorient, crrc, and reorientResult.
|
||||
- Extend Phase 6 observability only where it reflects authoritative state or retrieval summaries without becoming a scheduler.
|
||||
- Build explicit role-scoped specialist launch/read-back templates over the landed roles and job-control seams instead of adding invisible organs.
|
||||
- Dogfood the landed MVP loop on real bounded coding work before adding larger organs.
|
||||
- Keep persistent project state ruthlessly distilled so rehydration strengthens the next turn instead of bloating it.
|
||||
|
||||
accepted_design:
|
||||
- Epiphany is a fork-level modeling architecture, not merely a Codex collaboration mode.
|
||||
- Authoritative Epiphany state lives in Codex protocol/core session and rollout state.
|
||||
- The prompt reads typed state through a bounded `<epiphany_state>` developer fragment.
|
||||
- Clients read typed state through `Thread.epiphanyState`, compact reflections such as `thread/epiphany/scene`, `thread/epiphany/jobs`, and `thread/epiphany/freshness`, targeted state shards through `thread/epiphany/context`, runtime pressure through `thread/epiphany/pressure`, bounded CRRC reorientation verdicts through `thread/epiphany/reorient`, read-only CRRC next-action recommendations through `thread/epiphany/crrc`, and live bound-job progress through `thread/epiphany/jobsUpdated`.
|
||||
- Durable typed state writes route through `thread/epiphany/update`, accepted `thread/epiphany/promote`, or the bounded `thread/epiphany/jobLaunch`, `thread/epiphany/jobInterrupt`, and `thread/epiphany/reorientLaunch` authority surfaces when they mutate `job_bindings`.
|
||||
- Clients read typed state through `Thread.epiphanyState`, compact reflections such as `thread/epiphany/scene`, `thread/epiphany/jobs`, and `thread/epiphany/freshness`, targeted state shards through `thread/epiphany/context`, runtime pressure through `thread/epiphany/pressure`, bounded CRRC reorientation verdicts through `thread/epiphany/reorient`, read-only CRRC next-action recommendations through `thread/epiphany/crrc`, role ownership through `thread/epiphany/roles`, reviewable role output through `thread/epiphany/roleResult`, and live bound-job progress through `thread/epiphany/jobsUpdated`.
|
||||
- Durable typed state writes route through `thread/epiphany/update`, accepted `thread/epiphany/promote`, or the bounded `thread/epiphany/jobLaunch`, `thread/epiphany/jobInterrupt`, `thread/epiphany/reorientLaunch`, and `thread/epiphany/roleLaunch` authority surfaces when they mutate `job_bindings`.
|
||||
- `thread/epiphany/retrieve`, `thread/epiphany/distill`, `thread/epiphany/propose`, `thread/epiphany/scene`, `thread/epiphany/jobs`, `thread/epiphany/freshness`, `thread/epiphany/context`, `thread/epiphany/pressure`, and `thread/epiphany/reorient` stay read-only.
|
||||
- `thread/epiphany/index` writes the retrieval catalog, not durable Epiphany understanding.
|
||||
- Qdrant is the preferred persistent semantic backend; BM25 remains the bootstrap/fallback/control path.
|
||||
|
|
@ -84,6 +84,7 @@ accepted_design:
|
|||
- `thread/epiphany/crrc` is the first bounded CRRC coordinator recommendation surface: it composes pressure, the reorientation verdict, fixed reorient-worker binding/result status, finding read-back, and available actions into one read-only recommendation without launching, accepting, compacting, scheduling, or mutating state.
|
||||
- `tools/epiphany_mvp_status.py` is the first dogfood operator view: it starts or reads a thread through app-server and renders scene, pressure, reorient, jobs, reorient result, CRRC recommendation, and MVP role lanes as text or JSON without becoming a state store.
|
||||
- `thread/epiphany/roles` is the first harness-native role ownership surface: it projects implementation, modeling/checkpoint, verification/review, and reorientation lanes from authoritative typed state, bound jobs, pressure/reorient/CRRC, and worker result status without launching, scheduling, accepting, compacting, or mutating state.
|
||||
- `thread/epiphany/roleLaunch` and `thread/epiphany/roleResult` are the first explicit role-scoped specialist templates: modeling/checkpoint and verification/review can be launched through fixed bindings over the existing job-control seam and read back as reviewable findings, while implementation stays owned by the main coding agent and reorientation stays on the CRRC-specific launch/result path.
|
||||
- `thread/epiphany/reorientLaunch` is the first bounded runtime consumer over the reorientation verdict: it launches one fixed `reorient-worker` job through the existing job-control seam with explicit resume-versus-regather scope, authority, and checkpoint-derived payload, without making `thread/epiphany/reorient` a writer or turning CRRC into an ambient scheduler.
|
||||
- `thread/epiphany/reorientResult` is the first bounded read-back surface for that worker: it reads the fixed or requested reorient-worker binding through the current `agent_jobs` backend and projects completed output as a reviewable finding without mutation, promotion, scheduling, or hidden continuation.
|
||||
- `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.
|
||||
|
|
@ -109,6 +110,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 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:
|
||||
- Use `tools/epiphany_phase5_smoke.py` before modifying update, distill, propose, promote, read-projection, or notification behavior.
|
||||
- Use `tools/epiphany_phase6_scene_smoke.py` before modifying scene projection behavior.
|
||||
|
|
@ -123,6 +125,7 @@ landed_baseline:
|
|||
- Use `tools/epiphany_phase6_reorient_launch_smoke.py` before modifying the bounded reorient-worker result read-back surface.
|
||||
- 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 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.
|
||||
|
|
@ -141,5 +144,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, and first dogfood MVP status are landed. `thread/epiphany/roles` now projects implementation, modeling/checkpoint, verification/review, and reorientation lanes from authoritative typed state plus jobs/pressure/reorient/CRRC/result signals without launching, scheduling, accepting, compacting, or mutating state. The status tool consumes that harness-native roles surface instead of deriving lanes locally. The thin launcher seam, jobLaunch/jobInterrupt, jobsUpdated, reorientLaunch, reorientResult, reorientAccept, and crrc remain explicit and review-gated. Focused protocol/app-server tests, schema fixture regeneration/checks, app-server build, and MVP status smoke verify the role ownership/coordinator/operator seam. Persistent state remains distilled; evidence is a belief ledger, not an activity feed.
|
||||
next_action: Continue toward the MVP by adding the smallest explicit role-scoped specialist launch/read-back templates for modeling/checkpoint and verification/review over the landed job-control seam, while keeping scheduling manual and review-gated. Automatic broad specialist scheduling remains future work; do not restart the completed pressure, freshness, watcher-invalidation, investigation-checkpoint, reorientation, crrc, mvp-status, roles, jobs-binding, jobsUpdated, thin-launcher-seam, job-control, reorient-launch, reorient-result, or reorient-accept slice unless a concrete regression appears.
|
||||
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, and first dogfood MVP status are landed. `thread/epiphany/roleLaunch` now launches only fixed modeling/checkpoint or verification/review specialists over the existing job-control seam, and `thread/epiphany/roleResult` reads their backend output back as reviewable findings without mutation. 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: Dogfood the MVP loop on a real bounded coding task using the landed state, status, roles, roleLaunch/roleResult, and CRRC surfaces; fix only concrete blockers that prevent a human from testing the product loop. Automatic broad specialist scheduling, GUI-as-source-of-truth, a specialist marketplace, and a second job backend remain future work.
|
||||
|
|
|
|||
363
tools/epiphany_phase6_role_smoke.py
Normal file
363
tools/epiphany_phase6_role_smoke.py
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from epiphany_phase5_smoke import AppServerClient
|
||||
from epiphany_phase5_smoke import DEFAULT_APP_SERVER
|
||||
from epiphany_phase5_smoke import ROOT
|
||||
from epiphany_phase5_smoke import require
|
||||
from epiphany_phase5_smoke import reset_smoke_paths
|
||||
from epiphany_phase6_reorient_launch_smoke import GRAPH_NODE_ID
|
||||
from epiphany_phase6_reorient_launch_smoke import SUBGOAL_ID
|
||||
from epiphany_phase6_reorient_launch_smoke import job_by_id
|
||||
from epiphany_phase6_reorient_launch_smoke import locate_state_db
|
||||
from epiphany_phase6_reorient_smoke import WATCHED_RELATIVE_PATH
|
||||
from epiphany_phase6_reorient_smoke import prepare_workspace
|
||||
from epiphany_phase6_reorient_smoke import reorient_patch
|
||||
|
||||
|
||||
DEFAULT_CODEX_HOME = ROOT / ".epiphany-smoke" / "phase6-role-codex-home"
|
||||
DEFAULT_WORKSPACE = ROOT / ".epiphany-smoke" / "phase6-role-workspace"
|
||||
DEFAULT_RESULT = ROOT / ".epiphany-smoke" / "phase6-role-smoke-result.json"
|
||||
DEFAULT_TRANSCRIPT = ROOT / ".epiphany-smoke" / "phase6-role-smoke-transcript.jsonl"
|
||||
DEFAULT_STDERR = ROOT / ".epiphany-smoke" / "phase6-role-smoke-server.stderr.log"
|
||||
|
||||
MODELING_BINDING_ID = "modeling-checkpoint-worker"
|
||||
VERIFICATION_BINDING_ID = "verification-review-worker"
|
||||
|
||||
|
||||
def complete_role_backend_job(
|
||||
codex_home: Path,
|
||||
backend_job_id: str,
|
||||
*,
|
||||
binding_id: str,
|
||||
role_id: str,
|
||||
verdict: str,
|
||||
) -> dict[str, Any]:
|
||||
result = {
|
||||
"roleId": role_id,
|
||||
"verdict": verdict,
|
||||
"summary": f"{role_id} role result was read back from agent_jobs.",
|
||||
"nextSafeMove": f"Review the {role_id} finding before mutating Epiphany state.",
|
||||
"checkpointSummary": "Checkpoint remains bounded to the smoke slice.",
|
||||
"scratchSummary": "Scratch remains review-only until explicitly updated.",
|
||||
"filesInspected": [WATCHED_RELATIVE_PATH.as_posix()],
|
||||
"frontierNodeIds": [GRAPH_NODE_ID],
|
||||
"evidenceIds": ["ev-checkpoint"],
|
||||
}
|
||||
now = int(time.time())
|
||||
db_path = locate_state_db(codex_home)
|
||||
connection = sqlite3.connect(db_path)
|
||||
try:
|
||||
with connection:
|
||||
connection.execute(
|
||||
"""
|
||||
UPDATE agent_jobs
|
||||
SET status = 'completed', updated_at = ?, completed_at = ?, last_error = NULL
|
||||
WHERE id = ?
|
||||
""",
|
||||
(now, now, backend_job_id),
|
||||
)
|
||||
connection.execute(
|
||||
"""
|
||||
UPDATE agent_job_items
|
||||
SET status = 'completed',
|
||||
result_json = ?,
|
||||
reported_at = ?,
|
||||
completed_at = ?,
|
||||
updated_at = ?,
|
||||
last_error = NULL,
|
||||
assigned_thread_id = NULL
|
||||
WHERE job_id = ? AND item_id = ?
|
||||
""",
|
||||
(json.dumps(result), now, now, now, backend_job_id, binding_id),
|
||||
)
|
||||
finally:
|
||||
connection.close()
|
||||
return result
|
||||
|
||||
|
||||
def assert_role_job(
|
||||
job: dict[str, Any],
|
||||
*,
|
||||
binding_id: str,
|
||||
owner_role: str,
|
||||
authority_scope: str,
|
||||
scope: str,
|
||||
) -> None:
|
||||
require(job["id"] == binding_id, f"{binding_id} should use the fixed binding id")
|
||||
require(job["kind"] == "specialist", f"{binding_id} should use a specialist job")
|
||||
require(job["ownerRole"] == owner_role, f"{binding_id} should use the fixed owner role")
|
||||
require(job["authorityScope"] == authority_scope, f"{binding_id} should expose authority")
|
||||
require(job["scope"] == scope, f"{binding_id} should expose the fixed scope")
|
||||
require(job["backendKind"] == "agentJobs", f"{binding_id} should use agent_jobs")
|
||||
require(
|
||||
isinstance(job.get("backendJobId"), str) and job["backendJobId"],
|
||||
f"{binding_id} should expose a backend job id",
|
||||
)
|
||||
require(
|
||||
job["runtimeAgentJobId"] == job["backendJobId"],
|
||||
f"{binding_id} should align runtime and backend ids",
|
||||
)
|
||||
require(job["linkedSubgoalIds"] == [SUBGOAL_ID], f"{binding_id} should bind subgoal")
|
||||
require(job["linkedGraphNodeIds"] == [GRAPH_NODE_ID], f"{binding_id} should bind frontier")
|
||||
|
||||
|
||||
def launch_role(
|
||||
client: AppServerClient,
|
||||
thread_id: str,
|
||||
*,
|
||||
role_id: str,
|
||||
expected_revision: int,
|
||||
) -> dict[str, Any]:
|
||||
notification_start = len(client.notifications)
|
||||
launch = client.send(
|
||||
"thread/epiphany/roleLaunch",
|
||||
{
|
||||
"threadId": thread_id,
|
||||
"roleId": role_id,
|
||||
"expectedRevision": expected_revision,
|
||||
"maxRuntimeSeconds": 90,
|
||||
},
|
||||
)
|
||||
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",
|
||||
)
|
||||
notification = client.wait_for_notification(
|
||||
"thread/epiphany/stateUpdated",
|
||||
start_index=notification_start,
|
||||
timeout=15.0,
|
||||
)
|
||||
require(
|
||||
notification["params"]["source"] == "jobLaunch",
|
||||
f"{role_id} launch should emit jobLaunch state update source",
|
||||
)
|
||||
require(
|
||||
notification["params"]["revision"] == launch["revision"],
|
||||
f"{role_id} launch notification should expose the launch revision",
|
||||
)
|
||||
return launch
|
||||
|
||||
|
||||
def assert_role_result(
|
||||
result: dict[str, Any],
|
||||
*,
|
||||
role_id: str,
|
||||
binding_id: str,
|
||||
payload: dict[str, Any],
|
||||
expected_revision: int,
|
||||
) -> None:
|
||||
require(result["source"] == "live", f"{role_id} result should read live state")
|
||||
require(result["roleId"] == role_id, f"{role_id} result should echo role id")
|
||||
require(result["stateStatus"] == "ready", f"{role_id} result should see ready state")
|
||||
require(
|
||||
result["stateRevision"] == expected_revision,
|
||||
f"{role_id} result should preserve the read revision",
|
||||
)
|
||||
require(result["bindingId"] == binding_id, f"{role_id} result should use fixed binding")
|
||||
require(result["status"] == "completed", f"{role_id} result should be completed")
|
||||
require(
|
||||
result["finding"]["rawResult"] == payload,
|
||||
f"{role_id} result should expose raw worker output",
|
||||
)
|
||||
require(
|
||||
result["finding"]["nextSafeMove"]
|
||||
== f"Review the {role_id} finding before mutating Epiphany state.",
|
||||
f"{role_id} result should project next safe move",
|
||||
)
|
||||
|
||||
|
||||
def run_smoke(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()
|
||||
workspace = args.workspace.resolve()
|
||||
prepare_workspace(workspace)
|
||||
result_path = args.result.resolve()
|
||||
transcript_path = args.transcript.resolve()
|
||||
stderr_path = args.stderr.resolve()
|
||||
reset_smoke_paths(codex_home, result_path, transcript_path, stderr_path)
|
||||
|
||||
with AppServerClient(app_server, codex_home, transcript_path, stderr_path) as client:
|
||||
client.send(
|
||||
"initialize",
|
||||
{
|
||||
"clientInfo": {
|
||||
"name": "epiphany-phase6-role-smoke",
|
||||
"title": "Epiphany Phase 6 Role Smoke",
|
||||
"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_notification_start = len(client.notifications)
|
||||
update = client.send(
|
||||
"thread/epiphany/update",
|
||||
{"threadId": thread_id, "expectedRevision": 0, "patch": reorient_patch()},
|
||||
)
|
||||
assert update is not None
|
||||
require(update["revision"] == 1, "role smoke patch should advance revision to 1")
|
||||
client.wait_for_notification(
|
||||
"thread/epiphany/stateUpdated",
|
||||
start_index=update_notification_start,
|
||||
)
|
||||
|
||||
modeling_launch = launch_role(
|
||||
client,
|
||||
thread_id,
|
||||
role_id="modeling",
|
||||
expected_revision=1,
|
||||
)
|
||||
require(modeling_launch["revision"] == 2, "modeling launch should advance revision")
|
||||
assert_role_job(
|
||||
modeling_launch["job"],
|
||||
binding_id=MODELING_BINDING_ID,
|
||||
owner_role="epiphany-modeler",
|
||||
authority_scope="epiphany.role.modeling",
|
||||
scope="role-scoped modeling/checkpoint maintenance",
|
||||
)
|
||||
modeling_payload = complete_role_backend_job(
|
||||
codex_home,
|
||||
modeling_launch["job"]["backendJobId"],
|
||||
binding_id=MODELING_BINDING_ID,
|
||||
role_id="modeling",
|
||||
verdict="checkpoint-ready",
|
||||
)
|
||||
modeling_result_start = len(client.notifications)
|
||||
modeling_result = client.send(
|
||||
"thread/epiphany/roleResult",
|
||||
{"threadId": thread_id, "roleId": "modeling"},
|
||||
)
|
||||
assert modeling_result is not None
|
||||
assert_role_result(
|
||||
modeling_result,
|
||||
role_id="modeling",
|
||||
binding_id=MODELING_BINDING_ID,
|
||||
payload=modeling_payload,
|
||||
expected_revision=2,
|
||||
)
|
||||
client.require_no_notification(
|
||||
"thread/epiphany/stateUpdated",
|
||||
start_index=modeling_result_start,
|
||||
)
|
||||
|
||||
modeling_roles = client.send("thread/epiphany/roles", {"threadId": thread_id})
|
||||
assert modeling_roles is not None
|
||||
modeling_lane = next(
|
||||
role for role in modeling_roles["roles"] if role["id"] == "modeling"
|
||||
)
|
||||
require(
|
||||
modeling_lane["recommendedAction"] == "roleResult",
|
||||
"roles should point modeling at roleResult while the binding exists",
|
||||
)
|
||||
assert_role_job(
|
||||
job_by_id(modeling_lane["jobs"], MODELING_BINDING_ID),
|
||||
binding_id=MODELING_BINDING_ID,
|
||||
owner_role="epiphany-modeler",
|
||||
authority_scope="epiphany.role.modeling",
|
||||
scope="role-scoped modeling/checkpoint maintenance",
|
||||
)
|
||||
|
||||
verification_launch = launch_role(
|
||||
client,
|
||||
thread_id,
|
||||
role_id="verification",
|
||||
expected_revision=2,
|
||||
)
|
||||
require(
|
||||
verification_launch["revision"] == 3,
|
||||
"verification launch should advance revision",
|
||||
)
|
||||
assert_role_job(
|
||||
verification_launch["job"],
|
||||
binding_id=VERIFICATION_BINDING_ID,
|
||||
owner_role="epiphany-verifier",
|
||||
authority_scope="epiphany.role.verification",
|
||||
scope="role-scoped verification/review",
|
||||
)
|
||||
verification_payload = complete_role_backend_job(
|
||||
codex_home,
|
||||
verification_launch["job"]["backendJobId"],
|
||||
binding_id=VERIFICATION_BINDING_ID,
|
||||
role_id="verification",
|
||||
verdict="pass",
|
||||
)
|
||||
verification_result_start = len(client.notifications)
|
||||
verification_result = client.send(
|
||||
"thread/epiphany/roleResult",
|
||||
{"threadId": thread_id, "roleId": "verification"},
|
||||
)
|
||||
assert verification_result is not None
|
||||
assert_role_result(
|
||||
verification_result,
|
||||
role_id="verification",
|
||||
binding_id=VERIFICATION_BINDING_ID,
|
||||
payload=verification_payload,
|
||||
expected_revision=3,
|
||||
)
|
||||
client.require_no_notification(
|
||||
"thread/epiphany/stateUpdated",
|
||||
start_index=verification_result_start,
|
||||
)
|
||||
|
||||
final_read = client.send("thread/read", {"threadId": thread_id, "includeTurns": False})
|
||||
assert final_read is not None
|
||||
require(
|
||||
final_read["thread"]["epiphanyState"]["revision"] == 3,
|
||||
"role result read-back should not mutate durable state",
|
||||
)
|
||||
|
||||
result = {
|
||||
"threadId": thread_id,
|
||||
"codexHome": str(codex_home),
|
||||
"workspace": str(workspace),
|
||||
"modelingRevision": modeling_launch["revision"],
|
||||
"modelingResultStatus": modeling_result["status"],
|
||||
"modelingNextSafeMove": modeling_result["finding"]["nextSafeMove"],
|
||||
"verificationRevision": verification_launch["revision"],
|
||||
"verificationResultStatus": verification_result["status"],
|
||||
"verificationVerdict": verification_result["finding"]["verdict"],
|
||||
"finalReadRevision": final_read["thread"]["epiphanyState"]["revision"],
|
||||
}
|
||||
|
||||
result_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
result_path.write_text(
|
||||
json.dumps(result, indent=2, ensure_ascii=False) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Live-smoke explicit Phase 6 modeling/verification role launch and read-back."
|
||||
)
|
||||
parser.add_argument("--app-server", type=Path, default=DEFAULT_APP_SERVER)
|
||||
parser.add_argument("--codex-home", type=Path, default=DEFAULT_CODEX_HOME)
|
||||
parser.add_argument("--workspace", type=Path, default=DEFAULT_WORKSPACE)
|
||||
parser.add_argument("--result", type=Path, default=DEFAULT_RESULT)
|
||||
parser.add_argument("--transcript", type=Path, default=DEFAULT_TRANSCRIPT)
|
||||
parser.add_argument("--stderr", type=Path, default=DEFAULT_STDERR)
|
||||
args = parser.parse_args()
|
||||
result = run_smoke(args)
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
9
vendor/codex/codex-rs/app-server-protocol/schema/json/ClientRequest.json
generated
vendored
9
vendor/codex/codex-rs/app-server-protocol/schema/json/ClientRequest.json
generated
vendored
|
|
@ -3872,6 +3872,15 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadEpiphanyRoleId": {
|
||||
"enum": [
|
||||
"implementation",
|
||||
"modeling",
|
||||
"verification",
|
||||
"reorientation"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadEpiphanyUpdatePatch": {
|
||||
"properties": {
|
||||
"activeSubgoalId": {
|
||||
|
|
|
|||
|
|
@ -15988,6 +15988,79 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadEpiphanyRoleFinding": {
|
||||
"properties": {
|
||||
"checkpointSummary": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"evidenceIds": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"filesInspected": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"frontierNodeIds": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"itemError": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"jobError": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"nextSafeMove": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"rawResult": true,
|
||||
"roleId": {
|
||||
"$ref": "#/definitions/v2/ThreadEpiphanyRoleId"
|
||||
},
|
||||
"scratchSummary": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"verdict": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"rawResult",
|
||||
"roleId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadEpiphanyRoleId": {
|
||||
"enum": [
|
||||
"implementation",
|
||||
|
|
@ -16046,6 +16119,20 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadEpiphanyRoleResultStatus": {
|
||||
"enum": [
|
||||
"missingState",
|
||||
"missingBinding",
|
||||
"backendUnavailable",
|
||||
"backendMissing",
|
||||
"pending",
|
||||
"running",
|
||||
"completed",
|
||||
"failed",
|
||||
"cancelled"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadEpiphanyRoleStatus": {
|
||||
"enum": [
|
||||
"ready",
|
||||
|
|
@ -16173,6 +16260,8 @@
|
|||
"context",
|
||||
"jobs",
|
||||
"roles",
|
||||
"roleLaunch",
|
||||
"roleResult",
|
||||
"jobLaunch",
|
||||
"jobInterrupt",
|
||||
"freshness",
|
||||
|
|
|
|||
|
|
@ -13875,6 +13875,79 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadEpiphanyRoleFinding": {
|
||||
"properties": {
|
||||
"checkpointSummary": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"evidenceIds": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"filesInspected": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"frontierNodeIds": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"itemError": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"jobError": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"nextSafeMove": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"rawResult": true,
|
||||
"roleId": {
|
||||
"$ref": "#/definitions/ThreadEpiphanyRoleId"
|
||||
},
|
||||
"scratchSummary": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"verdict": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"rawResult",
|
||||
"roleId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadEpiphanyRoleId": {
|
||||
"enum": [
|
||||
"implementation",
|
||||
|
|
@ -13933,6 +14006,20 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadEpiphanyRoleResultStatus": {
|
||||
"enum": [
|
||||
"missingState",
|
||||
"missingBinding",
|
||||
"backendUnavailable",
|
||||
"backendMissing",
|
||||
"pending",
|
||||
"running",
|
||||
"completed",
|
||||
"failed",
|
||||
"cancelled"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadEpiphanyRoleStatus": {
|
||||
"enum": [
|
||||
"ready",
|
||||
|
|
@ -14060,6 +14147,8 @@
|
|||
"context",
|
||||
"jobs",
|
||||
"roles",
|
||||
"roleLaunch",
|
||||
"roleResult",
|
||||
"jobLaunch",
|
||||
"jobInterrupt",
|
||||
"freshness",
|
||||
|
|
|
|||
6
vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyRoleFinding.ts
generated
vendored
Normal file
6
vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyRoleFinding.ts
generated
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ThreadEpiphanyRoleId } from "./ThreadEpiphanyRoleId";
|
||||
|
||||
export type ThreadEpiphanyRoleFinding = { roleId: ThreadEpiphanyRoleId, verdict?: string | null, summary?: string | null, nextSafeMove?: string | null, checkpointSummary?: string | null, scratchSummary?: string | null, filesInspected?: Array<string>, frontierNodeIds?: Array<string>, evidenceIds?: Array<string>, jobError?: string | null, itemError?: string | null, rawResult: unknown, };
|
||||
5
vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyRoleResultStatus.ts
generated
vendored
Normal file
5
vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyRoleResultStatus.ts
generated
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ThreadEpiphanyRoleResultStatus = "missingState" | "missingBinding" | "backendUnavailable" | "backendMissing" | "pending" | "running" | "completed" | "failed" | "cancelled";
|
||||
|
|
@ -2,4 +2,4 @@
|
|||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ThreadEpiphanySceneAction = "index" | "retrieve" | "distill" | "context" | "jobs" | "roles" | "jobLaunch" | "jobInterrupt" | "freshness" | "pressure" | "reorient" | "crrc" | "reorientLaunch" | "reorientResult" | "reorientAccept" | "propose" | "promote" | "update";
|
||||
export type ThreadEpiphanySceneAction = "index" | "retrieve" | "distill" | "context" | "jobs" | "roles" | "roleLaunch" | "roleResult" | "jobLaunch" | "jobInterrupt" | "freshness" | "pressure" | "reorient" | "crrc" | "reorientLaunch" | "reorientResult" | "reorientAccept" | "propose" | "promote" | "update";
|
||||
|
|
|
|||
2
vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/index.ts
generated
vendored
2
vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/index.ts
generated
vendored
|
|
@ -355,8 +355,10 @@ export type { ThreadEpiphanyRetrieveIndexSummary } from "./ThreadEpiphanyRetriev
|
|||
export type { ThreadEpiphanyRetrieveResult } from "./ThreadEpiphanyRetrieveResult";
|
||||
export type { ThreadEpiphanyRetrieveResultKind } from "./ThreadEpiphanyRetrieveResultKind";
|
||||
export type { ThreadEpiphanyRetrieveShardSummary } from "./ThreadEpiphanyRetrieveShardSummary";
|
||||
export type { ThreadEpiphanyRoleFinding } from "./ThreadEpiphanyRoleFinding";
|
||||
export type { ThreadEpiphanyRoleId } from "./ThreadEpiphanyRoleId";
|
||||
export type { ThreadEpiphanyRoleLane } from "./ThreadEpiphanyRoleLane";
|
||||
export type { ThreadEpiphanyRoleResultStatus } from "./ThreadEpiphanyRoleResultStatus";
|
||||
export type { ThreadEpiphanyRoleStatus } from "./ThreadEpiphanyRoleStatus";
|
||||
export type { ThreadEpiphanyRolesSource } from "./ThreadEpiphanyRolesSource";
|
||||
export type { ThreadEpiphanyScene } from "./ThreadEpiphanyScene";
|
||||
|
|
|
|||
|
|
@ -351,6 +351,16 @@ client_request_definitions! {
|
|||
params: v2::ThreadEpiphanyRolesParams,
|
||||
response: v2::ThreadEpiphanyRolesResponse,
|
||||
},
|
||||
#[experimental("thread/epiphany/roleLaunch")]
|
||||
ThreadEpiphanyRoleLaunch => "thread/epiphany/roleLaunch" {
|
||||
params: v2::ThreadEpiphanyRoleLaunchParams,
|
||||
response: v2::ThreadEpiphanyRoleLaunchResponse,
|
||||
},
|
||||
#[experimental("thread/epiphany/roleResult")]
|
||||
ThreadEpiphanyRoleResult => "thread/epiphany/roleResult" {
|
||||
params: v2::ThreadEpiphanyRoleResultParams,
|
||||
response: v2::ThreadEpiphanyRoleResultResponse,
|
||||
},
|
||||
#[experimental("thread/epiphany/freshness")]
|
||||
ThreadEpiphanyFreshness => "thread/epiphany/freshness" {
|
||||
params: v2::ThreadEpiphanyFreshnessParams,
|
||||
|
|
@ -1732,6 +1742,8 @@ mod tests {
|
|||
v2::ThreadEpiphanySceneAction::Context,
|
||||
v2::ThreadEpiphanySceneAction::Jobs,
|
||||
v2::ThreadEpiphanySceneAction::Roles,
|
||||
v2::ThreadEpiphanySceneAction::RoleLaunch,
|
||||
v2::ThreadEpiphanySceneAction::RoleResult,
|
||||
v2::ThreadEpiphanySceneAction::JobLaunch,
|
||||
v2::ThreadEpiphanySceneAction::Freshness,
|
||||
v2::ThreadEpiphanySceneAction::Pressure,
|
||||
|
|
@ -1834,6 +1846,8 @@ mod tests {
|
|||
"context",
|
||||
"jobs",
|
||||
"roles",
|
||||
"roleLaunch",
|
||||
"roleResult",
|
||||
"jobLaunch",
|
||||
"freshness",
|
||||
"pressure",
|
||||
|
|
@ -1998,6 +2012,223 @@ mod tests {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_thread_epiphany_role_launch_response() -> Result<()> {
|
||||
let response = ClientResponse::ThreadEpiphanyRoleLaunch {
|
||||
request_id: RequestId::Integer(8),
|
||||
response: v2::ThreadEpiphanyRoleLaunchResponse {
|
||||
thread_id: "thr_123".to_string(),
|
||||
role_id: v2::ThreadEpiphanyRoleId::Modeling,
|
||||
revision: 4,
|
||||
changed_fields: vec![v2::ThreadEpiphanyStateUpdatedField::JobBindings],
|
||||
epiphany_state: codex_protocol::protocol::EpiphanyThreadState {
|
||||
revision: 4,
|
||||
job_bindings: vec![codex_protocol::protocol::EpiphanyJobBinding {
|
||||
id: "modeling-checkpoint-worker".to_string(),
|
||||
kind: codex_protocol::protocol::EpiphanyJobKind::Specialist,
|
||||
scope: "role-scoped modeling/checkpoint maintenance".to_string(),
|
||||
owner_role: "epiphany-modeler".to_string(),
|
||||
launcher_job_id: Some("epiphany-launch-1".to_string()),
|
||||
authority_scope: Some("epiphany.role.modeling".to_string()),
|
||||
backend_kind: Some(
|
||||
codex_protocol::protocol::EpiphanyJobBackendKind::AgentJobs,
|
||||
),
|
||||
backend_job_id: Some("backend-1".to_string()),
|
||||
runtime_agent_job_id: Some("backend-1".to_string()),
|
||||
linked_subgoal_ids: vec!["phase-6".to_string()],
|
||||
linked_graph_node_ids: vec!["state-spine".to_string()],
|
||||
progress_note: Some(
|
||||
"Explicitly launched through the Epiphany authority surface onto the agent_jobs backend."
|
||||
.to_string(),
|
||||
),
|
||||
blocking_reason: None,
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
job: v2::ThreadEpiphanyJob {
|
||||
id: "modeling-checkpoint-worker".to_string(),
|
||||
kind: v2::ThreadEpiphanyJobKind::Specialist,
|
||||
scope: "role-scoped modeling/checkpoint maintenance".to_string(),
|
||||
owner_role: "epiphany-modeler".to_string(),
|
||||
launcher_job_id: Some("epiphany-launch-1".to_string()),
|
||||
authority_scope: Some("epiphany.role.modeling".to_string()),
|
||||
backend_kind: Some(v2::ThreadEpiphanyJobBackendKind::AgentJobs),
|
||||
backend_job_id: Some("backend-1".to_string()),
|
||||
status: v2::ThreadEpiphanyJobStatus::Pending,
|
||||
runtime_agent_job_id: Some("backend-1".to_string()),
|
||||
items_processed: None,
|
||||
items_total: None,
|
||||
progress_note: Some(
|
||||
"Explicitly launched through the Epiphany authority surface onto the agent_jobs backend."
|
||||
.to_string(),
|
||||
),
|
||||
last_checkpoint_at_unix_seconds: None,
|
||||
blocking_reason: None,
|
||||
active_thread_ids: Vec::new(),
|
||||
linked_subgoal_ids: vec!["phase-6".to_string()],
|
||||
linked_graph_node_ids: vec!["state-spine".to_string()],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
assert_eq!(response.id(), &RequestId::Integer(8));
|
||||
assert_eq!(response.method(), "thread/epiphany/roleLaunch");
|
||||
assert_eq!(
|
||||
json!({
|
||||
"method": "thread/epiphany/roleLaunch",
|
||||
"id": 8,
|
||||
"response": {
|
||||
"threadId": "thr_123",
|
||||
"roleId": "modeling",
|
||||
"revision": 4,
|
||||
"changedFields": ["jobBindings"],
|
||||
"epiphanyState": {
|
||||
"revision": 4,
|
||||
"job_bindings": [{
|
||||
"id": "modeling-checkpoint-worker",
|
||||
"kind": "specialist",
|
||||
"scope": "role-scoped modeling/checkpoint maintenance",
|
||||
"owner_role": "epiphany-modeler",
|
||||
"launcher_job_id": "epiphany-launch-1",
|
||||
"authority_scope": "epiphany.role.modeling",
|
||||
"backend_kind": "agent_jobs",
|
||||
"backend_job_id": "backend-1",
|
||||
"runtime_agent_job_id": "backend-1",
|
||||
"linked_subgoal_ids": ["phase-6"],
|
||||
"linked_graph_node_ids": ["state-spine"],
|
||||
"progress_note": "Explicitly launched through the Epiphany authority surface onto the agent_jobs backend."
|
||||
}]
|
||||
},
|
||||
"job": {
|
||||
"id": "modeling-checkpoint-worker",
|
||||
"kind": "specialist",
|
||||
"scope": "role-scoped modeling/checkpoint maintenance",
|
||||
"ownerRole": "epiphany-modeler",
|
||||
"launcherJobId": "epiphany-launch-1",
|
||||
"authorityScope": "epiphany.role.modeling",
|
||||
"backendKind": "agentJobs",
|
||||
"backendJobId": "backend-1",
|
||||
"status": "pending",
|
||||
"runtimeAgentJobId": "backend-1",
|
||||
"progressNote": "Explicitly launched through the Epiphany authority surface onto the agent_jobs backend.",
|
||||
"linkedSubgoalIds": ["phase-6"],
|
||||
"linkedGraphNodeIds": ["state-spine"]
|
||||
}
|
||||
}
|
||||
}),
|
||||
serde_json::to_value(&response)?,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_thread_epiphany_role_result_response() -> Result<()> {
|
||||
let response = ClientResponse::ThreadEpiphanyRoleResult {
|
||||
request_id: RequestId::Integer(9),
|
||||
response: v2::ThreadEpiphanyRoleResultResponse {
|
||||
thread_id: "thr_123".to_string(),
|
||||
role_id: v2::ThreadEpiphanyRoleId::Verification,
|
||||
source: v2::ThreadEpiphanyRolesSource::Live,
|
||||
state_status: v2::ThreadEpiphanyReorientStateStatus::Ready,
|
||||
state_revision: Some(4),
|
||||
binding_id: "verification-review-worker".to_string(),
|
||||
status: v2::ThreadEpiphanyRoleResultStatus::Completed,
|
||||
job: Some(v2::ThreadEpiphanyJob {
|
||||
id: "verification-review-worker".to_string(),
|
||||
kind: v2::ThreadEpiphanyJobKind::Specialist,
|
||||
scope: "role-scoped verification/review".to_string(),
|
||||
owner_role: "epiphany-verifier".to_string(),
|
||||
launcher_job_id: Some("epiphany-launch-2".to_string()),
|
||||
authority_scope: Some("epiphany.role.verification".to_string()),
|
||||
backend_kind: Some(v2::ThreadEpiphanyJobBackendKind::AgentJobs),
|
||||
backend_job_id: Some("backend-2".to_string()),
|
||||
status: v2::ThreadEpiphanyJobStatus::Completed,
|
||||
runtime_agent_job_id: Some("backend-2".to_string()),
|
||||
items_processed: Some(1),
|
||||
items_total: Some(1),
|
||||
progress_note: Some("1/1 items completed.".to_string()),
|
||||
last_checkpoint_at_unix_seconds: None,
|
||||
blocking_reason: None,
|
||||
active_thread_ids: Vec::new(),
|
||||
linked_subgoal_ids: vec!["phase-6".to_string()],
|
||||
linked_graph_node_ids: vec!["state-spine".to_string()],
|
||||
}),
|
||||
finding: Some(v2::ThreadEpiphanyRoleFinding {
|
||||
role_id: v2::ThreadEpiphanyRoleId::Verification,
|
||||
verdict: Some("pass".to_string()),
|
||||
summary: Some("Evidence covers the bounded change.".to_string()),
|
||||
next_safe_move: Some("Promote the verified patch.".to_string()),
|
||||
checkpoint_summary: None,
|
||||
scratch_summary: None,
|
||||
files_inspected: vec!["src/lib.rs".to_string()],
|
||||
frontier_node_ids: vec!["state-spine".to_string()],
|
||||
evidence_ids: vec!["ev-1".to_string()],
|
||||
job_error: None,
|
||||
item_error: None,
|
||||
raw_result: json!({
|
||||
"verdict": "pass",
|
||||
"summary": "Evidence covers the bounded change.",
|
||||
"nextSafeMove": "Promote the verified patch."
|
||||
}),
|
||||
}),
|
||||
note: "Verification role specialist completed. Next safe move: Promote the verified patch."
|
||||
.to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
assert_eq!(response.id(), &RequestId::Integer(9));
|
||||
assert_eq!(response.method(), "thread/epiphany/roleResult");
|
||||
assert_eq!(
|
||||
json!({
|
||||
"method": "thread/epiphany/roleResult",
|
||||
"id": 9,
|
||||
"response": {
|
||||
"threadId": "thr_123",
|
||||
"roleId": "verification",
|
||||
"source": "live",
|
||||
"stateStatus": "ready",
|
||||
"stateRevision": 4,
|
||||
"bindingId": "verification-review-worker",
|
||||
"status": "completed",
|
||||
"job": {
|
||||
"id": "verification-review-worker",
|
||||
"kind": "specialist",
|
||||
"scope": "role-scoped verification/review",
|
||||
"ownerRole": "epiphany-verifier",
|
||||
"launcherJobId": "epiphany-launch-2",
|
||||
"authorityScope": "epiphany.role.verification",
|
||||
"backendKind": "agentJobs",
|
||||
"backendJobId": "backend-2",
|
||||
"status": "completed",
|
||||
"runtimeAgentJobId": "backend-2",
|
||||
"itemsProcessed": 1,
|
||||
"itemsTotal": 1,
|
||||
"progressNote": "1/1 items completed.",
|
||||
"linkedSubgoalIds": ["phase-6"],
|
||||
"linkedGraphNodeIds": ["state-spine"]
|
||||
},
|
||||
"finding": {
|
||||
"roleId": "verification",
|
||||
"verdict": "pass",
|
||||
"summary": "Evidence covers the bounded change.",
|
||||
"nextSafeMove": "Promote the verified patch.",
|
||||
"filesInspected": ["src/lib.rs"],
|
||||
"frontierNodeIds": ["state-spine"],
|
||||
"evidenceIds": ["ev-1"],
|
||||
"rawResult": {
|
||||
"verdict": "pass",
|
||||
"summary": "Evidence covers the bounded change.",
|
||||
"nextSafeMove": "Promote the verified patch."
|
||||
}
|
||||
},
|
||||
"note": "Verification role specialist completed. Next safe move: Promote the verified patch."
|
||||
}
|
||||
}),
|
||||
serde_json::to_value(&response)?,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_thread_epiphany_freshness_response() -> Result<()> {
|
||||
let response = ClientResponse::ThreadEpiphanyFreshness {
|
||||
|
|
@ -3980,6 +4211,35 @@ mod tests {
|
|||
assert_eq!(reason, Some("thread/epiphany/roles"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_epiphany_role_launch_is_marked_experimental() {
|
||||
let request = ClientRequest::ThreadEpiphanyRoleLaunch {
|
||||
request_id: RequestId::Integer(1),
|
||||
params: v2::ThreadEpiphanyRoleLaunchParams {
|
||||
thread_id: "thr_123".to_string(),
|
||||
role_id: v2::ThreadEpiphanyRoleId::Modeling,
|
||||
expected_revision: Some(1),
|
||||
max_runtime_seconds: Some(60),
|
||||
},
|
||||
};
|
||||
let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&request);
|
||||
assert_eq!(reason, Some("thread/epiphany/roleLaunch"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_epiphany_role_result_is_marked_experimental() {
|
||||
let request = ClientRequest::ThreadEpiphanyRoleResult {
|
||||
request_id: RequestId::Integer(1),
|
||||
params: v2::ThreadEpiphanyRoleResultParams {
|
||||
thread_id: "thr_123".to_string(),
|
||||
role_id: v2::ThreadEpiphanyRoleId::Verification,
|
||||
binding_id: None,
|
||||
},
|
||||
};
|
||||
let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&request);
|
||||
assert_eq!(reason, Some("thread/epiphany/roleResult"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_epiphany_freshness_is_marked_experimental() {
|
||||
let request = ClientRequest::ThreadEpiphanyFreshness {
|
||||
|
|
|
|||
|
|
@ -3998,6 +3998,8 @@ pub enum ThreadEpiphanySceneAction {
|
|||
Context,
|
||||
Jobs,
|
||||
Roles,
|
||||
RoleLaunch,
|
||||
RoleResult,
|
||||
JobLaunch,
|
||||
JobInterrupt,
|
||||
Freshness,
|
||||
|
|
@ -4227,6 +4229,116 @@ pub struct ThreadEpiphanyRoleLane {
|
|||
pub recommended_action: Option<ThreadEpiphanySceneAction>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadEpiphanyRoleLaunchParams {
|
||||
pub thread_id: String,
|
||||
pub role_id: ThreadEpiphanyRoleId,
|
||||
#[serde(default)]
|
||||
#[ts(optional = nullable)]
|
||||
pub expected_revision: Option<u64>,
|
||||
#[serde(default)]
|
||||
#[ts(optional = nullable)]
|
||||
pub max_runtime_seconds: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadEpiphanyRoleLaunchResponse {
|
||||
pub thread_id: String,
|
||||
pub role_id: ThreadEpiphanyRoleId,
|
||||
pub revision: u64,
|
||||
pub changed_fields: Vec<ThreadEpiphanyStateUpdatedField>,
|
||||
pub epiphany_state: CoreEpiphanyThreadState,
|
||||
pub job: ThreadEpiphanyJob,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadEpiphanyRoleResultParams {
|
||||
pub thread_id: String,
|
||||
pub role_id: ThreadEpiphanyRoleId,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional = nullable)]
|
||||
pub binding_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadEpiphanyRoleResultResponse {
|
||||
pub thread_id: String,
|
||||
pub role_id: ThreadEpiphanyRoleId,
|
||||
pub source: ThreadEpiphanyRolesSource,
|
||||
pub state_status: ThreadEpiphanyReorientStateStatus,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional = nullable)]
|
||||
pub state_revision: Option<u64>,
|
||||
pub binding_id: String,
|
||||
pub status: ThreadEpiphanyRoleResultStatus,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional = nullable)]
|
||||
pub job: Option<ThreadEpiphanyJob>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional = nullable)]
|
||||
pub finding: Option<ThreadEpiphanyRoleFinding>,
|
||||
pub note: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum ThreadEpiphanyRoleResultStatus {
|
||||
MissingState,
|
||||
MissingBinding,
|
||||
BackendUnavailable,
|
||||
BackendMissing,
|
||||
Pending,
|
||||
Running,
|
||||
Completed,
|
||||
Failed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadEpiphanyRoleFinding {
|
||||
pub role_id: ThreadEpiphanyRoleId,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional = nullable)]
|
||||
pub verdict: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional = nullable)]
|
||||
pub summary: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional = nullable)]
|
||||
pub next_safe_move: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional = nullable)]
|
||||
pub checkpoint_summary: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional = nullable)]
|
||||
pub scratch_summary: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub files_inspected: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub frontier_node_ids: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub evidence_ids: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional = nullable)]
|
||||
pub job_error: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional = nullable)]
|
||||
pub item_error: Option<String>,
|
||||
#[ts(type = "unknown")]
|
||||
pub raw_result: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
|
|
|||
|
|
@ -218,8 +218,14 @@ use codex_app_server_protocol::ThreadEpiphanyRetrieveResponse;
|
|||
use codex_app_server_protocol::ThreadEpiphanyRetrieveResult;
|
||||
use codex_app_server_protocol::ThreadEpiphanyRetrieveResultKind;
|
||||
use codex_app_server_protocol::ThreadEpiphanyRetrieveShardSummary;
|
||||
use codex_app_server_protocol::ThreadEpiphanyRoleFinding;
|
||||
use codex_app_server_protocol::ThreadEpiphanyRoleId;
|
||||
use codex_app_server_protocol::ThreadEpiphanyRoleLane;
|
||||
use codex_app_server_protocol::ThreadEpiphanyRoleLaunchParams;
|
||||
use codex_app_server_protocol::ThreadEpiphanyRoleLaunchResponse;
|
||||
use codex_app_server_protocol::ThreadEpiphanyRoleResultParams;
|
||||
use codex_app_server_protocol::ThreadEpiphanyRoleResultResponse;
|
||||
use codex_app_server_protocol::ThreadEpiphanyRoleResultStatus;
|
||||
use codex_app_server_protocol::ThreadEpiphanyRoleStatus;
|
||||
use codex_app_server_protocol::ThreadEpiphanyRolesParams;
|
||||
use codex_app_server_protocol::ThreadEpiphanyRolesResponse;
|
||||
|
|
@ -1096,6 +1102,14 @@ impl CodexMessageProcessor {
|
|||
self.thread_epiphany_roles(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::ThreadEpiphanyRoleLaunch { request_id, params } => {
|
||||
self.thread_epiphany_role_launch(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::ThreadEpiphanyRoleResult { request_id, params } => {
|
||||
self.thread_epiphany_role_result(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::ThreadEpiphanyFreshness { request_id, params } => {
|
||||
self.thread_epiphany_freshness(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
|
|
@ -4448,6 +4462,251 @@ impl CodexMessageProcessor {
|
|||
self.outgoing.send_response(request_id, response).await;
|
||||
}
|
||||
|
||||
async fn thread_epiphany_role_launch(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadEpiphanyRoleLaunchParams,
|
||||
) {
|
||||
let ThreadEpiphanyRoleLaunchParams {
|
||||
thread_id,
|
||||
role_id,
|
||||
expected_revision,
|
||||
max_runtime_seconds,
|
||||
} = params;
|
||||
|
||||
let thread_uuid = match ThreadId::from_string(&thread_id) {
|
||||
Ok(id) => id,
|
||||
Err(err) => {
|
||||
self.send_invalid_request_error(request_id, format!("invalid thread id: {err}"))
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let loaded_thread = match self.thread_manager.get_thread(thread_uuid).await {
|
||||
Ok(thread) => thread,
|
||||
Err(_) => {
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
format!("thread not loaded: {thread_uuid}"),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let state = match loaded_thread.epiphany_state().await {
|
||||
Some(state) => state,
|
||||
None => {
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
"cannot launch an Epiphany role specialist without authoritative Epiphany state"
|
||||
.to_string(),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let launch_request = match build_epiphany_role_launch_request(
|
||||
&thread_id,
|
||||
role_id,
|
||||
expected_revision,
|
||||
max_runtime_seconds,
|
||||
&state,
|
||||
) {
|
||||
Ok(request) => request,
|
||||
Err(message) => {
|
||||
self.send_invalid_request_error(request_id, message).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let binding_id = launch_request.binding_id.clone();
|
||||
let changed_fields = vec![ThreadEpiphanyStateUpdatedField::JobBindings];
|
||||
let launched = match loaded_thread.epiphany_launch_job(launch_request).await {
|
||||
Ok(launched) => launched,
|
||||
Err(CodexErr::InvalidRequest(message)) => {
|
||||
self.send_invalid_request_error(request_id, message).await;
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
self.send_internal_error(
|
||||
request_id,
|
||||
format!("failed to launch Epiphany role specialist for {thread_uuid}: {err}"),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let epiphany_state = client_visible_live_thread_epiphany_state(
|
||||
loaded_thread.as_ref(),
|
||||
launched.epiphany_state,
|
||||
)
|
||||
.await;
|
||||
let state_db_ctx = loaded_thread.epiphany_state_runtime().await;
|
||||
let job_resolution =
|
||||
load_epiphany_job_launcher_snapshots(state_db_ctx.as_ref(), Some(&epiphany_state))
|
||||
.await;
|
||||
let job = map_epiphany_jobs(Some(&epiphany_state), None, &job_resolution)
|
||||
.into_iter()
|
||||
.find(|job| job.id == binding_id)
|
||||
.unwrap_or_else(|| ThreadEpiphanyJob {
|
||||
id: launched.binding_id.clone(),
|
||||
kind: ThreadEpiphanyJobKind::Specialist,
|
||||
scope: "missing launched role projection".to_string(),
|
||||
owner_role: "epiphany-harness".to_string(),
|
||||
launcher_job_id: Some(launched.launcher_job_id.clone()),
|
||||
authority_scope: None,
|
||||
backend_kind: Some(ThreadEpiphanyJobBackendKind::AgentJobs),
|
||||
backend_job_id: Some(launched.backend_job_id.clone()),
|
||||
status: ThreadEpiphanyJobStatus::Pending,
|
||||
runtime_agent_job_id: Some(launched.backend_job_id.clone()),
|
||||
items_processed: None,
|
||||
items_total: None,
|
||||
progress_note: None,
|
||||
last_checkpoint_at_unix_seconds: None,
|
||||
blocking_reason: None,
|
||||
active_thread_ids: Vec::new(),
|
||||
linked_subgoal_ids: Vec::new(),
|
||||
linked_graph_node_ids: Vec::new(),
|
||||
});
|
||||
|
||||
self.outgoing
|
||||
.send_response(
|
||||
request_id,
|
||||
ThreadEpiphanyRoleLaunchResponse {
|
||||
thread_id: thread_uuid.to_string(),
|
||||
role_id,
|
||||
revision: epiphany_state.revision,
|
||||
changed_fields: changed_fields.clone(),
|
||||
epiphany_state: epiphany_state.clone(),
|
||||
job,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
self.outgoing
|
||||
.send_server_notification(ServerNotification::ThreadEpiphanyStateUpdated(
|
||||
ThreadEpiphanyStateUpdatedNotification {
|
||||
thread_id: thread_uuid.to_string(),
|
||||
source: ThreadEpiphanyStateUpdatedSource::JobLaunch,
|
||||
revision: epiphany_state.revision,
|
||||
changed_fields,
|
||||
epiphany_state,
|
||||
},
|
||||
))
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn thread_epiphany_role_result(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadEpiphanyRoleResultParams,
|
||||
) {
|
||||
let ThreadEpiphanyRoleResultParams {
|
||||
thread_id,
|
||||
role_id,
|
||||
binding_id,
|
||||
} = params;
|
||||
|
||||
let binding_id = match binding_id {
|
||||
Some(binding_id) => binding_id,
|
||||
None => match epiphany_role_binding_id(role_id) {
|
||||
Ok(binding_id) => binding_id.to_string(),
|
||||
Err(message) => {
|
||||
self.send_invalid_request_error(request_id, message).await;
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let thread_uuid = match ThreadId::from_string(&thread_id) {
|
||||
Ok(id) => id,
|
||||
Err(err) => {
|
||||
self.send_invalid_request_error(request_id, format!("invalid thread id: {err}"))
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let loaded_thread = self.thread_manager.get_thread(thread_uuid).await.ok();
|
||||
let thread = match self.read_thread_view(thread_uuid, false).await {
|
||||
Ok(thread) => thread,
|
||||
Err(ThreadReadViewError::InvalidRequest(message)) => {
|
||||
self.send_invalid_request_error(request_id, message).await;
|
||||
return;
|
||||
}
|
||||
Err(ThreadReadViewError::Internal(message)) => {
|
||||
self.send_internal_error(request_id, message).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let source = if loaded_thread.is_some() {
|
||||
ThreadEpiphanyRolesSource::Live
|
||||
} else {
|
||||
ThreadEpiphanyRolesSource::Stored
|
||||
};
|
||||
let Some(state) = thread.epiphany_state.as_ref() else {
|
||||
self.outgoing
|
||||
.send_response(
|
||||
request_id,
|
||||
ThreadEpiphanyRoleResultResponse {
|
||||
thread_id: thread_uuid.to_string(),
|
||||
role_id,
|
||||
source,
|
||||
state_status: ThreadEpiphanyReorientStateStatus::Missing,
|
||||
state_revision: None,
|
||||
binding_id,
|
||||
status: ThreadEpiphanyRoleResultStatus::MissingState,
|
||||
job: None,
|
||||
finding: None,
|
||||
note: "No authoritative Epiphany state exists for this thread.".to_string(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(message) = epiphany_role_binding_id(role_id) {
|
||||
self.send_invalid_request_error(request_id, message).await;
|
||||
return;
|
||||
}
|
||||
|
||||
let mut state_db_ctx = if let Some(loaded_thread) = loaded_thread.as_ref() {
|
||||
loaded_thread.epiphany_state_runtime().await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if state_db_ctx.is_none() {
|
||||
state_db_ctx = open_state_db_for_direct_thread_lookup(&self.config).await;
|
||||
}
|
||||
let job_resolution =
|
||||
load_epiphany_job_launcher_snapshots(state_db_ctx.as_ref(), Some(state)).await;
|
||||
let job = map_epiphany_jobs(Some(state), None, &job_resolution)
|
||||
.into_iter()
|
||||
.find(|job| job.id == binding_id);
|
||||
let (status, finding, note) =
|
||||
load_epiphany_role_result_snapshot(state, state_db_ctx.as_ref(), role_id, &binding_id)
|
||||
.await;
|
||||
|
||||
self.outgoing
|
||||
.send_response(
|
||||
request_id,
|
||||
ThreadEpiphanyRoleResultResponse {
|
||||
thread_id: thread_uuid.to_string(),
|
||||
role_id,
|
||||
source,
|
||||
state_status: ThreadEpiphanyReorientStateStatus::Ready,
|
||||
state_revision: Some(state.revision),
|
||||
binding_id,
|
||||
status,
|
||||
job,
|
||||
finding,
|
||||
note,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn thread_epiphany_freshness(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
|
|
@ -12193,6 +12452,10 @@ fn map_epiphany_scene(state: Option<&EpiphanyThreadState>, loaded: bool) -> Thre
|
|||
}
|
||||
|
||||
const EPIPHANY_SCENE_RECORD_LIMIT: usize = 5;
|
||||
const EPIPHANY_MODELING_ROLE_BINDING_ID: &str = "modeling-checkpoint-worker";
|
||||
const EPIPHANY_MODELING_OWNER_ROLE: &str = "epiphany-modeler";
|
||||
const EPIPHANY_VERIFICATION_ROLE_BINDING_ID: &str = "verification-review-worker";
|
||||
const EPIPHANY_VERIFICATION_OWNER_ROLE: &str = "epiphany-verifier";
|
||||
const EPIPHANY_REORIENT_LAUNCH_BINDING_ID: &str = "reorient-worker";
|
||||
const EPIPHANY_REORIENT_OWNER_ROLE: &str = "epiphany-reorient";
|
||||
|
||||
|
|
@ -12213,6 +12476,8 @@ fn epiphany_scene_available_actions(
|
|||
ThreadEpiphanySceneAction::Context,
|
||||
ThreadEpiphanySceneAction::Jobs,
|
||||
ThreadEpiphanySceneAction::Roles,
|
||||
ThreadEpiphanySceneAction::RoleLaunch,
|
||||
ThreadEpiphanySceneAction::RoleResult,
|
||||
ThreadEpiphanySceneAction::JobLaunch,
|
||||
ThreadEpiphanySceneAction::Freshness,
|
||||
ThreadEpiphanySceneAction::Pressure,
|
||||
|
|
@ -12235,6 +12500,162 @@ fn epiphany_scene_available_actions(
|
|||
actions
|
||||
}
|
||||
|
||||
fn epiphany_role_binding_id(role_id: ThreadEpiphanyRoleId) -> Result<&'static str, String> {
|
||||
match role_id {
|
||||
ThreadEpiphanyRoleId::Modeling => Ok(EPIPHANY_MODELING_ROLE_BINDING_ID),
|
||||
ThreadEpiphanyRoleId::Verification => Ok(EPIPHANY_VERIFICATION_ROLE_BINDING_ID),
|
||||
ThreadEpiphanyRoleId::Implementation => Err(
|
||||
"implementation is owned by the main coding agent; no role specialist launch template exists"
|
||||
.to_string(),
|
||||
),
|
||||
ThreadEpiphanyRoleId::Reorientation => Err(
|
||||
"reorientation uses thread/epiphany/reorientLaunch and thread/epiphany/reorientResult"
|
||||
.to_string(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn epiphany_role_owner(role_id: ThreadEpiphanyRoleId) -> Result<&'static str, String> {
|
||||
match role_id {
|
||||
ThreadEpiphanyRoleId::Modeling => Ok(EPIPHANY_MODELING_OWNER_ROLE),
|
||||
ThreadEpiphanyRoleId::Verification => Ok(EPIPHANY_VERIFICATION_OWNER_ROLE),
|
||||
ThreadEpiphanyRoleId::Implementation | ThreadEpiphanyRoleId::Reorientation => {
|
||||
Err(epiphany_role_binding_id(role_id).unwrap_err())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn epiphany_role_label(role_id: ThreadEpiphanyRoleId) -> &'static str {
|
||||
match role_id {
|
||||
ThreadEpiphanyRoleId::Implementation => "implementation",
|
||||
ThreadEpiphanyRoleId::Modeling => "modeling",
|
||||
ThreadEpiphanyRoleId::Verification => "verification",
|
||||
ThreadEpiphanyRoleId::Reorientation => "reorientation",
|
||||
}
|
||||
}
|
||||
|
||||
fn build_epiphany_role_launch_request(
|
||||
thread_id: &str,
|
||||
role_id: ThreadEpiphanyRoleId,
|
||||
expected_revision: Option<u64>,
|
||||
max_runtime_seconds: Option<u64>,
|
||||
state: &EpiphanyThreadState,
|
||||
) -> Result<EpiphanyJobLaunchRequest, String> {
|
||||
let binding_id = epiphany_role_binding_id(role_id)?;
|
||||
let owner_role = epiphany_role_owner(role_id)?;
|
||||
let linked_subgoal_ids = epiphany_active_subgoal_ids(Some(state));
|
||||
let linked_graph_node_ids = epiphany_active_graph_node_ids(Some(state));
|
||||
let (scope, authority_scope, instruction, output_schema_json) = match role_id {
|
||||
ThreadEpiphanyRoleId::Modeling => (
|
||||
"role-scoped modeling/checkpoint maintenance",
|
||||
"epiphany.role.modeling",
|
||||
build_epiphany_role_launch_instruction(role_id),
|
||||
epiphany_role_launch_output_schema(role_id),
|
||||
),
|
||||
ThreadEpiphanyRoleId::Verification => (
|
||||
"role-scoped verification/review",
|
||||
"epiphany.role.verification",
|
||||
build_epiphany_role_launch_instruction(role_id),
|
||||
epiphany_role_launch_output_schema(role_id),
|
||||
),
|
||||
ThreadEpiphanyRoleId::Implementation | ThreadEpiphanyRoleId::Reorientation => {
|
||||
return Err(epiphany_role_binding_id(role_id).unwrap_err());
|
||||
}
|
||||
};
|
||||
let input_json = serde_json::json!({
|
||||
"threadId": thread_id,
|
||||
"roleId": epiphany_role_label(role_id),
|
||||
"stateRevision": state.revision,
|
||||
"objective": &state.objective,
|
||||
"activeSubgoalId": &state.active_subgoal_id,
|
||||
"activeSubgoals": state
|
||||
.subgoals
|
||||
.iter()
|
||||
.filter(|subgoal| Some(subgoal.id.as_str()) == state.active_subgoal_id.as_deref())
|
||||
.collect::<Vec<_>>(),
|
||||
"activeGraphNodeIds": linked_graph_node_ids,
|
||||
"investigationCheckpoint": &state.investigation_checkpoint,
|
||||
"scratch": &state.scratch,
|
||||
"invariants": &state.invariants,
|
||||
"recentEvidence": state.recent_evidence.iter().take(8).collect::<Vec<_>>(),
|
||||
"graphFrontier": &state.graph_frontier,
|
||||
"graphCheckpoint": &state.graph_checkpoint,
|
||||
"churn": &state.churn,
|
||||
});
|
||||
|
||||
Ok(EpiphanyJobLaunchRequest {
|
||||
expected_revision,
|
||||
binding_id: binding_id.to_string(),
|
||||
kind: CoreEpiphanyJobKind::Specialist,
|
||||
scope: scope.to_string(),
|
||||
owner_role: owner_role.to_string(),
|
||||
authority_scope: authority_scope.to_string(),
|
||||
linked_subgoal_ids,
|
||||
linked_graph_node_ids: epiphany_active_graph_node_ids(Some(state)),
|
||||
instruction,
|
||||
input_json,
|
||||
output_schema_json: Some(output_schema_json),
|
||||
max_runtime_seconds,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_epiphany_role_launch_instruction(role_id: ThreadEpiphanyRoleId) -> String {
|
||||
match role_id {
|
||||
ThreadEpiphanyRoleId::Modeling => "Act as the Epiphany modeling/checkpoint specialist for one bounded pass. Inspect the supplied objective, scratch, frontier, map, evidence, and checkpoint. Return a compact structured result that says whether the checkpoint/map should be updated, what source files you inspected, and the next safe modeling move. Do not edit files, do not implement product code, and do not promote your own output.".to_string(),
|
||||
ThreadEpiphanyRoleId::Verification => "Act as the Epiphany verification/review specialist for one bounded pass. Review the supplied objective, invariants, recent evidence, worker findings, and active frontier. Return a compact structured result with an honest verdict, missing coverage, inspected files, and the next safe review move. Do not edit files, do not accept or promote your own output, and do not expand scope beyond review.".to_string(),
|
||||
ThreadEpiphanyRoleId::Implementation | ThreadEpiphanyRoleId::Reorientation => {
|
||||
"Unsupported Epiphany role specialist template.".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn epiphany_role_launch_output_schema(role_id: ThreadEpiphanyRoleId) -> serde_json::Value {
|
||||
let verdict_enum = match role_id {
|
||||
ThreadEpiphanyRoleId::Modeling => {
|
||||
vec![
|
||||
"checkpoint-ready",
|
||||
"checkpoint-update-needed",
|
||||
"regather-needed",
|
||||
]
|
||||
}
|
||||
ThreadEpiphanyRoleId::Verification => {
|
||||
vec!["pass", "needs-review", "needs-evidence", "fail"]
|
||||
}
|
||||
ThreadEpiphanyRoleId::Implementation | ThreadEpiphanyRoleId::Reorientation => vec![],
|
||||
};
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"roleId": {
|
||||
"type": "string",
|
||||
"enum": [epiphany_role_label(role_id)]
|
||||
},
|
||||
"verdict": {
|
||||
"type": "string",
|
||||
"enum": verdict_enum
|
||||
},
|
||||
"summary": {"type": "string"},
|
||||
"nextSafeMove": {"type": "string"},
|
||||
"checkpointSummary": {"type": "string"},
|
||||
"scratchSummary": {"type": "string"},
|
||||
"filesInspected": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"}
|
||||
},
|
||||
"frontierNodeIds": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"}
|
||||
},
|
||||
"evidenceIds": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"required": ["summary", "nextSafeMove"],
|
||||
"additionalProperties": true
|
||||
})
|
||||
}
|
||||
|
||||
fn build_epiphany_reorient_launch_request(
|
||||
thread_id: &str,
|
||||
expected_revision: Option<u64>,
|
||||
|
|
@ -12376,6 +12797,23 @@ fn map_epiphany_reorient_result_status(
|
|||
}
|
||||
}
|
||||
|
||||
fn map_epiphany_role_result_status(
|
||||
job_status: AgentJobStatus,
|
||||
item_status: AgentJobItemStatus,
|
||||
) -> ThreadEpiphanyRoleResultStatus {
|
||||
match job_status {
|
||||
AgentJobStatus::Cancelled => ThreadEpiphanyRoleResultStatus::Cancelled,
|
||||
AgentJobStatus::Failed => ThreadEpiphanyRoleResultStatus::Failed,
|
||||
AgentJobStatus::Pending => ThreadEpiphanyRoleResultStatus::Pending,
|
||||
AgentJobStatus::Running | AgentJobStatus::Completed => match item_status {
|
||||
AgentJobItemStatus::Pending => ThreadEpiphanyRoleResultStatus::Pending,
|
||||
AgentJobItemStatus::Running => ThreadEpiphanyRoleResultStatus::Running,
|
||||
AgentJobItemStatus::Completed => ThreadEpiphanyRoleResultStatus::Completed,
|
||||
AgentJobItemStatus::Failed => ThreadEpiphanyRoleResultStatus::Failed,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn map_epiphany_reorient_finding(
|
||||
raw_result: serde_json::Value,
|
||||
job_error: Option<String>,
|
||||
|
|
@ -12397,6 +12835,28 @@ fn map_epiphany_reorient_finding(
|
|||
}
|
||||
}
|
||||
|
||||
fn map_epiphany_role_finding(
|
||||
role_id: ThreadEpiphanyRoleId,
|
||||
raw_result: serde_json::Value,
|
||||
job_error: Option<String>,
|
||||
item_error: Option<String>,
|
||||
) -> ThreadEpiphanyRoleFinding {
|
||||
ThreadEpiphanyRoleFinding {
|
||||
role_id,
|
||||
verdict: json_string_field(&raw_result, "verdict"),
|
||||
summary: json_string_field(&raw_result, "summary"),
|
||||
next_safe_move: json_string_field(&raw_result, "nextSafeMove"),
|
||||
checkpoint_summary: json_string_field(&raw_result, "checkpointSummary"),
|
||||
scratch_summary: json_string_field(&raw_result, "scratchSummary"),
|
||||
files_inspected: json_string_array_field(&raw_result, "filesInspected"),
|
||||
frontier_node_ids: json_string_array_field(&raw_result, "frontierNodeIds"),
|
||||
evidence_ids: json_string_array_field(&raw_result, "evidenceIds"),
|
||||
job_error,
|
||||
item_error,
|
||||
raw_result,
|
||||
}
|
||||
}
|
||||
|
||||
fn json_string_field(value: &serde_json::Value, key: &str) -> Option<String> {
|
||||
value
|
||||
.get(key)
|
||||
|
|
@ -12418,6 +12878,60 @@ fn json_string_array_field(value: &serde_json::Value, key: &str) -> Vec<String>
|
|||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn render_epiphany_role_result_note(
|
||||
role_id: ThreadEpiphanyRoleId,
|
||||
status: ThreadEpiphanyRoleResultStatus,
|
||||
finding: Option<&ThreadEpiphanyRoleFinding>,
|
||||
item_error: Option<&str>,
|
||||
) -> String {
|
||||
match status {
|
||||
ThreadEpiphanyRoleResultStatus::Completed => {
|
||||
if let Some(finding) = finding {
|
||||
let next = finding.next_safe_move.as_deref().unwrap_or("not supplied");
|
||||
format!(
|
||||
"{:?} role specialist completed. Next safe move: {next}",
|
||||
role_id
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{:?} role specialist completed, but no structured result was recorded.",
|
||||
role_id
|
||||
)
|
||||
}
|
||||
}
|
||||
ThreadEpiphanyRoleResultStatus::Failed => item_error
|
||||
.map(|error| format!("{:?} role specialist failed: {error}", role_id))
|
||||
.unwrap_or_else(|| format!("{:?} role specialist failed.", role_id)),
|
||||
ThreadEpiphanyRoleResultStatus::Cancelled => {
|
||||
format!(
|
||||
"{:?} role specialist was cancelled before producing a result.",
|
||||
role_id
|
||||
)
|
||||
}
|
||||
ThreadEpiphanyRoleResultStatus::Running => {
|
||||
format!("{:?} role specialist is still running.", role_id)
|
||||
}
|
||||
ThreadEpiphanyRoleResultStatus::Pending => {
|
||||
format!(
|
||||
"{:?} role specialist has not produced a result yet.",
|
||||
role_id
|
||||
)
|
||||
}
|
||||
ThreadEpiphanyRoleResultStatus::MissingState => {
|
||||
"No authoritative Epiphany state exists for this thread.".to_string()
|
||||
}
|
||||
ThreadEpiphanyRoleResultStatus::MissingBinding => {
|
||||
"No matching Epiphany role specialist binding exists.".to_string()
|
||||
}
|
||||
ThreadEpiphanyRoleResultStatus::BackendUnavailable => {
|
||||
"The bound runtime backend is unavailable.".to_string()
|
||||
}
|
||||
ThreadEpiphanyRoleResultStatus::BackendMissing => {
|
||||
"The bound runtime backend job or item is missing.".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_epiphany_reorient_result_note(
|
||||
status: ThreadEpiphanyReorientResultStatus,
|
||||
finding: Option<&ThreadEpiphanyReorientFinding>,
|
||||
|
|
@ -12459,6 +12973,100 @@ fn render_epiphany_reorient_result_note(
|
|||
}
|
||||
}
|
||||
|
||||
async fn load_epiphany_role_result_snapshot(
|
||||
state: &EpiphanyThreadState,
|
||||
state_db_ctx: Option<&StateDbHandle>,
|
||||
role_id: ThreadEpiphanyRoleId,
|
||||
binding_id: &str,
|
||||
) -> (
|
||||
ThreadEpiphanyRoleResultStatus,
|
||||
Option<ThreadEpiphanyRoleFinding>,
|
||||
String,
|
||||
) {
|
||||
let Some(binding) = state
|
||||
.job_bindings
|
||||
.iter()
|
||||
.find(|binding| binding.id == binding_id)
|
||||
else {
|
||||
return (
|
||||
ThreadEpiphanyRoleResultStatus::MissingBinding,
|
||||
None,
|
||||
"No matching Epiphany role specialist binding exists.".to_string(),
|
||||
);
|
||||
};
|
||||
let Some(agent_job_id) = binding_agent_jobs_job_id(binding) else {
|
||||
return (
|
||||
ThreadEpiphanyRoleResultStatus::BackendUnavailable,
|
||||
None,
|
||||
"The matching binding is not currently backed by the agent_jobs backend.".to_string(),
|
||||
);
|
||||
};
|
||||
let Some(state_db_ctx) = state_db_ctx else {
|
||||
return (
|
||||
ThreadEpiphanyRoleResultStatus::BackendUnavailable,
|
||||
None,
|
||||
"State runtime is unavailable, so the bound agent_jobs result cannot be read."
|
||||
.to_string(),
|
||||
);
|
||||
};
|
||||
let agent_job = match state_db_ctx.get_agent_job(agent_job_id).await {
|
||||
Ok(Some(job)) => job,
|
||||
Ok(None) => {
|
||||
return (
|
||||
ThreadEpiphanyRoleResultStatus::BackendMissing,
|
||||
None,
|
||||
format!(
|
||||
"Bound agent_jobs backend job {:?} was not found.",
|
||||
agent_job_id
|
||||
),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
return (
|
||||
ThreadEpiphanyRoleResultStatus::BackendUnavailable,
|
||||
None,
|
||||
format!("Failed to read Epiphany role backend job: {err}"),
|
||||
);
|
||||
}
|
||||
};
|
||||
let item = match state_db_ctx
|
||||
.get_agent_job_item(agent_job_id, binding_id)
|
||||
.await
|
||||
{
|
||||
Ok(Some(item)) => item,
|
||||
Ok(None) => {
|
||||
return (
|
||||
ThreadEpiphanyRoleResultStatus::BackendMissing,
|
||||
None,
|
||||
"Bound agent_jobs backend item was not found for this role specialist.".to_string(),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
return (
|
||||
ThreadEpiphanyRoleResultStatus::BackendUnavailable,
|
||||
None,
|
||||
format!("Failed to read Epiphany role backend item: {err}"),
|
||||
);
|
||||
}
|
||||
};
|
||||
let status = map_epiphany_role_result_status(agent_job.status, item.status);
|
||||
let finding = item.result_json.as_ref().map(|result| {
|
||||
map_epiphany_role_finding(
|
||||
role_id,
|
||||
result.clone(),
|
||||
agent_job.last_error.clone(),
|
||||
item.last_error.clone(),
|
||||
)
|
||||
});
|
||||
let note = render_epiphany_role_result_note(
|
||||
role_id,
|
||||
status,
|
||||
finding.as_ref(),
|
||||
item.last_error.as_deref(),
|
||||
);
|
||||
(status, finding, note)
|
||||
}
|
||||
|
||||
async fn load_epiphany_reorient_result_snapshot(
|
||||
state: Option<&EpiphanyThreadState>,
|
||||
state_db_ctx: Option<&StateDbHandle>,
|
||||
|
|
@ -12663,9 +13271,69 @@ fn map_epiphany_roles(
|
|||
result_status: ThreadEpiphanyReorientResultStatus,
|
||||
reorient_job: Option<&ThreadEpiphanyJob>,
|
||||
) -> Vec<ThreadEpiphanyRoleLane> {
|
||||
let state_present = state.is_some();
|
||||
let checkpoint = state.and_then(|state| state.investigation_checkpoint.as_ref());
|
||||
let graph_remap_job = jobs.iter().find(|job| job.id == "graph-remap").cloned();
|
||||
let verification_job = jobs.iter().find(|job| job.id == "verification").cloned();
|
||||
let modeling_jobs = jobs
|
||||
.iter()
|
||||
.filter(|job| {
|
||||
matches!(
|
||||
job.id.as_str(),
|
||||
EPIPHANY_MODELING_ROLE_BINDING_ID | "graph-remap"
|
||||
)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let modeling_bound_job = modeling_jobs
|
||||
.iter()
|
||||
.find(|job| job.id == EPIPHANY_MODELING_ROLE_BINDING_ID);
|
||||
let modeling_has_bound_job = modeling_bound_job.is_some();
|
||||
let modeling_status = map_epiphany_modeling_role_status(
|
||||
checkpoint.is_some(),
|
||||
modeling_bound_job.or_else(|| modeling_jobs.first()),
|
||||
);
|
||||
let modeling_recommended_action = if modeling_has_bound_job {
|
||||
Some(ThreadEpiphanySceneAction::RoleResult)
|
||||
} else if state_present {
|
||||
Some(ThreadEpiphanySceneAction::RoleLaunch)
|
||||
} else {
|
||||
Some(ThreadEpiphanySceneAction::Update)
|
||||
};
|
||||
let verification_jobs = jobs
|
||||
.iter()
|
||||
.filter(|job| {
|
||||
matches!(
|
||||
job.id.as_str(),
|
||||
EPIPHANY_VERIFICATION_ROLE_BINDING_ID | "verification"
|
||||
)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let verification_bound_job = verification_jobs
|
||||
.iter()
|
||||
.find(|job| job.id == EPIPHANY_VERIFICATION_ROLE_BINDING_ID);
|
||||
let verification_has_bound_job = verification_bound_job.is_some();
|
||||
let verification_primary_job = verification_bound_job.or_else(|| verification_jobs.first());
|
||||
let verification_status = verification_primary_job
|
||||
.as_ref()
|
||||
.map(|job| map_epiphany_job_status_to_role_status(job.status))
|
||||
.unwrap_or(ThreadEpiphanyRoleStatus::Unavailable);
|
||||
let verification_note = verification_primary_job
|
||||
.as_ref()
|
||||
.and_then(|job| {
|
||||
job.blocking_reason
|
||||
.clone()
|
||||
.or_else(|| job.progress_note.clone())
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
"Review evidence, worker findings, and verifier results before promotion.".to_string()
|
||||
});
|
||||
let verification_recommended_action = if verification_has_bound_job {
|
||||
Some(ThreadEpiphanySceneAction::RoleResult)
|
||||
} else if state_present {
|
||||
Some(ThreadEpiphanySceneAction::RoleLaunch)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let reorientation_jobs = reorient_job.cloned().into_iter().collect::<Vec<_>>();
|
||||
|
||||
vec![
|
||||
|
|
@ -12704,45 +13372,31 @@ fn map_epiphany_roles(
|
|||
id: ThreadEpiphanyRoleId::Modeling,
|
||||
title: "Modeling / Checkpoint".to_string(),
|
||||
owner_role: "epiphany-modeler".to_string(),
|
||||
status: map_epiphany_modeling_role_status(
|
||||
checkpoint.is_some(),
|
||||
graph_remap_job.as_ref(),
|
||||
),
|
||||
status: modeling_status,
|
||||
note: render_epiphany_modeling_role_note(checkpoint, decision),
|
||||
jobs: graph_remap_job.into_iter().collect(),
|
||||
authority_scopes: vec!["thread/epiphany/update".to_string()],
|
||||
recommended_action: if checkpoint.is_some() {
|
||||
None
|
||||
} else {
|
||||
Some(ThreadEpiphanySceneAction::Update)
|
||||
},
|
||||
jobs: modeling_jobs,
|
||||
authority_scopes: vec![
|
||||
"thread/epiphany/roleLaunch".to_string(),
|
||||
"thread/epiphany/roleResult".to_string(),
|
||||
"thread/epiphany/update".to_string(),
|
||||
],
|
||||
recommended_action: modeling_recommended_action,
|
||||
},
|
||||
ThreadEpiphanyRoleLane {
|
||||
id: ThreadEpiphanyRoleId::Verification,
|
||||
title: "Verification / Review".to_string(),
|
||||
owner_role: "epiphany-verifier".to_string(),
|
||||
status: verification_job
|
||||
.as_ref()
|
||||
.map(|job| map_epiphany_job_status_to_role_status(job.status))
|
||||
.unwrap_or(ThreadEpiphanyRoleStatus::Unavailable),
|
||||
note: verification_job
|
||||
.as_ref()
|
||||
.and_then(|job| {
|
||||
job.blocking_reason
|
||||
.clone()
|
||||
.or_else(|| job.progress_note.clone())
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
"Review evidence, worker findings, and verifier results before promotion."
|
||||
.to_string()
|
||||
}),
|
||||
jobs: verification_job.into_iter().collect(),
|
||||
status: verification_status,
|
||||
note: verification_note,
|
||||
jobs: verification_jobs,
|
||||
authority_scopes: vec![
|
||||
"thread/epiphany/roleLaunch".to_string(),
|
||||
"thread/epiphany/roleResult".to_string(),
|
||||
"thread/epiphany/distill".to_string(),
|
||||
"thread/epiphany/propose".to_string(),
|
||||
"thread/epiphany/promote".to_string(),
|
||||
],
|
||||
recommended_action: Some(ThreadEpiphanySceneAction::Promote),
|
||||
recommended_action: verification_recommended_action,
|
||||
},
|
||||
ThreadEpiphanyRoleLane {
|
||||
id: ThreadEpiphanyRoleId::Reorientation,
|
||||
|
|
@ -16199,10 +16853,14 @@ mod tests {
|
|||
ThreadEpiphanySceneAction::Distill,
|
||||
ThreadEpiphanySceneAction::Context,
|
||||
ThreadEpiphanySceneAction::Jobs,
|
||||
ThreadEpiphanySceneAction::Roles,
|
||||
ThreadEpiphanySceneAction::RoleLaunch,
|
||||
ThreadEpiphanySceneAction::RoleResult,
|
||||
ThreadEpiphanySceneAction::JobLaunch,
|
||||
ThreadEpiphanySceneAction::Freshness,
|
||||
ThreadEpiphanySceneAction::Pressure,
|
||||
ThreadEpiphanySceneAction::Reorient,
|
||||
ThreadEpiphanySceneAction::Crrc,
|
||||
ThreadEpiphanySceneAction::ReorientLaunch,
|
||||
ThreadEpiphanySceneAction::Update,
|
||||
ThreadEpiphanySceneAction::JobInterrupt,
|
||||
|
|
@ -17003,6 +17661,119 @@ mod tests {
|
|||
assert_eq!(roles[3].status, ThreadEpiphanyRoleStatus::Ready);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_epiphany_role_launch_request_uses_fixed_mvp_templates() {
|
||||
let state = codex_protocol::protocol::EpiphanyThreadState {
|
||||
revision: 7,
|
||||
objective: Some("Keep the MVP slice reviewable.".to_string()),
|
||||
active_subgoal_id: Some("sg-1".to_string()),
|
||||
subgoals: vec![codex_protocol::protocol::EpiphanySubgoal {
|
||||
id: "sg-1".to_string(),
|
||||
title: "Role launch".to_string(),
|
||||
status: "active".to_string(),
|
||||
summary: None,
|
||||
}],
|
||||
graph_frontier: Some(codex_protocol::protocol::EpiphanyGraphFrontier {
|
||||
active_node_ids: vec!["node-1".to_string()],
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let modeling = build_epiphany_role_launch_request(
|
||||
"thr_123",
|
||||
ThreadEpiphanyRoleId::Modeling,
|
||||
Some(7),
|
||||
Some(90),
|
||||
&state,
|
||||
)
|
||||
.expect("modeling should have a fixed launch template");
|
||||
assert_eq!(modeling.binding_id, EPIPHANY_MODELING_ROLE_BINDING_ID);
|
||||
assert_eq!(modeling.owner_role, EPIPHANY_MODELING_OWNER_ROLE);
|
||||
assert_eq!(modeling.authority_scope, "epiphany.role.modeling");
|
||||
assert_eq!(modeling.linked_subgoal_ids, vec!["sg-1".to_string()]);
|
||||
assert_eq!(modeling.linked_graph_node_ids, vec!["node-1".to_string()]);
|
||||
assert_eq!(modeling.input_json["roleId"], "modeling");
|
||||
assert_eq!(
|
||||
modeling.output_schema_json.as_ref().unwrap()["required"][0],
|
||||
"summary"
|
||||
);
|
||||
|
||||
let verification = build_epiphany_role_launch_request(
|
||||
"thr_123",
|
||||
ThreadEpiphanyRoleId::Verification,
|
||||
Some(7),
|
||||
Some(90),
|
||||
&state,
|
||||
)
|
||||
.expect("verification should have a fixed launch template");
|
||||
assert_eq!(
|
||||
verification.binding_id,
|
||||
EPIPHANY_VERIFICATION_ROLE_BINDING_ID
|
||||
);
|
||||
assert_eq!(verification.owner_role, EPIPHANY_VERIFICATION_OWNER_ROLE);
|
||||
assert_eq!(verification.authority_scope, "epiphany.role.verification");
|
||||
assert_eq!(verification.input_json["roleId"], "verification");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_epiphany_role_launch_request_rejects_untemplated_roles() {
|
||||
let state = codex_protocol::protocol::EpiphanyThreadState::default();
|
||||
|
||||
assert!(
|
||||
build_epiphany_role_launch_request(
|
||||
"thr_123",
|
||||
ThreadEpiphanyRoleId::Implementation,
|
||||
None,
|
||||
None,
|
||||
&state,
|
||||
)
|
||||
.unwrap_err()
|
||||
.contains("main coding agent")
|
||||
);
|
||||
assert!(
|
||||
build_epiphany_role_launch_request(
|
||||
"thr_123",
|
||||
ThreadEpiphanyRoleId::Reorientation,
|
||||
None,
|
||||
None,
|
||||
&state,
|
||||
)
|
||||
.unwrap_err()
|
||||
.contains("reorientLaunch")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_epiphany_role_finding_projects_structured_output() {
|
||||
let raw_result = serde_json::json!({
|
||||
"verdict": "pass",
|
||||
"summary": "The evidence covers the bounded slice.",
|
||||
"nextSafeMove": "Promote after human review.",
|
||||
"filesInspected": ["src/lib.rs"],
|
||||
"frontierNodeIds": ["node-1"],
|
||||
"evidenceIds": ["ev-1"]
|
||||
});
|
||||
|
||||
let finding = map_epiphany_role_finding(
|
||||
ThreadEpiphanyRoleId::Verification,
|
||||
raw_result.clone(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(finding.role_id, ThreadEpiphanyRoleId::Verification);
|
||||
assert_eq!(finding.verdict.as_deref(), Some("pass"));
|
||||
assert_eq!(
|
||||
finding.next_safe_move.as_deref(),
|
||||
Some("Promote after human review.")
|
||||
);
|
||||
assert_eq!(finding.files_inspected, vec!["src/lib.rs"]);
|
||||
assert_eq!(finding.frontier_node_ids, vec!["node-1"]);
|
||||
assert_eq!(finding.evidence_ids, vec!["ev-1"]);
|
||||
assert_eq!(finding.raw_result, raw_result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reorient_finding_acceptance_builds_scratch_and_checkpoint_update() {
|
||||
let finding = ThreadEpiphanyReorientFinding {
|
||||
|
|
|
|||
Reference in a new issue