Persist runtime events and artifacts

This commit is contained in:
ZacharyZhang-NY committed 2026-04-19 14:46:54 -04:00
1 parent 271c6cfec8
commit 98f975a4ca
7 files changed
+347 -4

No files matched your search

@@ -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::<Vec<_>>();
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::<Vec<_>>();
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()
@@ -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,
@@ -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<Task>,
pub approvals: Vec<Approval>,
pub runs: Vec<Run>,
pub run_events: Vec<RunEventRecord>,
pub artifacts: Vec<ArtifactRecord>,
pub activity: Vec<Activity>,
pub agent_messages: Vec<AgentMessage>,
pub documents: Vec<Document>,
@@ -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,
+84 -2
View File
@@ -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<String> {
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<String> {
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<Vec<RunEventRecord>> {
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<Vec<ArtifactRecord>> {
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<Vec<Activity>> {
match company_id {
Some(company_id) => query_many(
@@ -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)
);
}
+2
View File
@@ -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.