Add typed tool followup requests
This commit is contained in:
parent
15b079db93
commit
89fa715e8e
10 changed files with 290 additions and 23 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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]"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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.
|
|
@ -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.
|
||||
|
|
|
|||
Reference in a new issue