Persist runtime events and artifacts
This commit is contained in:
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,
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user