This repository has been archived on 2026-10-29. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
Epiphany/epiphany-core/src/bin/epiphany-runtime-spine.rs
2026-06-09 02:45:09 +01:00

613 lines
20 KiB
Rust

use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
use chrono::SecondsFormat;
use epiphany_core::EpiphanyRuntimeEvent;
use epiphany_core::EpiphanyRuntimeJob;
use epiphany_core::EpiphanyRuntimeJobResult;
use epiphany_core::RuntimeSpineEventOptions;
use epiphany_core::RuntimeSpineInitOptions;
use epiphany_core::RuntimeSpineJobOptions;
use epiphany_core::RuntimeSpineJobResultOptions;
use epiphany_core::RuntimeSpineSessionOptions;
use epiphany_core::append_runtime_event;
use epiphany_core::complete_runtime_job;
use epiphany_core::create_runtime_job;
use epiphany_core::create_runtime_session;
use epiphany_core::initialize_runtime_spine;
use epiphany_core::runtime_spine_cache;
use epiphany_core::runtime_spine_status;
use epiphany_core::write_runtime_hello_frame;
use epiphany_core::write_runtime_schema_catalog_json;
use epiphany_model_adapter::EpiphanyModelInputItem;
use epiphany_model_adapter::EpiphanyModelRequest;
use std::env;
use std::path::PathBuf;
use uuid::Uuid;
const DEFAULT_STORE: &str = "state/runtime-spine.cc";
fn main() -> Result<()> {
let args = Args::parse()?;
match args.command {
Command::Init {
runtime_id,
display_name,
} => {
let identity = initialize_runtime_spine(
&args.store,
RuntimeSpineInitOptions {
runtime_id,
display_name,
created_at: now(),
},
)?;
println!("runtime spine initialized");
println!("store: {}", args.store.display());
println!("runtime: {}", identity.runtime_id);
println!(
"documents: {}",
identity.supported_document_types.join(", ")
);
}
Command::Status => {
let status = runtime_spine_status(&args.store)?;
println!("runtime spine status");
println!("store: {}", status.store);
println!("present: {}", status.present);
println!(
"runtime: {}",
status.runtime_id.unwrap_or_else(|| "missing".to_string())
);
println!(
"display: {}",
status.display_name.unwrap_or_else(|| "missing".to_string())
);
println!(
"sessions: {} active: {}",
status.sessions, status.active_sessions
);
println!("jobs: {} open: {}", status.jobs, status.open_jobs);
println!("job results: {}", status.job_results);
println!("events: {}", status.events);
println!(
"tool invocations: {} pending: {} receipts: {}",
status.tool_invocation_intents,
status.pending_tool_invocations,
status.tool_invocation_receipts
);
if !status.supported_document_types.is_empty() {
println!("documents: {}", status.supported_document_types.join(", "));
}
}
Command::ListJobs => {
let mut cache = runtime_spine_cache(&args.store)?;
cache.pull_all_backing_stores()?;
let mut jobs = cache.get_all::<EpiphanyRuntimeJob>()?;
let results = cache.get_all::<EpiphanyRuntimeJobResult>()?;
jobs.sort_by(|left, right| left.created_at.cmp(&right.created_at));
println!("runtime jobs");
for job in jobs {
let result = results.iter().find(|result| result.job_id == job.job_id);
println!(
"{}\t{:?}\t{}\t{}\t{}",
job.job_id,
job.status,
job.role,
result
.map(|result| result.result_id.as_str())
.unwrap_or("no-result"),
job.summary.replace(['\r', '\n', '\t'], " ")
);
}
}
Command::ListModelRequests => {
let mut cache = runtime_spine_cache(&args.store)?;
cache.pull_all_backing_stores()?;
let mut requests = cache.get_all::<EpiphanyModelRequest>()?;
requests.sort_by(|left, right| left.request_id.cmp(&right.request_id));
println!("model requests");
for request in requests {
let input_chars = request
.input
.iter()
.map(model_input_item_chars)
.sum::<usize>();
println!(
"{}\t{}\t{}\tinstructions={}\tinputItems={}\tinputChars={}\tcontract={}",
request.request_id,
request.provider,
request.model,
request.instructions.chars().count(),
request.input.len(),
input_chars,
request.output_contract_id.as_deref().unwrap_or("none")
);
}
}
Command::ListEvents => {
let mut cache = runtime_spine_cache(&args.store)?;
cache.pull_all_backing_stores()?;
let mut events = cache.get_all::<EpiphanyRuntimeEvent>()?;
events.sort_by(|left, right| {
left.occurred_at
.cmp(&right.occurred_at)
.then_with(|| left.event_id.cmp(&right.event_id))
});
println!("runtime events");
for event in events {
println!(
"{}\t{}\t{}\t{}\t{}",
event.occurred_at,
event.event_type,
event.job_id.as_deref().unwrap_or("no-job"),
event.source,
event.summary.replace(['\r', '\n', '\t'], " ")
);
}
}
Command::OpenSession {
session_id,
objective,
coordinator_note,
} => {
let session = create_runtime_session(
&args.store,
RuntimeSpineSessionOptions {
session_id,
objective,
created_at: now(),
coordinator_note,
},
)?;
println!("runtime session opened");
println!("session: {}", session.session_id);
println!("objective: {}", session.objective);
}
Command::RecordEvent {
event_id,
event_type,
source,
session_id,
job_id,
summary,
} => {
let event = append_runtime_event(
&args.store,
RuntimeSpineEventOptions {
event_id,
occurred_at: now(),
event_type,
source,
session_id,
job_id,
summary,
},
)?;
println!("runtime event recorded");
println!("event: {}", event.event_id);
println!("type: {}", event.event_type);
}
Command::OpenJob {
job_id,
session_id,
role,
summary,
artifact_refs,
} => {
let job = create_runtime_job(
&args.store,
RuntimeSpineJobOptions {
job_id,
session_id,
role,
created_at: now(),
summary,
artifact_refs,
},
)?;
println!("runtime job opened");
println!("job: {}", job.job_id);
println!("session: {}", job.session_id);
println!("role: {}", job.role);
}
Command::CompleteJob {
result_id,
job_id,
verdict,
summary,
next_safe_move,
evidence_refs,
artifact_refs,
} => {
let result = complete_runtime_job(
&args.store,
RuntimeSpineJobResultOptions {
result_id,
job_id,
completed_at: now(),
verdict,
summary,
next_safe_move,
evidence_refs,
artifact_refs,
},
)?;
println!("runtime job completed");
println!("result: {}", result.result_id);
println!("job: {}", result.job_id);
println!("verdict: {}", result.verdict);
}
Command::HelloFrame { output } => {
let bytes = write_runtime_hello_frame(&args.store, &output)?;
println!("cultnet hello frame written");
println!("path: {}", output.display());
println!("bytes: {bytes}");
}
Command::SchemaCatalog {
output,
include_schema_json,
} => {
let bytes = write_runtime_schema_catalog_json(&output, include_schema_json)?;
println!("cultnet schema catalog written");
println!("path: {}", output.display());
println!("bytes: {bytes}");
}
}
Ok(())
}
#[derive(Debug)]
struct Args {
store: PathBuf,
command: Command,
}
#[derive(Debug)]
enum Command {
Init {
runtime_id: String,
display_name: String,
},
Status,
ListJobs,
ListModelRequests,
ListEvents,
OpenSession {
session_id: String,
objective: String,
coordinator_note: String,
},
RecordEvent {
event_id: String,
event_type: String,
source: String,
session_id: Option<String>,
job_id: Option<String>,
summary: String,
},
OpenJob {
job_id: String,
session_id: String,
role: String,
summary: String,
artifact_refs: Vec<String>,
},
CompleteJob {
result_id: String,
job_id: String,
verdict: String,
summary: String,
next_safe_move: String,
evidence_refs: Vec<String>,
artifact_refs: Vec<String>,
},
HelloFrame {
output: PathBuf,
},
SchemaCatalog {
output: PathBuf,
include_schema_json: bool,
},
}
impl Args {
fn parse() -> Result<Self> {
let mut store = PathBuf::from(DEFAULT_STORE);
let mut positional = Vec::new();
let mut args = env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--store" => store = take_path(&mut args, "--store")?,
_ => positional.push(arg),
}
}
let command = parse_command(positional)?;
Ok(Self { store, command })
}
}
fn parse_command(mut args: Vec<String>) -> Result<Command> {
if args.is_empty() {
return Err(anyhow!(usage()));
}
let command = args.remove(0);
match command.as_str() {
"init" => {
let mut runtime_id = "epiphany-local".to_string();
let mut display_name = "Epiphany Local".to_string();
parse_options(args, |name, value| match name {
"--runtime-id" => {
runtime_id = value;
Ok(())
}
"--display-name" => {
display_name = value;
Ok(())
}
_ => Err(anyhow!("unknown init argument: {name}")),
})?;
Ok(Command::Init {
runtime_id,
display_name,
})
}
"status" => Ok(Command::Status),
"list-jobs" => Ok(Command::ListJobs),
"list-model-requests" => Ok(Command::ListModelRequests),
"list-events" => Ok(Command::ListEvents),
"open-session" => {
let mut session_id = format!("session-{}", Uuid::new_v4());
let mut objective = String::new();
let mut coordinator_note = String::new();
parse_options(args, |name, value| match name {
"--session-id" => {
session_id = value;
Ok(())
}
"--objective" => {
objective = value;
Ok(())
}
"--coordinator-note" => {
coordinator_note = value;
Ok(())
}
_ => Err(anyhow!("unknown open-session argument: {name}")),
})?;
if objective.trim().is_empty() {
return Err(anyhow!("open-session requires --objective"));
}
Ok(Command::OpenSession {
session_id,
objective,
coordinator_note,
})
}
"record-event" => {
let mut event_id = format!("event-{}", Uuid::new_v4());
let mut event_type = String::new();
let mut source = "operator".to_string();
let mut session_id = None;
let mut job_id = None;
let mut summary = String::new();
parse_options(args, |name, value| match name {
"--event-id" => {
event_id = value;
Ok(())
}
"--type" => {
event_type = value;
Ok(())
}
"--source" => {
source = value;
Ok(())
}
"--session-id" => {
session_id = Some(value);
Ok(())
}
"--job-id" => {
job_id = Some(value);
Ok(())
}
"--summary" => {
summary = value;
Ok(())
}
_ => Err(anyhow!("unknown record-event argument: {name}")),
})?;
if event_type.trim().is_empty() {
return Err(anyhow!("record-event requires --type"));
}
Ok(Command::RecordEvent {
event_id,
event_type,
source,
session_id,
job_id,
summary,
})
}
"open-job" => {
let mut job_id = format!("job-{}", Uuid::new_v4());
let mut session_id = String::new();
let mut role = String::new();
let mut summary = String::new();
let mut artifact_refs = Vec::new();
parse_options(args, |name, value| match name {
"--job-id" => {
job_id = value;
Ok(())
}
"--session-id" => {
session_id = value;
Ok(())
}
"--role" => {
role = value;
Ok(())
}
"--summary" => {
summary = value;
Ok(())
}
"--artifact-ref" => {
artifact_refs.push(value);
Ok(())
}
_ => Err(anyhow!("unknown open-job argument: {name}")),
})?;
if session_id.trim().is_empty() {
return Err(anyhow!("open-job requires --session-id"));
}
if role.trim().is_empty() {
return Err(anyhow!("open-job requires --role"));
}
Ok(Command::OpenJob {
job_id,
session_id,
role,
summary,
artifact_refs,
})
}
"complete-job" => {
let mut result_id = format!("result-{}", Uuid::new_v4());
let mut job_id = String::new();
let mut verdict = String::new();
let mut summary = String::new();
let mut next_safe_move = String::new();
let mut evidence_refs = Vec::new();
let mut artifact_refs = Vec::new();
parse_options(args, |name, value| match name {
"--result-id" => {
result_id = value;
Ok(())
}
"--job-id" => {
job_id = value;
Ok(())
}
"--verdict" => {
verdict = value;
Ok(())
}
"--summary" => {
summary = value;
Ok(())
}
"--next-safe-move" => {
next_safe_move = value;
Ok(())
}
"--evidence-ref" => {
evidence_refs.push(value);
Ok(())
}
"--artifact-ref" => {
artifact_refs.push(value);
Ok(())
}
_ => Err(anyhow!("unknown complete-job argument: {name}")),
})?;
if job_id.trim().is_empty() {
return Err(anyhow!("complete-job requires --job-id"));
}
if verdict.trim().is_empty() {
return Err(anyhow!("complete-job requires --verdict"));
}
if summary.trim().is_empty() {
return Err(anyhow!("complete-job requires --summary"));
}
Ok(Command::CompleteJob {
result_id,
job_id,
verdict,
summary,
next_safe_move,
evidence_refs,
artifact_refs,
})
}
"hello-frame" => {
let mut output = PathBuf::from(".epiphany-dogfood/runtime-spine/hello.cultnet");
parse_options(args, |name, value| match name {
"--output" => {
output = PathBuf::from(value);
Ok(())
}
_ => Err(anyhow!("unknown hello-frame argument: {name}")),
})?;
Ok(Command::HelloFrame { output })
}
"schema-catalog" => {
let mut output = PathBuf::from(".epiphany-dogfood/runtime-spine/schema-catalog.json");
let mut include_schema_json = false;
parse_options(args, |name, value| match name {
"--output" => {
output = PathBuf::from(value);
Ok(())
}
"--include-schema-json" => {
include_schema_json = parse_bool_flag(&value, "--include-schema-json")?;
Ok(())
}
_ => Err(anyhow!("unknown schema-catalog argument: {name}")),
})?;
Ok(Command::SchemaCatalog {
output,
include_schema_json,
})
}
_ => Err(anyhow!(usage())),
}
}
fn parse_options(
args: Vec<String>,
mut on_option: impl FnMut(&str, String) -> Result<()>,
) -> Result<()> {
let mut args = args.into_iter();
while let Some(name) = args.next() {
if !name.starts_with("--") {
return Err(anyhow!("unexpected positional argument: {name}"));
}
let value = args
.next()
.with_context(|| format!("{name} requires a value"))?;
on_option(&name, value)?;
}
Ok(())
}
fn take_path(args: &mut impl Iterator<Item = String>, name: &str) -> Result<PathBuf> {
args.next()
.map(PathBuf::from)
.ok_or_else(|| anyhow!("{name} requires a value"))
}
fn parse_bool_flag(value: &str, name: &str) -> Result<bool> {
match value {
"true" | "1" | "yes" => Ok(true),
"false" | "0" | "no" => Ok(false),
_ => Err(anyhow!("{name} must be true/false")),
}
}
fn now() -> String {
chrono::Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true)
}
fn usage() -> &'static str {
"usage: epiphany-runtime-spine [--store path] <init|status|list-jobs|list-model-requests|list-events|open-session|open-job|complete-job|record-event|hello-frame|schema-catalog>"
}
fn model_input_item_chars(item: &EpiphanyModelInputItem) -> usize {
match item {
EpiphanyModelInputItem::UserText { text }
| EpiphanyModelInputItem::AssistantText { text } => text.chars().count(),
EpiphanyModelInputItem::ToolResult { output, .. } => output.chars().count(),
}
}