mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-11 22:53:00 +00:00
refactor(events): tidy schema v2 plumbing
Quality cleanup on top of the v2 envelope commits: - fabro-workflow/src/event.rs: add ActorKind/ActorRef/RunProvenance to the existing ::fabro_types import block so call sites can use unqualified names (restores CLAUDE.md import style). Extract a node_stored_fields helper to collapse 4 near-identical match arms in stored_event_fields. Drop the no-op ..default() from the Agent arm where all 9 fields are set explicitly. - fabro-types/src/run_event/mod.rs: collapse 9 copies of the obj.get/as_str/to_string chain in from_ref behind an opt_str closure. - fabro-server/src/server.rs: dedupe the two identical error closures in api_event_envelope_from_store. Skip the typed ApiEventEnvelope roundtrip in sse_event_from_store so streamed events go straight from the wire Value to a JSON string. - fabro-workflow/src/handler/llm/api.rs: inline current_visit into its sole caller current_stage_event_scope. Also fixes pre-existing test compile breakage carried in by the v2 commits: restore the fabro_types::RunId import in support.rs (removed byb51403aebut still referenced by find_run_dir), and thread parallel_group_id/parallel_branch_id: None through 9 Event::Stage*/Event::Agent constructors in run_progress and store/dump tests that91d61016missed. No behavior change aside from the SSE hot path avoiding one full strong-type deserialize + reserialize per event. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b51403ae64
commit
747ccc0383
8 changed files with 62 additions and 106 deletions
|
|
@ -511,6 +511,8 @@ mod tests {
|
|||
name: "Plan".into(),
|
||||
index: 0,
|
||||
visit: 1,
|
||||
parallel_group_id: None,
|
||||
parallel_branch_id: None,
|
||||
duration_ms: 5000,
|
||||
status: "success".into(),
|
||||
preferred_label: None,
|
||||
|
|
@ -555,6 +557,8 @@ mod tests {
|
|||
},
|
||||
session_id: None,
|
||||
parent_session_id: None,
|
||||
parallel_group_id: None,
|
||||
parallel_branch_id: None,
|
||||
};
|
||||
|
||||
let stored = to_run_event(&fixtures::RUN_1, &event);
|
||||
|
|
|
|||
|
|
@ -479,6 +479,8 @@ mod tests {
|
|||
event,
|
||||
session_id: None,
|
||||
parent_session_id: None,
|
||||
parallel_group_id: None,
|
||||
parallel_branch_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -488,6 +490,8 @@ mod tests {
|
|||
name: name.into(),
|
||||
index: 0,
|
||||
visit: 1,
|
||||
parallel_group_id: None,
|
||||
parallel_branch_id: None,
|
||||
handler_type: String::new(),
|
||||
attempt: 1,
|
||||
max_attempts: 1,
|
||||
|
|
@ -512,6 +516,8 @@ mod tests {
|
|||
name: name.into(),
|
||||
index: 0,
|
||||
visit: 1,
|
||||
parallel_group_id: None,
|
||||
parallel_branch_id: None,
|
||||
duration_ms: 5000,
|
||||
status: "success".into(),
|
||||
preferred_label: None,
|
||||
|
|
@ -709,6 +715,8 @@ mod tests {
|
|||
name: "Code".into(),
|
||||
index: 0,
|
||||
visit: 1,
|
||||
parallel_group_id: None,
|
||||
parallel_branch_id: None,
|
||||
attempt: 2,
|
||||
max_attempts: 3,
|
||||
delay_ms: 1500,
|
||||
|
|
@ -954,6 +962,8 @@ mod tests {
|
|||
name: "Code".into(),
|
||||
index: 0,
|
||||
visit: 1,
|
||||
parallel_group_id: None,
|
||||
parallel_branch_id: None,
|
||||
attempt: 2,
|
||||
max_attempts: 3,
|
||||
delay_ms: 1500,
|
||||
|
|
@ -1159,6 +1169,8 @@ mod tests {
|
|||
name: "Code".into(),
|
||||
index: 0,
|
||||
visit: 1,
|
||||
parallel_group_id: None,
|
||||
parallel_branch_id: None,
|
||||
handler_type: "agent".into(),
|
||||
attempt: 1,
|
||||
max_attempts: 1,
|
||||
|
|
|
|||
|
|
@ -600,6 +600,8 @@ mod tests {
|
|||
name: "Code".to_string(),
|
||||
index: 1,
|
||||
visit: 2,
|
||||
parallel_group_id: None,
|
||||
parallel_branch_id: None,
|
||||
duration_ms: 250,
|
||||
status: "partial_success".to_string(),
|
||||
preferred_label: None,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ use fabro_config::Storage;
|
|||
use fabro_server::bind::Bind;
|
||||
use fabro_store::EventEnvelope;
|
||||
use fabro_test::TestContext;
|
||||
use fabro_types::RunId;
|
||||
use serde_json::Value;
|
||||
use shlex::try_quote;
|
||||
|
||||
|
|
|
|||
|
|
@ -1480,8 +1480,8 @@ fn event_matches_run_filter(event: &EventEnvelope, run_filter: Option<&HashSet<R
|
|||
}
|
||||
|
||||
fn sse_event_from_store(event: &EventEnvelope) -> Option<Event> {
|
||||
let event = api_event_envelope_from_store(event).ok()?;
|
||||
let data = serde_json::to_string(&event).ok()?;
|
||||
let wire = event.to_wire_value().ok()?;
|
||||
let data = serde_json::to_string(&wire).ok()?;
|
||||
let data = redact_jsonl_line(&data);
|
||||
Some(Event::default().data(data))
|
||||
}
|
||||
|
|
@ -2381,20 +2381,15 @@ fn octet_stream_response(bytes: Bytes) -> Response {
|
|||
|
||||
#[allow(clippy::result_large_err)]
|
||||
fn api_event_envelope_from_store(event: &EventEnvelope) -> Result<ApiEventEnvelope, Response> {
|
||||
let value = event.to_wire_value().map_err(|err| {
|
||||
fn serialize_error(err: impl std::fmt::Display) -> Response {
|
||||
ApiError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to serialize stored event: {err}"),
|
||||
)
|
||||
.into_response()
|
||||
})?;
|
||||
serde_json::from_value(value).map_err(|err| {
|
||||
ApiError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to serialize stored event: {err}"),
|
||||
)
|
||||
.into_response()
|
||||
})
|
||||
}
|
||||
let value = event.to_wire_value().map_err(serialize_error)?;
|
||||
serde_json::from_value(value).map_err(serialize_error)
|
||||
}
|
||||
|
||||
fn clear_live_run_state(run: &mut ManagedRun) {
|
||||
|
|
|
|||
|
|
@ -595,6 +595,7 @@ impl RunEvent {
|
|||
let obj = value.as_object().ok_or_else(|| {
|
||||
<serde_json::Error as DeError>::custom("run event must be a JSON object")
|
||||
})?;
|
||||
let opt_str = |key: &str| obj.get(key).and_then(Value::as_str).map(str::to_string);
|
||||
let id = obj.get("id").and_then(Value::as_str).ok_or_else(|| {
|
||||
<serde_json::Error as DeError>::custom("missing or non-string field: id")
|
||||
})?;
|
||||
|
|
@ -621,38 +622,14 @@ impl RunEvent {
|
|||
id: id.to_string(),
|
||||
ts,
|
||||
run_id,
|
||||
node_id: obj
|
||||
.get("node_id")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string),
|
||||
node_label: obj
|
||||
.get("node_label")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string),
|
||||
stage_id: obj
|
||||
.get("stage_id")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string),
|
||||
parallel_group_id: obj
|
||||
.get("parallel_group_id")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string),
|
||||
parallel_branch_id: obj
|
||||
.get("parallel_branch_id")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string),
|
||||
session_id: obj
|
||||
.get("session_id")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string),
|
||||
parent_session_id: obj
|
||||
.get("parent_session_id")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string),
|
||||
tool_call_id: obj
|
||||
.get("tool_call_id")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string),
|
||||
node_id: opt_str("node_id"),
|
||||
node_label: opt_str("node_label"),
|
||||
stage_id: opt_str("stage_id"),
|
||||
parallel_group_id: opt_str("parallel_group_id"),
|
||||
parallel_branch_id: opt_str("parallel_branch_id"),
|
||||
session_id: opt_str("session_id"),
|
||||
parent_session_id: opt_str("parent_session_id"),
|
||||
tool_call_id: opt_str("tool_call_id"),
|
||||
actor,
|
||||
event,
|
||||
properties: &properties,
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ use std::sync::atomic::{AtomicI64, Ordering};
|
|||
|
||||
use ::fabro_types::run_event as fabro_types;
|
||||
use ::fabro_types::{
|
||||
BilledTokenCounts, RunBlobId, RunControlAction, RunEvent, RunId, StageId, StageStatus,
|
||||
StatusReason,
|
||||
ActorKind, ActorRef, BilledTokenCounts, RunBlobId, RunControlAction, RunEvent, RunId,
|
||||
RunProvenance, StageId, StageStatus, StatusReason,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use chrono::Utc;
|
||||
|
|
@ -55,7 +55,7 @@ pub enum Event {
|
|||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
db_prefix: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
provenance: Option<::fabro_types::RunProvenance>,
|
||||
provenance: Option<RunProvenance>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
manifest_blob: Option<RunBlobId>,
|
||||
},
|
||||
|
|
@ -1289,13 +1289,22 @@ struct StoredEventFields {
|
|||
parallel_group_id: Option<String>,
|
||||
parallel_branch_id: Option<String>,
|
||||
tool_call_id: Option<String>,
|
||||
actor: Option<::fabro_types::ActorRef>,
|
||||
actor: Option<ActorRef>,
|
||||
}
|
||||
|
||||
fn default_node_label(node_id: Option<&String>, node_label: Option<String>) -> Option<String> {
|
||||
node_label.or_else(|| node_id.cloned())
|
||||
}
|
||||
|
||||
fn node_stored_fields(node_id: Option<String>) -> StoredEventFields {
|
||||
let node_label = default_node_label(node_id.as_ref(), None);
|
||||
StoredEventFields {
|
||||
node_id,
|
||||
node_label,
|
||||
..StoredEventFields::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn billed_token_counts_from_llm(usage: &LlmTokenCounts) -> BilledTokenCounts {
|
||||
BilledTokenCounts {
|
||||
input_tokens: usage.input_tokens,
|
||||
|
|
@ -1383,15 +1392,7 @@ fn stored_event_fields(event: &Event) -> StoredEventFields {
|
|||
| Event::CommandStarted { node_id, .. }
|
||||
| Event::CommandCompleted { node_id, .. }
|
||||
| Event::AgentCliStarted { node_id, .. }
|
||||
| Event::AgentCliCompleted { node_id, .. } => {
|
||||
let node_id = Some(node_id.clone());
|
||||
let node_label = default_node_label(node_id.as_ref(), None);
|
||||
StoredEventFields {
|
||||
node_id,
|
||||
node_label,
|
||||
..StoredEventFields::default()
|
||||
}
|
||||
}
|
||||
| Event::AgentCliCompleted { node_id, .. } => node_stored_fields(Some(node_id.clone())),
|
||||
Event::Agent {
|
||||
stage,
|
||||
visit,
|
||||
|
|
@ -1416,17 +1417,9 @@ fn stored_event_fields(event: &Event) -> StoredEventFields {
|
|||
parallel_branch_id: parallel_branch_id.clone(),
|
||||
tool_call_id,
|
||||
actor,
|
||||
..StoredEventFields::default()
|
||||
}
|
||||
}
|
||||
Event::GitCommit { node_id, .. } => {
|
||||
let node_label = default_node_label(node_id.as_ref(), None);
|
||||
StoredEventFields {
|
||||
node_id: node_id.clone(),
|
||||
node_label,
|
||||
..StoredEventFields::default()
|
||||
}
|
||||
}
|
||||
Event::GitCommit { node_id, .. } => node_stored_fields(node_id.clone()),
|
||||
Event::ParallelBranchStarted {
|
||||
parallel_group_id,
|
||||
parallel_branch_id,
|
||||
|
|
@ -1453,35 +1446,16 @@ fn stored_event_fields(event: &Event) -> StoredEventFields {
|
|||
| Event::InterviewStarted { stage, .. }
|
||||
| Event::InterviewTimeout { stage, .. }
|
||||
| Event::InterviewInterrupted { stage, .. }
|
||||
| Event::Failover { stage, .. } => {
|
||||
let node_id = Some(stage.clone());
|
||||
let node_label = default_node_label(node_id.as_ref(), None);
|
||||
StoredEventFields {
|
||||
node_id,
|
||||
node_label,
|
||||
..StoredEventFields::default()
|
||||
}
|
||||
}
|
||||
Event::StallWatchdogTimeout { node, .. } => {
|
||||
let node_id = Some(node.clone());
|
||||
let node_label = default_node_label(node_id.as_ref(), None);
|
||||
StoredEventFields {
|
||||
node_id,
|
||||
node_label,
|
||||
..StoredEventFields::default()
|
||||
}
|
||||
}
|
||||
| Event::Failover { stage, .. } => node_stored_fields(Some(stage.clone())),
|
||||
Event::StallWatchdogTimeout { node, .. } => node_stored_fields(Some(node.clone())),
|
||||
_ => StoredEventFields::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn actor_from_provenance(
|
||||
provenance: &::fabro_types::RunProvenance,
|
||||
) -> Option<::fabro_types::ActorRef> {
|
||||
let subject = provenance.subject.as_ref()?;
|
||||
let login = subject.login.clone()?;
|
||||
Some(::fabro_types::ActorRef {
|
||||
kind: ::fabro_types::ActorKind::User,
|
||||
fn actor_from_provenance(provenance: &RunProvenance) -> Option<ActorRef> {
|
||||
let login = provenance.subject.as_ref()?.login.clone()?;
|
||||
Some(ActorRef {
|
||||
kind: ActorKind::User,
|
||||
id: Some(login.clone()),
|
||||
display: Some(login),
|
||||
})
|
||||
|
|
@ -1495,13 +1469,10 @@ fn agent_tool_call_id(event: &AgentEvent) -> Option<&str> {
|
|||
}
|
||||
}
|
||||
|
||||
fn agent_actor_for_event(
|
||||
event: &AgentEvent,
|
||||
session_id: Option<&str>,
|
||||
) -> Option<::fabro_types::ActorRef> {
|
||||
fn agent_actor_for_event(event: &AgentEvent, session_id: Option<&str>) -> Option<ActorRef> {
|
||||
match event {
|
||||
AgentEvent::AssistantMessage { model, .. } => Some(::fabro_types::ActorRef {
|
||||
kind: ::fabro_types::ActorKind::Agent,
|
||||
AgentEvent::AssistantMessage { model, .. } => Some(ActorRef {
|
||||
kind: ActorKind::Agent,
|
||||
id: session_id.map(str::to_string),
|
||||
display: Some(model.clone()),
|
||||
}),
|
||||
|
|
@ -3281,16 +3252,14 @@ mod tests {
|
|||
},
|
||||
);
|
||||
let actor = stored.actor.as_ref().expect("actor set");
|
||||
assert_eq!(actor.kind, ::fabro_types::ActorKind::Agent);
|
||||
assert_eq!(actor.kind, ActorKind::Agent);
|
||||
assert_eq!(actor.id.as_deref(), Some("ses_agent"));
|
||||
assert_eq!(actor.display.as_deref(), Some("claude-sonnet"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_created_populates_user_actor_from_provenance() {
|
||||
use ::fabro_types::{
|
||||
Graph, RunAuthMethod, RunProvenance, RunSubjectProvenance, Settings, fixtures,
|
||||
};
|
||||
use ::fabro_types::{Graph, RunAuthMethod, RunSubjectProvenance, Settings, fixtures};
|
||||
|
||||
let provenance = RunProvenance {
|
||||
server: None,
|
||||
|
|
@ -3322,7 +3291,7 @@ mod tests {
|
|||
},
|
||||
);
|
||||
let actor = stored.actor.as_ref().expect("actor set");
|
||||
assert_eq!(actor.kind, ::fabro_types::ActorKind::User);
|
||||
assert_eq!(actor.kind, ActorKind::User);
|
||||
assert_eq!(actor.id.as_deref(), Some("alice"));
|
||||
assert_eq!(actor.display.as_deref(), Some("alice"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,10 +37,6 @@ fn build_profile(model: &str, provider: Provider) -> Box<dyn AgentProfile> {
|
|||
}
|
||||
}
|
||||
|
||||
fn current_visit(context: &Context) -> u32 {
|
||||
u32::try_from(visit_from_context(context)).unwrap_or(u32::MAX)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct StageEventScope {
|
||||
visit: u32,
|
||||
|
|
@ -50,7 +46,7 @@ struct StageEventScope {
|
|||
|
||||
fn current_stage_event_scope(context: &Context) -> StageEventScope {
|
||||
StageEventScope {
|
||||
visit: current_visit(context),
|
||||
visit: u32::try_from(visit_from_context(context)).unwrap_or(u32::MAX),
|
||||
parallel_group_id: context.parallel_group_id(),
|
||||
parallel_branch_id: context.parallel_branch_id(),
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue