diff --git a/AgentCompany-Rust/crates/app_ui/src/app_root/runs.rs b/AgentCompany-Rust/crates/app_ui/src/app_root/runs.rs index ce766375..13c385d1 100644 --- a/AgentCompany-Rust/crates/app_ui/src/app_root/runs.rs +++ b/AgentCompany-Rust/crates/app_ui/src/app_root/runs.rs @@ -236,6 +236,36 @@ impl AppRoot { ) .child(run_timeline(&self.snapshot, run)), ) + .child( + div() + .p_5() + .rounded_lg() + .border_1() + .border_color(theme::panel_border()) + .bg(theme::panel()) + .child( + div() + .text_lg() + .text_color(theme::text_primary()) + .child("Runtime events"), + ) + .child(run_events_panel(&self.snapshot, run.id.as_str())), + ) + .child( + div() + .p_5() + .rounded_lg() + .border_1() + .border_color(theme::panel_border()) + .bg(theme::panel()) + .child( + div() + .text_lg() + .text_color(theme::text_primary()) + .child("Artifacts"), + ) + .child(run_artifacts_panel(&self.snapshot, run.id.as_str())), + ) }) .when(selected.is_none(), |container| { container.child( @@ -398,6 +428,110 @@ fn run_timeline(snapshot: &domain::ProfileSnapshot, run: &domain::Run) -> impl I )) } +fn run_events_panel(snapshot: &domain::ProfileSnapshot, run_id: &str) -> impl IntoElement { + let events = snapshot + .run_events + .iter() + .filter(|event| event.run_id == run_id) + .take(8) + .collect::>(); + if events.is_empty() { + div() + .mt(px(16.0)) + .text_sm() + .text_color(theme::text_muted()) + .child("No runtime events recorded yet.") + .into_any_element() + } else { + div() + .mt(px(16.0)) + .gap_3() + .children(events.into_iter().map(|event| { + div() + .py_3() + .border_b_1() + .border_color(theme::panel_border()) + .child( + div() + .text_sm() + .text_color(theme::text_primary()) + .child(event.title.clone()), + ) + .child( + div() + .mt(px(6.0)) + .text_xs() + .text_color(theme::text_muted()) + .child(format!( + "{} · {}", + event.event_kind, + short_date(event.created_at.as_str()) + )), + ) + .child( + div() + .mt(px(6.0)) + .text_sm() + .text_color(theme::text_secondary()) + .child(event.detail.clone()), + ) + })) + .into_any_element() + } +} + +fn run_artifacts_panel(snapshot: &domain::ProfileSnapshot, run_id: &str) -> impl IntoElement { + let artifacts = snapshot + .artifacts + .iter() + .filter(|artifact| artifact.run_id == run_id) + .take(8) + .collect::>(); + if artifacts.is_empty() { + div() + .mt(px(16.0)) + .text_sm() + .text_color(theme::text_muted()) + .child("No artifacts recorded yet.") + .into_any_element() + } else { + div() + .mt(px(16.0)) + .gap_3() + .children(artifacts.into_iter().map(|artifact| { + div() + .py_3() + .border_b_1() + .border_color(theme::panel_border()) + .child( + div() + .text_sm() + .text_color(theme::text_primary()) + .child(artifact.title.clone()), + ) + .child( + div() + .mt(px(6.0)) + .text_xs() + .text_color(theme::text_muted()) + .child(format!( + "{} · {}", + artifact.provider, + short_date(artifact.created_at.as_str()) + )), + ) + .child( + div() + .mt(px(6.0)) + .text_sm() + .text_color(theme::text_secondary()) + .child(artifact.external_id.clone()), + ) + })) + .into_any_element() + } +} + fn timeline_row(label: &'static str, value: String) -> impl IntoElement { div() .flex() diff --git a/AgentCompany-Rust/crates/domain/src/models.rs b/AgentCompany-Rust/crates/domain/src/models.rs index f7839d7f..1bde9c4e 100644 --- a/AgentCompany-Rust/crates/domain/src/models.rs +++ b/AgentCompany-Rust/crates/domain/src/models.rs @@ -200,6 +200,29 @@ pub struct Document { pub updated_at: String, } +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct RunEventRecord { + pub id: String, + pub company_id: String, + pub run_id: String, + pub event_kind: String, + pub title: String, + pub detail: String, + pub created_at: String, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct ArtifactRecord { + pub id: String, + pub company_id: String, + pub run_id: String, + pub provider: String, + pub external_id: String, + pub title: String, + pub metadata_json: String, + pub created_at: String, +} + #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct ThreadWaitState { pub id: String, diff --git a/AgentCompany-Rust/crates/domain/src/snapshot.rs b/AgentCompany-Rust/crates/domain/src/snapshot.rs index ff215ed4..10510b8a 100644 --- a/AgentCompany-Rust/crates/domain/src/snapshot.rs +++ b/AgentCompany-Rust/crates/domain/src/snapshot.rs @@ -1,6 +1,6 @@ use crate::{ - Activity, Agent, AgentMessage, Approval, Company, Connector, Document, Goal, Project, Run, - Task, Workspace, + Activity, Agent, AgentMessage, Approval, ArtifactRecord, Company, Connector, Document, Goal, + Project, Run, RunEventRecord, Task, Workspace, }; use serde::{Deserialize, Serialize}; @@ -19,6 +19,8 @@ pub struct ProfileSnapshot { pub tasks: Vec, pub approvals: Vec, pub runs: Vec, + pub run_events: Vec, + pub artifacts: Vec, pub activity: Vec, pub agent_messages: Vec, pub documents: Vec, diff --git a/AgentCompany-Rust/crates/storage/src/schema.rs b/AgentCompany-Rust/crates/storage/src/schema.rs index 1e56d52f..7e460341 100644 --- a/AgentCompany-Rust/crates/storage/src/schema.rs +++ b/AgentCompany-Rust/crates/storage/src/schema.rs @@ -183,6 +183,31 @@ pub fn initialize(conn: &Connection) -> Result<()> { foreign key(company_id) references companies(id) on delete cascade ); + create table if not exists run_events ( + id text primary key, + company_id text not null, + run_id text not null, + event_kind text not null, + title text not null, + detail text not null default '', + created_at text not null, + foreign key(company_id) references companies(id) on delete cascade, + foreign key(run_id) references runs(id) on delete cascade + ); + + create table if not exists artifacts ( + id text primary key, + company_id text not null, + run_id text not null, + provider text not null, + external_id text not null, + title text not null, + metadata_json text not null default '{}', + created_at text not null, + foreign key(company_id) references companies(id) on delete cascade, + foreign key(run_id) references runs(id) on delete cascade + ); + create table if not exists agent_messages ( id text primary key, company_id text not null, diff --git a/AgentCompany-Rust/crates/storage/src/store.rs b/AgentCompany-Rust/crates/storage/src/store.rs index 133802d8..810fc396 100644 --- a/AgentCompany-Rust/crates/storage/src/store.rs +++ b/AgentCompany-Rust/crates/storage/src/store.rs @@ -1,8 +1,8 @@ use crate::schema; use anyhow::{Context, Result, anyhow}; use domain::{ - Activity, Agent, AgentMessage, Approval, ApprovalState, Company, Connector, Document, Goal, - ProfileSnapshot, Project, Run, Task, Workspace, + Activity, Agent, AgentMessage, Approval, ApprovalState, ArtifactRecord, Company, Connector, + Document, Goal, ProfileSnapshot, Project, Run, RunEventRecord, Task, Workspace, }; use rusqlite::{Connection, OptionalExtension, params}; use std::{ @@ -86,6 +86,8 @@ impl ProfileStore { tasks: self.query_tasks(current_ref)?, approvals: self.query_approvals(current_ref)?, runs: self.query_runs(current_ref)?, + run_events: self.query_run_events(current_ref)?, + artifacts: self.query_artifacts(current_ref)?, activity: self.query_activity(current_ref)?, agent_messages: self.query_messages(current_ref)?, documents: self.query_documents(current_ref)?, @@ -233,6 +235,41 @@ impl ProfileStore { Ok(()) } + pub fn append_run_event( + &mut self, + company_id: &str, + run_id: &str, + event_kind: &str, + title: &str, + detail: &str, + ) -> Result { + let event_id = Uuid::new_v4().to_string(); + let ts = now_iso()?; + self.conn.execute( + "insert into run_events (id, company_id, run_id, event_kind, title, detail, created_at) values (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + params![event_id, company_id, run_id, event_kind, title.trim(), detail.trim(), ts], + )?; + Ok(event_id) + } + + pub fn register_artifact( + &mut self, + company_id: &str, + run_id: &str, + provider: &str, + external_id: &str, + title: &str, + metadata_json: &str, + ) -> Result { + let artifact_id = Uuid::new_v4().to_string(); + let ts = now_iso()?; + self.conn.execute( + "insert into artifacts (id, company_id, run_id, provider, external_id, title, metadata_json, created_at) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + params![artifact_id, company_id, run_id, provider.trim(), external_id.trim(), title.trim(), metadata_json.trim(), ts], + )?; + Ok(artifact_id) + } + pub fn decide_approval( &mut self, company_id: &str, @@ -642,6 +679,51 @@ impl ProfileStore { } } + fn query_run_events(&self, company_id: Option<&str>) -> Result> { + match company_id { + Some(company_id) => query_many( + &self.conn, + "select id, company_id, run_id, event_kind, title, detail, created_at from run_events where company_id = ?1 order by created_at desc limit 200", + [company_id], + |row| { + Ok(RunEventRecord { + id: row.get(0)?, + company_id: row.get(1)?, + run_id: row.get(2)?, + event_kind: row.get(3)?, + title: row.get(4)?, + detail: row.get(5)?, + created_at: row.get(6)?, + }) + }, + ), + None => Ok(Vec::new()), + } + } + + fn query_artifacts(&self, company_id: Option<&str>) -> Result> { + match company_id { + Some(company_id) => query_many( + &self.conn, + "select id, company_id, run_id, provider, external_id, title, metadata_json, created_at from artifacts where company_id = ?1 order by created_at desc limit 200", + [company_id], + |row| { + Ok(ArtifactRecord { + id: row.get(0)?, + company_id: row.get(1)?, + run_id: row.get(2)?, + provider: row.get(3)?, + external_id: row.get(4)?, + title: row.get(5)?, + metadata_json: row.get(6)?, + created_at: row.get(7)?, + }) + }, + ), + None => Ok(Vec::new()), + } + } + fn query_activity(&self, company_id: Option<&str>) -> Result> { match company_id { Some(company_id) => query_many( diff --git a/AgentCompany-Rust/crates/storage/tests/runtime_contract_snapshot_e2e.rs b/AgentCompany-Rust/crates/storage/tests/runtime_contract_snapshot_e2e.rs new file mode 100644 index 00000000..6f50e949 --- /dev/null +++ b/AgentCompany-Rust/crates/storage/tests/runtime_contract_snapshot_e2e.rs @@ -0,0 +1,75 @@ +use storage::{NewCompanyInput, ProfileStore}; +use tempfile::tempdir; + +#[test] +fn runtime_contract_entities_round_trip_through_snapshot() { + let sandbox = tempdir().expect("temp dir"); + let mut store = ProfileStore::open(sandbox.path()).expect("open store"); + + let company_id = store + .create_company(NewCompanyInput { + name: "Orbit Runtime".to_string(), + description: "Runtime contract verification".to_string(), + workspace_path: "/tmp/orbit-runtime".to_string(), + execution_mode: "ai_ceo".to_string(), + }) + .expect("create company"); + + let ts = support::time::now_iso().expect("timestamp"); + let run_id = uuid::Uuid::new_v4().to_string(); + let conn = rusqlite::Connection::open(store.db_path()).expect("open raw db"); + conn.execute( + "insert into runs (id, company_id, task_id, agent_id, workspace_id, connector_id, status, summary, error_message, model, cost_usd, started_at, finished_at, created_at, updated_at) values (?1, ?2, null, '__board__', null, 'anthropic', 'running', 'Runtime active', null, 'claude-sonnet', 0.25, ?3, null, ?3, ?3)", + rusqlite::params![run_id, company_id, ts], + ) + .expect("insert run"); + + let event_id = store + .append_run_event( + &company_id, + &run_id, + "tool_call", + "write_file", + "Requested README update", + ) + .expect("append run event"); + let artifact_id = store + .register_artifact( + &company_id, + &run_id, + "filesystem", + "/tmp/orbit-runtime/README.md", + "README update", + "{\"path\":\"/tmp/orbit-runtime/README.md\"}", + ) + .expect("register artifact"); + + let snapshot = store.snapshot().expect("snapshot"); + assert!(snapshot.run_events.iter().any(|event| { + event.id == event_id + && event.run_id == run_id + && event.event_kind == "tool_call" + && event.title == "write_file" + })); + assert!(snapshot.artifacts.iter().any(|artifact| { + artifact.id == artifact_id + && artifact.run_id == run_id + && artifact.provider == "filesystem" + && artifact.title == "README update" + })); + + let reopened = ProfileStore::open(sandbox.path()).expect("reopen store"); + let reopened_snapshot = reopened.snapshot().expect("reopened snapshot"); + assert!( + reopened_snapshot + .run_events + .iter() + .any(|event| event.id == event_id) + ); + assert!( + reopened_snapshot + .artifacts + .iter() + .any(|artifact| artifact.id == artifact_id) + ); +} diff --git a/AgentCompany-Rust/progress.txt b/AgentCompany-Rust/progress.txt index 27cf1837..140f60fe 100644 --- a/AgentCompany-Rust/progress.txt +++ b/AgentCompany-Rust/progress.txt @@ -10,3 +10,5 @@ Started: 2026-04-19 - Completed real settings/workspace state mutations in native Rust path: theme, locale, execution mode, and primary workspace. - Added native_settings_workspace_e2e.rs to verify mutation persistence across reopen. - Completed first native motion/state parity layer using GPUI animation wrappers for onboarding, inbox, and runs. +- Added runtime contract persistence entities for run events and artifacts, exposed them in ProfileSnapshot, and rendered them in the native runs inspector. +- Added runtime_contract_snapshot_e2e.rs to verify runtime event and artifact persistence across reopen.