Add typed tool followup requests

This commit is contained in:
Meta Vens 2026-05-28 08:19:13 +01:00
commit 89fa715e8e
10 changed files with 290 additions and 23 deletions

View file

@ -549,6 +549,8 @@ pub struct EpiphanyToolInvocationStatus {
pub adapter: String,
pub server: String,
pub tool_name: String,
pub call_id: Option<String>,
pub model_request_id: Option<String>,
pub caller: String,
pub reason: String,
pub created_at: String,
@ -1351,6 +1353,8 @@ pub fn runtime_tool_invocation_statuses(
adapter: intent.adapter,
server: intent.server,
tool_name: intent.tool_name,
call_id: intent.call_id,
model_request_id: intent.model_request_id,
caller: intent.caller,
reason: intent.reason,
created_at: intent.created_at,

View file

@ -52,6 +52,8 @@ pub struct EpiphanyModelRequest {
pub service_tier: Option<String>,
#[cultcache(key = 10, default)]
pub output_contract_id: Option<String>,
#[cultcache(key = 11, default)]
pub previous_response_id: Option<String>,
}
impl EpiphanyModelRequest {
@ -74,6 +76,7 @@ impl EpiphanyModelRequest {
reasoning_summary: None,
service_tier: None,
output_contract_id: None,
previous_response_id: None,
}
}
}

View file

@ -65,6 +65,8 @@ pub struct EpiphanyOpenAiModelRequest {
pub service_tier: Option<String>,
#[cultcache(key = 9, default)]
pub output_contract_id: Option<String>,
#[cultcache(key = 10, default)]
pub previous_response_id: Option<String>,
}
impl EpiphanyOpenAiModelRequest {
@ -85,6 +87,7 @@ impl EpiphanyOpenAiModelRequest {
reasoning_summary: None,
service_tier: None,
output_contract_id: None,
previous_response_id: None,
}
}
}

View file

@ -204,6 +204,7 @@ pub fn responses_body_from_epiphany(
stream: true,
include: Vec::new(),
service_tier: parse_service_tier(request.service_tier.as_deref())?,
previous_response_id: request.previous_response_id,
prompt_cache_key: None,
text: None,
client_metadata: None,
@ -227,6 +228,8 @@ struct EpiphanyResponsesBody {
#[serde(skip_serializing_if = "Option::is_none")]
service_tier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
previous_response_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
prompt_cache_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
text: Option<serde_json::Value>,
@ -562,11 +565,13 @@ mod tests {
request.reasoning_effort = Some("low".to_string());
request.reasoning_summary = Some("concise".to_string());
request.service_tier = Some("flex".to_string());
request.previous_response_id = Some("resp-1".to_string());
let responses = responses_body_from_epiphany(request).expect("request should map");
assert_eq!(responses["model"], "gpt-5.4");
assert_eq!(responses["instructions"], "Answer plainly.");
assert_eq!(responses["previous_response_id"], "resp-1");
assert_eq!(responses["input"][0]["content"][0]["type"], "input_text");
assert_eq!(responses["stream"], true);
assert_eq!(responses["service_tier"], "flex");

View file

@ -15,6 +15,7 @@ use epiphany_openai_runtime::EpiphanyOpenAiRuntimeOptions;
use epiphany_openai_runtime::EpiphanyWorkerRuntimeOptions;
use epiphany_openai_runtime::OPENAI_RUNTIME_ROLE;
use epiphany_openai_runtime::assistant_text_from_model_events;
use epiphany_openai_runtime::build_tool_followup_model_request;
use epiphany_openai_runtime::default_codex_home;
use epiphany_openai_runtime::default_options;
use epiphany_openai_runtime::ensure_openai_runtime_ready;
@ -77,6 +78,26 @@ async fn main() -> Result<()> {
.await?;
print_json(&summary)?;
}
"tool-followup" => {
let options = parse_tool_followup_options(args.collect())?;
let request = build_tool_followup_model_request(
&options.store_path,
&options.request_id,
&options.followup_request_id,
)?;
if let Some(parent) = options.output.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("failed to create {}", parent.display()))?;
}
fs::write(&options.output, serde_json::to_string_pretty(&request)?)
.with_context(|| format!("failed to write {}", options.output.display()))?;
print_json(&json!({
"requestId": request.request_id,
"previousResponseId": request.previous_response_id,
"inputItems": request.input.len(),
"output": options.output,
}))?;
}
"smoke" => {
let options = parse_smoke_options(args.collect())?;
require_supported_provider(&options.provider)?;
@ -218,6 +239,13 @@ struct RunWorkerCliOptions {
model: String,
}
struct ToolFollowupCliOptions {
store_path: PathBuf,
request_id: String,
followup_request_id: String,
output: PathBuf,
}
fn parse_model_turn_options(args: Vec<String>) -> Result<ModelTurnCliOptions> {
let mut provider = DEFAULT_PROVIDER.to_string();
let mut store_path = PathBuf::from(DEFAULT_STORE);
@ -287,6 +315,32 @@ fn parse_run_worker_options(args: Vec<String>) -> Result<RunWorkerCliOptions> {
})
}
fn parse_tool_followup_options(args: Vec<String>) -> Result<ToolFollowupCliOptions> {
let mut store_path = PathBuf::from(DEFAULT_STORE);
let mut request_id = None;
let mut followup_request_id = None;
let mut output = None;
let mut iter = args.into_iter();
while let Some(arg) = iter.next() {
match arg.as_str() {
"--store" => store_path = PathBuf::from(next_value(&mut iter, "--store")?),
"--request-id" => request_id = Some(next_value(&mut iter, "--request-id")?),
"--followup-request-id" => {
followup_request_id = Some(next_value(&mut iter, "--followup-request-id")?)
}
"--output" => output = Some(PathBuf::from(next_value(&mut iter, "--output")?)),
other => return Err(anyhow!("unknown tool-followup argument: {other}")),
}
}
Ok(ToolFollowupCliOptions {
store_path,
request_id: request_id.context("tool-followup requires --request-id")?,
followup_request_id: followup_request_id
.unwrap_or_else(|| format!("tool-followup-{}", Uuid::new_v4())),
output: output.context("tool-followup requires --output")?,
})
}
fn parse_smoke_options(args: Vec<String>) -> Result<SmokeCliOptions> {
let mut provider = DEFAULT_PROVIDER.to_string();
let mut store_path = PathBuf::from(format!(
@ -334,5 +388,5 @@ fn now() -> String {
}
fn usage() -> &'static str {
"usage: epiphany-model-runtime <model-turn|run-worker|smoke> [--provider openai-codex] [--store path] [--codex-home path] [--request path] [--session-id id] [--job-id id] [--objective text] [--default-model model] [--output-last-message path]"
"usage: epiphany-model-runtime <model-turn|run-worker|tool-followup|smoke> [--provider openai-codex] [--store path] [--codex-home path] [--request path] [--request-id id] [--followup-request-id id] [--output path] [--session-id id] [--job-id id] [--objective text] [--default-model model] [--output-last-message path]"
}

View file

@ -39,7 +39,9 @@ pub use epiphany_openai_codex_spine::default_codex_home;
use epiphany_openai_codex_spine::status_from_auth_manager;
use epiphany_tool_adapter::CODEX_MCP_TOOL_ADAPTER_ID;
use epiphany_tool_adapter::EpiphanyToolInvocationIntent;
use epiphany_tool_adapter::EpiphanyToolInvocationReceipt;
use epiphany_tool_adapter::tool_invocation_intent_key;
use epiphany_tool_adapter::tool_invocation_receipt_key;
use serde::de::DeserializeOwned;
pub const OPENAI_RUNTIME_ROLE: &str = "openai-model-adapter";
@ -572,6 +574,87 @@ pub fn assistant_text_from_model_events(
Ok(text)
}
pub fn build_tool_followup_model_request(
store_path: impl AsRef<Path>,
original_request_id: &str,
followup_request_id: &str,
) -> Result<EpiphanyModelRequest> {
let mut cache = runtime_spine_cache(store_path)?;
cache.pull_all_backing_stores()?;
let original = cache
.get::<EpiphanyModelRequest>(&model_request_key(original_request_id))?
.ok_or_else(|| anyhow!("model request {original_request_id:?} does not exist"))?;
let receipt = cache
.get::<EpiphanyModelReceipt>(&model_receipt_key(original_request_id))?
.ok_or_else(|| anyhow!("model receipt {original_request_id:?} does not exist"))?;
let previous_response_id = receipt.provider_response_id.clone().ok_or_else(|| {
anyhow!("model receipt {original_request_id:?} has no provider_response_id")
})?;
let original_prefix = format!("model-{}-", sanitize_request_id(original_request_id));
let mut followup_items = Vec::new();
for intent in cache.get_all::<EpiphanyToolInvocationIntent>()? {
if intent.model_request_id.as_deref() != Some(original_request_id)
&& !intent.intent_id.starts_with(&original_prefix)
{
continue;
}
let Some(call_id) = intent.call_id.clone() else {
continue;
};
let Some(receipt) = cache.get::<EpiphanyToolInvocationReceipt>(
&tool_invocation_receipt_key(&intent.intent_id),
)?
else {
continue;
};
followup_items.push((intent, call_id, receipt));
}
followup_items.sort_by(|left, right| {
left.0
.created_at
.cmp(&right.0.created_at)
.then_with(|| left.0.intent_id.cmp(&right.0.intent_id))
});
if followup_items.is_empty() {
return Err(anyhow!(
"model request {original_request_id:?} has no completed tool receipts with call ids"
));
}
let mut followup = original;
followup.request_id = followup_request_id.to_string();
followup.previous_response_id = Some(previous_response_id);
followup.input = followup_items
.into_iter()
.map(
|(intent, call_id, receipt)| EpiphanyModelInputItem::ToolResult {
call_id,
output: tool_receipt_output_for_model(&intent, &receipt),
},
)
.collect();
Ok(followup)
}
fn tool_receipt_output_for_model(
intent: &EpiphanyToolInvocationIntent,
receipt: &EpiphanyToolInvocationReceipt,
) -> String {
if let Some(result) = receipt.result_json.as_ref() {
return result.clone();
}
serde_json::json!({
"status": receipt.status,
"adapter": receipt.adapter,
"server": receipt.server,
"toolName": receipt.tool_name,
"intentId": intent.intent_id,
"receiptId": receipt.receipt_id,
"error": receipt.error,
})
.to_string()
}
pub fn default_options(
store_path: PathBuf,
codex_home: PathBuf,
@ -670,6 +753,7 @@ pub fn model_request_from_openai_request(
reasoning_summary: request.reasoning_summary.clone(),
service_tier: request.service_tier.clone(),
output_contract_id: request.output_contract_id.clone(),
previous_response_id: request.previous_response_id.clone(),
}
}
@ -691,6 +775,7 @@ pub fn openai_request_from_model_request(
reasoning_summary: request.reasoning_summary.clone(),
service_tier: request.service_tier.clone(),
output_contract_id: request.output_contract_id.clone(),
previous_response_id: request.previous_response_id.clone(),
}
}
@ -810,24 +895,27 @@ pub fn tool_invocation_intent_from_model_event(
if !arguments_are_invocation_ready(arguments) {
return None;
}
Some(EpiphanyToolInvocationIntent::new(
format!(
"model-{}-{}-{}",
sanitize_request_id(&event.request_id),
event.sequence,
sanitize_request_id(call_id)
),
CODEX_MCP_TOOL_ADAPTER_ID,
server,
tool_name,
arguments.clone(),
format!("model-runtime:{}", event.provider),
format!(
"Model request {} emitted MCP tool call {}.",
event.request_id, call_id
),
now(),
))
Some(
EpiphanyToolInvocationIntent::new(
format!(
"model-{}-{}-{}",
sanitize_request_id(&event.request_id),
event.sequence,
sanitize_request_id(call_id)
),
CODEX_MCP_TOOL_ADAPTER_ID,
server,
tool_name,
arguments.clone(),
format!("model-runtime:{}", event.provider),
format!(
"Model request {} emitted MCP tool call {}.",
event.request_id, call_id
),
now(),
)
.with_model_call(call_id.clone(), event.request_id.clone()),
)
}
fn split_mcp_tool_name(name: &str) -> Option<(String, String)> {
@ -1340,6 +1428,99 @@ mod tests {
assert_eq!(intent.tool_name, "list_events");
assert_eq!(intent.arguments_json, r#"{"limit":3}"#);
assert_eq!(intent.intent_id, "model-request-1-7-call-1");
assert_eq!(intent.call_id.as_deref(), Some("call/1"));
assert_eq!(intent.model_request_id.as_deref(), Some("request-1"));
Ok(())
}
#[test]
fn builds_tool_followup_model_request_from_receipts() -> Result<()> {
let temp = tempdir()?;
let store = temp.path().join("runtime.msgpack");
let request = EpiphanyOpenAiModelRequest::new(
"req-tools",
"conversation-1",
"gpt-5.4",
"Answer after tool output.",
);
let options = default_options(store.clone(), PathBuf::from(".codex"), &request);
ensure_openai_runtime_ready(&options)?;
ensure_runtime_session(
&store,
RuntimeSpineSessionOptions {
session_id: options.session_id.clone(),
objective: options.objective.clone(),
created_at: now(),
coordinator_note: options.coordinator_note.clone(),
},
)?;
create_runtime_job(
&store,
RuntimeSpineJobOptions {
job_id: options.job_id.clone(),
session_id: options.session_id.clone(),
role: OPENAI_RUNTIME_ROLE.to_string(),
created_at: now(),
summary: "tool test job".to_string(),
artifact_refs: Vec::new(),
},
)?;
let mut receipt = EpiphanyOpenAiModelReceipt::new("req-tools", "gpt-5.4");
receipt.response_id = Some("resp-tools".to_string());
receipt.transport = Some("test".to_string());
let events = vec![
EpiphanyOpenAiStreamEvent {
schema_id: epiphany_openai_adapter::OPENAI_ADAPTER_EVENT_SCHEMA_ID.to_string(),
request_id: "req-tools".to_string(),
sequence: 0,
payload: EpiphanyOpenAiStreamPayload::ToolCall {
call_id: "call-original".to_string(),
name: "mcp__smoke_server__smoke_tool".to_string(),
arguments: "{}".to_string(),
},
},
EpiphanyOpenAiStreamEvent {
schema_id: epiphany_openai_adapter::OPENAI_ADAPTER_EVENT_SCHEMA_ID.to_string(),
request_id: "req-tools".to_string(),
sequence: 1,
payload: EpiphanyOpenAiStreamPayload::Completed { receipt },
},
];
let summary = record_openai_events(&store, &options, &request, &events)?;
let intent_id = summary
.tool_intent_ids
.first()
.expect("tool intent id")
.clone();
let mut cache = runtime_spine_cache(&store)?;
cache.pull_all_backing_stores()?;
let mut tool_receipt = EpiphanyToolInvocationReceipt::new(
"receipt-tool",
intent_id.clone(),
epiphany_tool_adapter::CODEX_MCP_TOOL_ADAPTER_ID,
"smoke_server",
"smoke_tool",
"completed",
now(),
);
tool_receipt.result_json = Some(r#"{"ok":true}"#.to_string());
cache.put(
tool_invocation_receipt_key(&tool_receipt.intent_id),
&tool_receipt,
)?;
let followup =
build_tool_followup_model_request(&store, "req-tools", "req-tools-followup")?;
assert_eq!(followup.request_id, "req-tools-followup");
assert_eq!(followup.previous_response_id.as_deref(), Some("resp-tools"));
assert_eq!(followup.input.len(), 1);
assert_eq!(
followup.input[0],
EpiphanyModelInputItem::ToolResult {
call_id: "call-original".to_string(),
output: r#"{"ok":true}"#.to_string()
}
);
Ok(())
}

View file

@ -78,6 +78,10 @@ pub struct EpiphanyToolInvocationIntent {
pub reason: String,
#[cultcache(key = 8)]
pub created_at: String,
#[cultcache(key = 9, default)]
pub call_id: Option<String>,
#[cultcache(key = 10, default)]
pub model_request_id: Option<String>,
}
impl EpiphanyToolInvocationIntent {
@ -101,8 +105,20 @@ impl EpiphanyToolInvocationIntent {
caller: caller.into(),
reason: reason.into(),
created_at: created_at.into(),
call_id: None,
model_request_id: None,
}
}
pub fn with_model_call(
mut self,
call_id: impl Into<String>,
model_request_id: impl Into<String>,
) -> Self {
self.call_id = Some(call_id.into());
self.model_request_id = Some(model_request_id.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]

View file

@ -40,7 +40,7 @@ remembers doctrine; the branch remembers the blade.
- `epiphany-model-runtime` is the current provider-neutral executable boundary. It is built by the `epiphany-openai-runtime` crate for now and accepts `--provider openai-codex`; the old `epiphany-openai-runtime` binary, `--openai-runtime-bin`, and `openai-runtime` executor name survive only as compatibility aliases. Coordinator run mode, local wrapper run mode, and repo birth runner should call `--model-runtime-bin` plus `--model-provider`. The typed request/event/receipt contract is now provider-neutral in `epiphany-model-adapter` as `epiphany.model_request.v0`, `epiphany.model_stream_event.v0`, `epiphany.model_receipt.v0`, and `epiphany.model_adapter_status.v0`; OpenAI/Codex documents are adapter evidence behind that boundary, not scheduler/status/state authority.
- Coordinator plan/run passes now emit `epiphany.coordinator_run_receipt.v0` into runtime-spine. `coordinator-summary.json` and `coordinator-steps.jsonl` remain useful operator artifacts, but the durable receipt of what the coordinator decided is typed CultCache/CultNet state.
- The first tool/MCP contract surface exists in `epiphany-tool-adapter`: `epiphany.tool_capability.v0`, `epiphany.tool_invocation_intent.v0`, and `epiphany.tool_invocation_receipt.v0`. Runtime-spine advertises these as the Epiphany-facing tool boundary. `epiphany-model-runtime` now materializes complete MCP-shaped model tool calls as typed invocation intents, and `epiphany-tool-codex-mcp-spine` is the quarantined executing adapter: it consumes one typed invocation intent from the mixed runtime-spine CultCache store, calls user-declared Codex MCP through `codex-mcp`, and emits a typed invocation receipt. Raw MCP JSON remains protocol-edge cargo; Codex MCP is transport scaffolding, not Epiphany state, prompt, scheduler, or policy authority.
- Native status now exposes that quarantined bridge spine without granting it authority. `runtime_spine_status` counts tool invocation intents, receipts, and pending calls; `runtime_tool_invocation_statuses` derives read-only intent/receipt status rows; `epiphany-runtime-spine status` reports the counts; and `epiphany-mvp-status --runtime-store <path>` renders the latest tool calls under `tools`. This is operator visibility only. The next clean backend slice is the typed tool-result feedback path into model continuation, with Epiphany-owned prompt/result policy and no Codex prompt machinery instructing Epiphany agents.
- Native status now exposes that quarantined bridge spine without granting it authority. `runtime_spine_status` counts tool invocation intents, receipts, and pending calls; `runtime_tool_invocation_statuses` derives read-only intent/receipt status rows; `epiphany-runtime-spine status` reports the counts; and `epiphany-mvp-status --runtime-store <path>` renders the latest tool calls under `tools`. This is operator visibility only. The model-runtime feedback seam is also now typed: MCP-shaped model tool calls preserve the original provider `call_id` plus source model request id, and `epiphany-model-runtime tool-followup` builds a provider-neutral follow-up `EpiphanyModelRequest` from completed typed tool receipts using the previous provider response id. Codex still executes the quarantined MCP edge; Epiphany owns the continuation request shape.
- The active foundation directive is now `notes/codex-starvation-and-cultnet-liberation-plan.md`. The previous Codex app-server control-plane rebuild made Epiphany less rotten inside Codex, but that is not the destination. Epiphany must become a native CultCache/CultMesh/CultNet runtime while remaining an honest modified Codex-derived backend for subscription auth/model use. Codex should remain relatively vanilla and keep doing Codex things, including useful app-server and streaming bridge affordances, but it must not own Epiphany state, processes, prompts, scheduler decisions, or policy. The bridge handles interop; Codex and Epiphany do not get to rummage around in each other's organs.
- `notes/epiphany-cultmesh-dreaming-roadmap.md` is the new concrete design for distributed Epiphany dreaming. The invariant is hard: private state stays local; public dreams are separately authored typed documents distributed through CultMesh/CultNet; foreign dreams are thought weather until a reviewed local adoption receipt digests them. The first code slice should be dream schemas plus a local CultMesh-backed store, not public fanout.
- CultMesh is now the preferred Rust abstraction for that local store work. The compile-time Rust substrate is repo-contained: `vendor/cultcache-rs`, `vendor/cultnet-rs`, and `vendor/cultmesh-rs`. The old `E:\Projects\CultLib\crates\*` dependency body is dead in this checkout and must not be revived by accident. Vendored CultCache reads/writes `cultcache.store.v1`, so `state/ledgers.msgpack` and `epiphany-state status` are aligned again. The first integrations are deliberately small: status, Verse policy, global room policy, organ contract policy, native operator status, native operator snapshots, and local operator run intent/receipt round-trip through CultMesh. Use `cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-cultmesh-smoke`, `cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-cultmesh-status -- smoke --store .\.epiphany-smoke\cultmesh\operator-status.ccmp --runtime-id epiphany-cultmesh-status-smoke`, `cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-operator-snapshot -- smoke --store .\.epiphany-smoke\cultmesh\operator-snapshots.ccmp --runtime-id epiphany-operator-snapshot-smoke --snapshot-id smoke-status`, and `cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-operator-run -- smoke --store .\.epiphany-smoke\cultmesh\operator-runs.ccmp --runtime-id epiphany-operator-run-smoke --run-id smoke-run` for the focused proofs. Future local dream/status/Verse work should start from CultMesh, not raw CultNet, unless the task is polishing the CultMesh internals themselves. Preserve the three Verse tiers: `epiphany-internal` for private sub-agent typed state, `gamecult-local` for trusted LAN/Yggdrasil-tunnel GameCult sharing, and `epiphany-global` for untrusted public dreams plus topic-specific threaded public rooms for Face posts.
@ -183,7 +183,7 @@ remembers doctrine; the branch remembers the blade.
- The planning loop is now runtime-backed and operator-visible. Typed captures, backlog items, roadmap streams, Objective Drafts, and GitHub issue source refs live in `EpiphanyThreadState`, validate through revision-gated `thread/epiphany/update`, render into prompts when present, project read-only through the `planning` view lens, render in the GUI Planning panel, can be synthesized by the fixed Imagination/planning lane, and can be explicitly adopted through the artifacted `adoptObjectiveDraft` GUI action. Chat is deliberation, not an objective pipe; ideas and GitHub Issues remain planning state until explicit human adoption.
- The bridge/dashboard slice is now landed far enough to test locally. Native `epiphany-rider-bridge` inspects Rider installation, solution, VCS, changed files, and captures file/selection/symbol context into `.epiphany-gui/rider` artifacts; Aquarium has Inspect Rider and renders Rider audit artifacts in Environment; Aquarium also embeds the adjacent EpiphanyGraph viewer over typed `graphs.architecture`, `graphs.dataflow`, and `graphs.links`.
- A first Rider plugin scaffold lives under `integrations/rider`: tool window, Refresh status, and Send Context to Epiphany action. It shells through native `epiphany-rider-bridge` and does not own state. `gradle` is not installed on this machine, so the scaffold is not build-verified yet.
- Next real move: continue Codex starvation and native Epiphany ownership by wiring model-tool receipts back into typed model continuation. Quarantine Rider/Unity bridge work for now; those artifacts remain optional later environment evidence, not MVP blockers. Keep EpiphanyAgent focused on backend contracts, typed state, coordinator policy, heartbeat scheduling, Face guardrails, prompt-authority separation, and the ongoing purification from Python/JSON scaffolding into Rust/CultCache/CultMesh/CultNet organs. Resume repo birth/Aquarium/Rider/Unity work only when the user explicitly shifts priority back outward. GitHub Issues import is deferred until the backlog source is fresh enough to deserve Imagination's attention.
- Next real move: continue Codex starvation and native Epiphany ownership by making the coordinator/runtime loop consume the typed `tool-followup` request instead of requiring the operator to run the three commands manually. Quarantine Rider/Unity bridge work for now; those artifacts remain optional later environment evidence, not MVP blockers. Keep EpiphanyAgent focused on backend contracts, typed state, coordinator policy, heartbeat scheduling, Face guardrails, prompt-authority separation, and the ongoing purification from Python/JSON scaffolding into Rust/CultCache/CultMesh/CultNet organs. Resume repo birth/Aquarium/Rider/Unity work only when the user explicitly shifts priority back outward. GitHub Issues import is deferred until the backlog source is fresh enough to deserve Imagination's attention.
- The repo is an Epiphany fork of Codex, not a Codex preset.
- `vendor/codex` is tracked directly, not a submodule.
- `epiphany-core` owns the heavy Epiphany organs where practical.

Binary file not shown.

View file

@ -72,7 +72,7 @@ active_subgoals:
- Use `notes/epiphany-cultmesh-dreaming-roadmap.md` as the design authority for distributed Epiphany dreaming. Public dreams are separately authored typed documents distributed through CultMesh/CultNet; private state remains local and cannot be shared by flipping a visibility flag. Foreign dreams are external thought weather until a reviewed local adoption receipt digests them into memory, planning, doctrine, or governance.
- CultMesh is now the preferred ergonomic Rust abstraction over CultCache and CultNet for Epiphany-owned local database surfaces. The Rust substrate is repo-contained under `vendor/cultcache-rs`, `vendor/cultnet-rs`, and `vendor/cultmesh-rs`; the old `E:\Projects\CultLib\crates\*` path is dead in this checkout and must not be reintroduced as a compile-time dependency. Future local Verse/dream/status work should start at CultMesh unless it is explicitly polishing CultMesh internals. Preserve the three-Verse boundary: `epiphany-internal` for private sub-agent typed state, `gamecult-local` for trusted LAN/Yggdrasil-tunnel GameCult project sharing, and `epiphany-global` for untrusted public dreams plus topic-specific threaded public rooms for Face posts.
- The active priority is Codex starvation and CultNet liberation. Use `notes/codex-starvation-and-cultnet-liberation-plan.md` as the directive and `notes/codex-auth-spine-inventory.md` as the source-grounded keeper list for the retained OpenAI subscription auth/model transport bridge. Classify all Epiphany-relevant JSON contamination, define the native CultNet runtime boundary, keep worker launch/result/selfPatch cargo as typed documents, evacuate Epiphany operator/state/process authority from Codex JSON-RPC, wrap MCP's unavoidable JSON protocol edge in typed Epiphany intent/result/receipt documents, and keep cutting until Epiphany can run its own state and processes without `codex_message_processor.rs` except for bridge compatibility.
- The next Codex-starvation slice is typed tool-result feedback into model continuation. The quarantined Codex MCP spine may execute typed tool intents and emit typed receipts, but the runtime/status surface must remain read-only until Epiphany owns the continuation policy. Codex transports tool calls; it must not inject prompt doctrine or decide what Epiphany agents do with tool results.
- The next Codex-starvation slice is automatic coordinator/runtime consumption of typed tool-result follow-up requests. The quarantined Codex MCP spine may execute typed tool intents and emit typed receipts, and the model runtime can now build a provider-neutral follow-up request from those receipts, but the coordinator still needs to own when that follow-up turn runs. Codex transports tool calls; it must not inject prompt doctrine or decide what Epiphany agents do with tool results.
- The May 2026 teardown/rebuild migration in `notes/epiphany-ideal-architecture-rebuild-plan.md` is closed unless a concrete regression reopens it. Preserve the new foundation: Epiphany view/coordinator policy lives in `epiphany-core`, runtime-spine owns job/result lifecycle identity through `runtime_links`, duplicate acceptance is receipt-keyed, public findings stay typed/sealed, view expansion requires a core contract, and app-server remains routing/adaptation rather than a second policy throne.
- 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, freshness, context, graphQuery, roleResult, reorientResult, and the canonical view lenses for scene, jobs, roles, planning, pressure, reorient, crrc, and coordinator.
@ -177,6 +177,7 @@ accepted_design:
- Pre-compaction checkpoint intervention is now wired at token-count boundaries: when a loaded Epiphany thread crosses the existing `shouldPrepareCompaction` pressure threshold during an active turn, the harness steers the active agent once for that turn with a bounded CRRC directive to bank scratch/checkpoint/map/evidence before compaction/reorientation. A successful steer marks a turn-scoped pending compact handoff consumed at clean turn completion; this is banking the fire before the dark, not semantic acceptance, broad scheduling, or automatic implementation continuation.
- Native `epiphany-mvp-status` is the first dogfood operator view: it starts or reads a thread through app-server and renders scene, planning, pressure, reorient, jobs, role result read-backs, reorient result, CRRC recommendation, heartbeat, Face bubbles, and MVP role lanes as text or machine output without becoming a state store. The old Python status module has been cut; native Rust/CultCache/CultNet surfaces are the product path.
- Native status now exposes the quarantined tool/MCP spine as operator-visible read-only state. Runtime-spine counts typed tool invocation intents, receipts, and pending calls; `runtime_tool_invocation_statuses` derives intent/receipt status rows; `epiphany-runtime-spine status` prints the counts; and `epiphany-mvp-status --runtime-store <path>` renders a `tools` section without executing calls, accepting results, or letting Codex prompt machinery steer Epiphany agents.
- Model-tool feedback now has a typed continuation seam. `EpiphanyToolInvocationIntent` preserves the exact model `call_id` plus source model request id; provider-neutral and OpenAI request documents carry optional `previous_response_id`; the OpenAI Responses mapper emits that id as `previous_response_id`; and `epiphany-model-runtime tool-followup` builds a provider-neutral follow-up request from typed tool receipts. The manual smoke path is model-runtime smoke -> quarantined Codex MCP spine run -> model-runtime `tool-followup`; the next organ is coordinator ownership of that sequence.
- The `roles` view lens is the harness-native role ownership surface: it projects implementation, Imagination/planning, 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: Imagination/planning, modeling/checkpoint, and verification/review launch through fixed heartbeat-backed bindings with typed runtime-spine receipts and can be read back as reviewable findings from typed runtime-spine job results, while implementation stays owned by the main coding agent and reorientation stays on the CRRC-specific launch/result path.
- `thread/epiphany/roleAccept` is the first review-gated role finding acceptance path: it accepts completed heartbeat-backed findings from typed runtime-spine results, accepts Imagination/planning findings only when they return a valid planning-only `statePatch` with draft Objective Draft review gates, accepts completed modeling/checkpoint findings only with a modeling-safe `statePatch`, builds accepted evidence/observation/receipt bundles through `epiphany-core`, and rejects verification, implementation, reorientation, job binding, objective adoption, or broad authority fields.
@ -552,4 +553,4 @@ current_status:
materializes complete MCP-shaped model tool calls as typed tool invocation
intents, and the Codex MCP spine can consume those intents from the mixed
runtime-spine store and emit typed success/failure receipts.
next_action: Continue Codex starvation and CultNet liberation by wiring typed tool invocation receipts back into model continuation through an Epiphany-owned policy path; the pending-tool operator visibility surface is landed, so the next fork is how follow-up model input is owned without Codex prompt machinery steering Epiphany agents.
next_action: Continue Codex starvation and CultNet liberation by making the coordinator/runtime loop consume the typed tool-followup request path automatically, preserving Epiphany-owned continuation policy while Codex remains only auth/model/MCP transport.