mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-07 08:27:12 +00:00
refactor: unify workflow stored event model
Add a shared StoredEvent schema in fabro-types and switch workflow, store, CLI, and server event handling to use it directly. This removes the writer/reader mismatch around flattened failure data, updates affected projections and progress rendering, and refreshes the fixture/snapshot coverage around the canonical event shape.
This commit is contained in:
parent
14516f3e9c
commit
871bc500e4
32 changed files with 2019 additions and 543 deletions
|
|
@ -1,6 +1,7 @@
|
|||
use std::convert::TryFrom;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use fabro_types::StoredEvent;
|
||||
use fabro_workflow::event::RunNoticeLevel;
|
||||
use fabro_workflow::outcome::{StageUsage, compute_stage_cost};
|
||||
use serde_json::{Map, Value};
|
||||
|
|
@ -240,10 +241,7 @@ pub(super) enum ProgressEvent {
|
|||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub(super) fn from_envelope_fields(
|
||||
event_name: &str,
|
||||
fields: &Map<String, Value>,
|
||||
) -> Option<ProgressEvent> {
|
||||
fn from_envelope_fields(event_name: &str, fields: &Map<String, Value>) -> Option<ProgressEvent> {
|
||||
match event_name {
|
||||
"run.started" => Some(ProgressEvent::WorkflowStarted {
|
||||
worktree_dir: prop_string_field(fields, "worktree_dir"),
|
||||
|
|
@ -457,6 +455,26 @@ pub(super) fn from_envelope_fields(
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) fn from_stored_event(stored: &StoredEvent) -> Option<ProgressEvent> {
|
||||
let Value::Object(fields) = stored.to_value().ok()? else {
|
||||
return None;
|
||||
};
|
||||
let event_name = fields.get("event")?.as_str()?;
|
||||
from_envelope_fields(event_name, &fields)
|
||||
}
|
||||
|
||||
pub(super) fn from_json_line(line: &str) -> Option<ProgressEvent> {
|
||||
if let Ok(stored) = StoredEvent::from_json_str(line) {
|
||||
return from_stored_event(&stored);
|
||||
}
|
||||
|
||||
let Value::Object(fields) = serde_json::from_str(line).ok()? else {
|
||||
return None;
|
||||
};
|
||||
let event_name = fields.get("event")?.as_str()?;
|
||||
from_envelope_fields(event_name, &fields)
|
||||
}
|
||||
|
||||
fn parse_run_notice_level(level: Option<&str>) -> RunNoticeLevel {
|
||||
match level.unwrap_or("info") {
|
||||
"warn" => RunNoticeLevel::Warn,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
use serde_json::Value;
|
||||
|
||||
use fabro_workflow::event::RunEventEnvelope;
|
||||
use fabro_types::StoredEvent;
|
||||
|
||||
mod event;
|
||||
mod info_display;
|
||||
|
|
@ -9,7 +7,7 @@ mod setup_display;
|
|||
mod stage_display;
|
||||
mod styles;
|
||||
|
||||
use event::{ProgressEvent, from_envelope_fields};
|
||||
use event::{ProgressEvent, from_json_line, from_stored_event};
|
||||
use info_display::InfoDisplay;
|
||||
use renderer::ProgressRenderer;
|
||||
use setup_display::SetupDisplay;
|
||||
|
|
@ -68,23 +66,14 @@ impl ProgressUI {
|
|||
}
|
||||
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
pub(crate) fn handle_event(&mut self, event: &RunEventEnvelope) {
|
||||
let Ok(Value::Object(envelope)) = serde_json::to_value(event) else {
|
||||
return;
|
||||
};
|
||||
if let Some(progress_event) = from_envelope_fields(&event.event, &envelope) {
|
||||
pub(crate) fn handle_event(&mut self, event: &StoredEvent) {
|
||||
if let Some(progress_event) = from_stored_event(event) {
|
||||
self.dispatch(progress_event);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn handle_json_line(&mut self, line: &str) {
|
||||
let Ok(Value::Object(envelope)) = serde_json::from_str(line) else {
|
||||
return;
|
||||
};
|
||||
let Some(event_name) = envelope.get("event").and_then(|value| value.as_str()) else {
|
||||
return;
|
||||
};
|
||||
if let Some(progress_event) = from_envelope_fields(event_name, &envelope) {
|
||||
if let Some(progress_event) = from_json_line(line) {
|
||||
self.dispatch(progress_event);
|
||||
}
|
||||
}
|
||||
|
|
@ -428,7 +417,9 @@ mod tests {
|
|||
use fabro_agent::{AgentEvent, SandboxEvent};
|
||||
use fabro_llm::types::Usage;
|
||||
use fabro_types::fixtures;
|
||||
use fabro_workflow::event::{RunNoticeLevel, WorkflowRunEvent, canonicalize_event};
|
||||
use fabro_workflow::event::{
|
||||
RunNoticeLevel, WorkflowRunEvent, canonicalize_event, to_stored_event,
|
||||
};
|
||||
use fabro_workflow::outcome::StageUsage;
|
||||
|
||||
use super::*;
|
||||
|
|
@ -470,13 +461,13 @@ mod tests {
|
|||
}
|
||||
|
||||
fn emit(ui: &mut ProgressUI, event: WorkflowRunEvent) {
|
||||
let envelope = canonicalize_event(&fixtures::RUN_1, &event);
|
||||
ui.handle_event(&envelope);
|
||||
let stored = to_stored_event(&fixtures::RUN_1, &event);
|
||||
ui.handle_event(&stored);
|
||||
}
|
||||
|
||||
fn emit_ref(ui: &mut ProgressUI, event: &WorkflowRunEvent) {
|
||||
let envelope = canonicalize_event(&fixtures::RUN_1, event);
|
||||
ui.handle_event(&envelope);
|
||||
let stored = to_stored_event(&fixtures::RUN_1, event);
|
||||
ui.handle_event(&stored);
|
||||
}
|
||||
|
||||
fn agent_event(stage: &str, event: AgentEvent) -> WorkflowRunEvent {
|
||||
|
|
|
|||
|
|
@ -513,7 +513,20 @@ mod tests {
|
|||
"id": format!("evt-{run_id}-stage-completed"),
|
||||
"ts": "2026-03-27T12:00:01.000Z",
|
||||
"run_id": run_id.to_string(),
|
||||
"event": "stage.completed"
|
||||
"event": "stage.completed",
|
||||
"node_id": "code",
|
||||
"node_label": "Code",
|
||||
"properties": {
|
||||
"index": 1,
|
||||
"duration_ms": 1,
|
||||
"status": "success",
|
||||
"response": "Implemented",
|
||||
"notes": "all good",
|
||||
"files_touched": ["src/lib.rs"],
|
||||
"node_visits": {"code": 2},
|
||||
"attempt": 1,
|
||||
"max_attempts": 1
|
||||
}
|
||||
}),
|
||||
&run_id,
|
||||
)
|
||||
|
|
@ -562,7 +575,7 @@ mod tests {
|
|||
);
|
||||
let node_status: NodeStatusRecord =
|
||||
read_json(&output.path().join("nodes/code/visit-2/status.json"));
|
||||
assert_eq!(node_status.status, StageStatus::PartialSuccess);
|
||||
assert_eq!(node_status.status, StageStatus::Success);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(output.path().join("nodes/code/visit-2/stdout.log")).unwrap(),
|
||||
"stdout line"
|
||||
|
|
|
|||
|
|
@ -453,12 +453,8 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"event": "sandbox.ready",
|
||||
"id": "[EVENT_ID]",
|
||||
"properties": {
|
||||
"cpu": null,
|
||||
"duration_ms": "[DURATION_MS]",
|
||||
"memory": null,
|
||||
"name": null,
|
||||
"provider": "local",
|
||||
"url": null
|
||||
"provider": "local"
|
||||
},
|
||||
"run_id": "[ULID]",
|
||||
"ts": "[TIMESTAMP]"
|
||||
|
|
@ -521,17 +517,12 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"internal.thread_id": null
|
||||
},
|
||||
"duration_ms": "[DURATION_MS]",
|
||||
"files_touched": [],
|
||||
"index": 0,
|
||||
"max_attempts": 1,
|
||||
"node_visits": {
|
||||
"start": 1
|
||||
},
|
||||
"notes": null,
|
||||
"preferred_label": null,
|
||||
"status": "success",
|
||||
"suggested_next_ids": [],
|
||||
"usage": null
|
||||
"status": "success"
|
||||
},
|
||||
"run_id": "[ULID]",
|
||||
"ts": "[TIMESTAMP]"
|
||||
|
|
@ -540,10 +531,8 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"event": "edge.selected",
|
||||
"id": "[EVENT_ID]",
|
||||
"properties": {
|
||||
"condition": null,
|
||||
"from_node": "start",
|
||||
"is_jump": false,
|
||||
"label": null,
|
||||
"reason": "unconditional",
|
||||
"stage_status": "success",
|
||||
"to_node": "approve"
|
||||
|
|
|
|||
|
|
@ -63,28 +63,28 @@ fn logs_completed_run_outputs_raw_ndjson() {
|
|||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{"event":"run.created","id":"[EVENT_ID]","properties":{"graph":{"attrs":{"goal":{"String":"Run tests and report results"},"rankdir":{"String":"LR"}},"edges":[{"attrs":{},"from":"start","to":"run_tests"},{"attrs":{},"from":"run_tests","to":"report"},{"attrs":{},"from":"report","to":"exit"}],"name":"Simple","nodes":{"exit":{"attrs":{"label":{"String":"Exit"},"shape":{"String":"Msquare"}},"id":"exit"},"report":{"attrs":{"label":{"String":"Report"},"prompt":{"String":"Summarize the test results"}},"id":"report"},"run_tests":{"attrs":{"label":{"String":"Run Tests"},"prompt":{"String":"Run the test suite and report results"}},"id":"run_tests"},"start":{"attrs":{"label":{"String":"Start"},"shape":{"String":"Mdiamond"}},"id":"start"}}},"host_repo_path":"[TEMP_DIR]","labels":{},"run_dir":"[STORAGE_DIR]/runs/20260403-[ULID]","settings":{"auto_approve":true,"dry_run":true,"fabro":{"root":"fabro/"},"features":{"retros":false,"session_sandboxes":false},"goal":"Run tests and report results","hooks":[{"blocking":true,"command":"cargo fmt","event":"post_tool_use","matcher":"write_file|edit_file|apply_patch","name":"cargo-fmt","sandbox":null,"timeout_ms":null}],"llm":{"fallbacks":null,"model":"claude-sonnet-4-6","provider":"anthropic"},"mode":"standalone","no_retro":true,"pull_request":{"auto_merge":false,"draft":false,"enabled":true,"merge_strategy":"squash"},"sandbox":{"daytona":{"auto_stop_interval":30,"labels":{"repo":"fabro-sh/fabro"},"network":null,"skip_clone":false,"snapshot":{"cpu":4,"disk":20,"dockerfile":"FROM ubuntu:24.04/n/nRUN apt-get update && apt-get install -y --no-install-recommends curl git ca-certificates build-essential pkg-config libssl-dev unzip python3 && rm -rf /var/lib/apt/lists/*/n/n# GitHub CLI/nRUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && echo \"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main\" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null && apt-get update && apt-get install -y --no-install-recommends gh && rm -rf /var/lib/apt/lists/*/n/n# Rust/nRUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y/nENV PATH=\"/root/.cargo/bin:${PATH}\"/nRUN cargo install cargo-nextest --locked/nENV CARGO_INCREMENTAL=0/n/n# Bun/nRUN curl -fsSL https://bun.sh/install | bash/nENV PATH=\"/root/.bun/bin:${PATH}\"/n/nWORKDIR /root/n","memory":8,"name":"fabro-v6"}},"devcontainer":null,"env":null,"local":null,"preserve":null,"provider":"local"},"storage_dir":"[STORAGE_DIR]","version":1},"workflow_slug":"simple","workflow_source":"digraph Simple {/n graph [goal=\"Run tests and report results\"]/n rankdir=LR/n/n start [shape=Mdiamond, label=\"Start\"]/n exit [shape=Msquare, label=\"Exit\"]/n/n run_tests [label=\"Run Tests\", prompt=\"Run the test suite and report results\"]/n report [label=\"Report\", prompt=\"Summarize the test results\"]/n/n start -> run_tests -> report -> exit/n}/n","working_directory":"[TEMP_DIR]"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"run.created","id":"[EVENT_ID]","properties":{"graph":{"attrs":{"goal":{"String":"Run tests and report results"},"rankdir":{"String":"LR"}},"edges":[{"attrs":{},"from":"start","to":"run_tests"},{"attrs":{},"from":"run_tests","to":"report"},{"attrs":{},"from":"report","to":"exit"}],"name":"Simple","nodes":{"exit":{"attrs":{"label":{"String":"Exit"},"shape":{"String":"Msquare"}},"id":"exit"},"report":{"attrs":{"label":{"String":"Report"},"prompt":{"String":"Summarize the test results"}},"id":"report"},"run_tests":{"attrs":{"label":{"String":"Run Tests"},"prompt":{"String":"Run the test suite and report results"}},"id":"run_tests"},"start":{"attrs":{"label":{"String":"Start"},"shape":{"String":"Mdiamond"}},"id":"start"}}},"host_repo_path":"[TEMP_DIR]","labels":{},"run_dir":"[STORAGE_DIR]/runs/20260404-[ULID]","settings":{"auto_approve":true,"dry_run":true,"fabro":{"root":"fabro/"},"features":{"retros":false,"session_sandboxes":false},"goal":"Run tests and report results","hooks":[{"blocking":true,"command":"cargo fmt","event":"post_tool_use","matcher":"write_file|edit_file|apply_patch","name":"cargo-fmt","sandbox":null,"timeout_ms":null}],"llm":{"fallbacks":null,"model":"claude-sonnet-4-6","provider":"anthropic"},"mode":"standalone","no_retro":true,"pull_request":{"auto_merge":false,"draft":false,"enabled":true,"merge_strategy":"squash"},"sandbox":{"daytona":{"auto_stop_interval":30,"labels":{"repo":"fabro-sh/fabro"},"network":null,"skip_clone":false,"snapshot":{"cpu":4,"disk":20,"dockerfile":"FROM ubuntu:24.04/n/nRUN apt-get update && apt-get install -y --no-install-recommends curl git ca-certificates build-essential pkg-config libssl-dev unzip python3 && rm -rf /var/lib/apt/lists/*/n/n# GitHub CLI/nRUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && echo \"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main\" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null && apt-get update && apt-get install -y --no-install-recommends gh && rm -rf /var/lib/apt/lists/*/n/n# Rust/nRUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y/nENV PATH=\"/root/.cargo/bin:${PATH}\"/nRUN cargo install cargo-nextest --locked/nENV CARGO_INCREMENTAL=0/n/n# Bun/nRUN curl -fsSL https://bun.sh/install | bash/nENV PATH=\"/root/.bun/bin:${PATH}\"/n/nWORKDIR /root/n","memory":8,"name":"fabro-v6"}},"devcontainer":null,"env":null,"local":null,"preserve":null,"provider":"local"},"storage_dir":"[STORAGE_DIR]","version":1},"workflow_slug":"simple","workflow_source":"digraph Simple {/n graph [goal=\"Run tests and report results\"]/n rankdir=LR/n/n start [shape=Mdiamond, label=\"Start\"]/n exit [shape=Msquare, label=\"Exit\"]/n/n run_tests [label=\"Run Tests\", prompt=\"Run the test suite and report results\"]/n report [label=\"Report\", prompt=\"Summarize the test results\"]/n/n start -> run_tests -> report -> exit/n}/n","working_directory":"[TEMP_DIR]"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"run.submitted","id":"[EVENT_ID]","properties":{},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"run.starting","id":"[EVENT_ID]","properties":{"reason":"sandbox_initializing"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"sandbox.initializing","id":"[EVENT_ID]","properties":{"provider":"local"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"sandbox.ready","id":"[EVENT_ID]","properties":{"cpu":null,"duration_ms": [DURATION_MS],"memory":null,"name":null,"provider":"local","url":null},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"sandbox.ready","id":"[EVENT_ID]","properties":{"duration_ms": [DURATION_MS],"provider":"local"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"sandbox.initialized","id":"[EVENT_ID]","properties":{"provider":"local","working_directory":"[TEMP_DIR]"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"run.started","id":"[EVENT_ID]","properties":{"goal":"Run tests and report results","name":"Simple"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"run.running","id":"[EVENT_ID]","properties":{},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"stage.started","id":"[EVENT_ID]","node_id":"start","node_label":"Start","properties":{"attempt":1,"handler_type":"start","index":0,"max_attempts":1},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"stage.completed","id":"[EVENT_ID]","node_id":"start","node_label":"Start","properties":{"attempt":1,"context_values":{"current.preamble":"Goal: Run tests and report results/n","current_node":"start","graph.goal":"Run tests and report results","graph.rankdir":"LR","internal.fidelity":"compact","internal.node_visit_count":1,"internal.run_id":"[ULID]","internal.thread_id":null},"duration_ms": [DURATION_MS],"files_touched":[],"index":0,"max_attempts":1,"node_visits":{"start":1},"notes":"[Simulated] start","preferred_label":null,"status":"success","suggested_next_ids":[],"usage":null},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"edge.selected","id":"[EVENT_ID]","properties":{"condition":null,"from_node":"start","is_jump":false,"label":null,"reason":"unconditional","stage_status":"success","to_node":"run_tests"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"stage.completed","id":"[EVENT_ID]","node_id":"start","node_label":"Start","properties":{"attempt":1,"context_values":{"current.preamble":"Goal: Run tests and report results/n","current_node":"start","graph.goal":"Run tests and report results","graph.rankdir":"LR","internal.fidelity":"compact","internal.node_visit_count":1,"internal.run_id":"[ULID]","internal.thread_id":null},"duration_ms": [DURATION_MS],"index":0,"max_attempts":1,"node_visits":{"start":1},"notes":"[Simulated] start","status":"success"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"edge.selected","id":"[EVENT_ID]","properties":{"from_node":"start","is_jump":false,"reason":"unconditional","stage_status":"success","to_node":"run_tests"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"checkpoint.completed","id":"[EVENT_ID]","node_id":"start","node_label":"start","properties":{"completed_nodes":["start"],"context_values":{"current.preamble":"Goal: Run tests and report results/n","current_node":"start","failure_class":"","failure_signature":"","graph.goal":"Run tests and report results","graph.rankdir":"LR","internal.fidelity":"compact","internal.node_visit_count":1,"internal.retry_count.start":0,"internal.run_id":"[ULID]","internal.thread_id":null,"outcome":"success"},"current_node":"start","next_node_id":"run_tests","node_outcomes":{"start":{"notes":"[Simulated] start","status":"success","usage":null}},"node_visits":{"start":1},"status":"success"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"stage.started","id":"[EVENT_ID]","node_id":"run_tests","node_label":"Run Tests","properties":{"attempt":1,"handler_type":"agent","index":1,"max_attempts":1},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"stage.completed","id":"[EVENT_ID]","node_id":"run_tests","node_label":"Run Tests","properties":{"attempt":1,"context_updates":{"last_response":"[Simulated] Response for stage: run_tests","last_stage":"run_tests","response.run_tests":"[Simulated] Response for stage: run_tests"},"context_values":{"current.preamble":"Goal: Run tests and report results/n","current_node":"run_tests","failure_class":"","failure_signature":"","graph.goal":"Run tests and report results","graph.rankdir":"LR","internal.fidelity":"compact","internal.node_visit_count":1,"internal.retry_count.start":0,"internal.run_id":"[ULID]","internal.thread_id":"start","outcome":"success","thread.start.current_node":"run_tests"},"duration_ms": [DURATION_MS],"files_touched":[],"index":1,"max_attempts":1,"node_visits":{"run_tests":1,"start":1},"notes":"[Simulated] run_tests","preferred_label":null,"response":"[Simulated] Response for stage: run_tests","status":"success","suggested_next_ids":[],"usage":null},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"edge.selected","id":"[EVENT_ID]","properties":{"condition":null,"from_node":"run_tests","is_jump":false,"label":null,"reason":"unconditional","stage_status":"success","to_node":"report"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"stage.completed","id":"[EVENT_ID]","node_id":"run_tests","node_label":"Run Tests","properties":{"attempt":1,"context_updates":{"last_response":"[Simulated] Response for stage: run_tests","last_stage":"run_tests","response.run_tests":"[Simulated] Response for stage: run_tests"},"context_values":{"current.preamble":"Goal: Run tests and report results/n","current_node":"run_tests","failure_class":"","failure_signature":"","graph.goal":"Run tests and report results","graph.rankdir":"LR","internal.fidelity":"compact","internal.node_visit_count":1,"internal.retry_count.start":0,"internal.run_id":"[ULID]","internal.thread_id":"start","outcome":"success","thread.start.current_node":"run_tests"},"duration_ms": [DURATION_MS],"index":1,"max_attempts":1,"node_visits":{"run_tests":1,"start":1},"notes":"[Simulated] run_tests","response":"[Simulated] Response for stage: run_tests","status":"success"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"edge.selected","id":"[EVENT_ID]","properties":{"from_node":"run_tests","is_jump":false,"reason":"unconditional","stage_status":"success","to_node":"report"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"checkpoint.completed","id":"[EVENT_ID]","node_id":"run_tests","node_label":"run_tests","properties":{"completed_nodes":["start","run_tests"],"context_values":{"current.preamble":"Goal: Run tests and report results/n","current_node":"run_tests","failure_class":"","failure_signature":"","graph.goal":"Run tests and report results","graph.rankdir":"LR","internal.fidelity":"compact","internal.node_visit_count":1,"internal.retry_count.run_tests":0,"internal.retry_count.start":0,"internal.run_id":"[ULID]","internal.thread_id":"start","last_response":"[Simulated] Response for stage: run_tests","last_stage":"run_tests","outcome":"success","response.run_tests":"[Simulated] Response for stage: run_tests","thread.start.current_node":"run_tests"},"current_node":"run_tests","next_node_id":"report","node_outcomes":{"run_tests":{"context_updates":{"last_response":"[Simulated] Response for stage: run_tests","last_stage":"run_tests","response.run_tests":"[Simulated] Response for stage: run_tests"},"notes":"[Simulated] run_tests","status":"success","usage":null},"start":{"notes":"[Simulated] start","status":"success","usage":null}},"node_visits":{"run_tests":1,"start":1},"status":"success"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"stage.started","id":"[EVENT_ID]","node_id":"report","node_label":"Report","properties":{"attempt":1,"handler_type":"agent","index":2,"max_attempts":1},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"stage.completed","id":"[EVENT_ID]","node_id":"report","node_label":"Report","properties":{"attempt":1,"context_updates":{"last_response":"[Simulated] Response for stage: report","last_stage":"report","response.report":"[Simulated] Response for stage: report"},"context_values":{"current.preamble":"Goal: Run tests and report results/n/n## Completed stages/n- **run_tests**: success/n","current_node":"report","failure_class":"","failure_signature":"","graph.goal":"Run tests and report results","graph.rankdir":"LR","internal.fidelity":"compact","internal.node_visit_count":1,"internal.retry_count.run_tests":0,"internal.retry_count.start":0,"internal.run_id":"[ULID]","internal.thread_id":"run_tests","last_response":"[Simulated] Response for stage: run_tests","last_stage":"run_tests","outcome":"success","response.run_tests":"[Simulated] Response for stage: run_tests","thread.run_tests.current_node":"report","thread.start.current_node":"run_tests"},"duration_ms": [DURATION_MS],"files_touched":[],"index":2,"max_attempts":1,"node_visits":{"report":1,"run_tests":1,"start":1},"notes":"[Simulated] report","preferred_label":null,"response":"[Simulated] Response for stage: report","status":"success","suggested_next_ids":[],"usage":null},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"edge.selected","id":"[EVENT_ID]","properties":{"condition":null,"from_node":"report","is_jump":false,"label":null,"reason":"unconditional","stage_status":"success","to_node":"exit"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"stage.completed","id":"[EVENT_ID]","node_id":"report","node_label":"Report","properties":{"attempt":1,"context_updates":{"last_response":"[Simulated] Response for stage: report","last_stage":"report","response.report":"[Simulated] Response for stage: report"},"context_values":{"current.preamble":"Goal: Run tests and report results/n/n## Completed stages/n- **run_tests**: success/n","current_node":"report","failure_class":"","failure_signature":"","graph.goal":"Run tests and report results","graph.rankdir":"LR","internal.fidelity":"compact","internal.node_visit_count":1,"internal.retry_count.run_tests":0,"internal.retry_count.start":0,"internal.run_id":"[ULID]","internal.thread_id":"run_tests","last_response":"[Simulated] Response for stage: run_tests","last_stage":"run_tests","outcome":"success","response.run_tests":"[Simulated] Response for stage: run_tests","thread.run_tests.current_node":"report","thread.start.current_node":"run_tests"},"duration_ms": [DURATION_MS],"index":2,"max_attempts":1,"node_visits":{"report":1,"run_tests":1,"start":1},"notes":"[Simulated] report","response":"[Simulated] Response for stage: report","status":"success"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"edge.selected","id":"[EVENT_ID]","properties":{"from_node":"report","is_jump":false,"reason":"unconditional","stage_status":"success","to_node":"exit"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"checkpoint.completed","id":"[EVENT_ID]","node_id":"report","node_label":"report","properties":{"completed_nodes":["start","run_tests","report"],"context_values":{"current.preamble":"Goal: Run tests and report results/n/n## Completed stages/n- **run_tests**: success/n","current_node":"report","failure_class":"","failure_signature":"","graph.goal":"Run tests and report results","graph.rankdir":"LR","internal.fidelity":"compact","internal.node_visit_count":1,"internal.retry_count.report":0,"internal.retry_count.run_tests":0,"internal.retry_count.start":0,"internal.run_id":"[ULID]","internal.thread_id":"run_tests","last_response":"[Simulated] Response for stage: report","last_stage":"report","outcome":"success","response.report":"[Simulated] Response for stage: report","response.run_tests":"[Simulated] Response for stage: run_tests","thread.run_tests.current_node":"report","thread.start.current_node":"run_tests"},"current_node":"report","next_node_id":"exit","node_outcomes":{"report":{"context_updates":{"last_response":"[Simulated] Response for stage: report","last_stage":"report","response.report":"[Simulated] Response for stage: report"},"notes":"[Simulated] report","status":"success","usage":null},"run_tests":{"context_updates":{"last_response":"[Simulated] Response for stage: run_tests","last_stage":"run_tests","response.run_tests":"[Simulated] Response for stage: run_tests"},"notes":"[Simulated] run_tests","status":"success","usage":null},"start":{"notes":"[Simulated] start","status":"success","usage":null}},"node_visits":{"report":1,"run_tests":1,"start":1},"status":"success"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"stage.started","id":"[EVENT_ID]","node_id":"exit","node_label":"Exit","properties":{"attempt":1,"handler_type":"exit","index":3,"max_attempts":1},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"stage.completed","id":"[EVENT_ID]","node_id":"exit","node_label":"Exit","properties":{"attempt":1,"duration_ms": [DURATION_MS],"files_touched":[],"index":3,"max_attempts":1,"notes":null,"preferred_label":null,"status":"success","suggested_next_ids":[],"usage":null},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"stage.completed","id":"[EVENT_ID]","node_id":"exit","node_label":"Exit","properties":{"attempt":1,"duration_ms": [DURATION_MS],"index":3,"max_attempts":1,"status":"success"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"run.completed","id":"[EVENT_ID]","properties":{"artifact_count":0,"duration_ms": [DURATION_MS],"reason":"completed","status":"success"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"sandbox.cleanup.started","id":"[EVENT_ID]","properties":{"provider":"local"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"sandbox.cleanup.completed","id":"[EVENT_ID]","properties":{"duration_ms": [DURATION_MS],"provider":"local"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
|
|
@ -228,28 +228,28 @@ fn logs_follow_detached_run_streams_until_completion() {
|
|||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{"event":"run.created","id":"[EVENT_ID]","properties":{"graph":{"attrs":{"goal":{"String":"Run tests and report results"},"rankdir":{"String":"LR"}},"edges":[{"attrs":{},"from":"start","to":"run_tests"},{"attrs":{},"from":"run_tests","to":"report"},{"attrs":{},"from":"report","to":"exit"}],"name":"Simple","nodes":{"exit":{"attrs":{"label":{"String":"Exit"},"shape":{"String":"Msquare"}},"id":"exit"},"report":{"attrs":{"label":{"String":"Report"},"prompt":{"String":"Summarize the test results"}},"id":"report"},"run_tests":{"attrs":{"label":{"String":"Run Tests"},"prompt":{"String":"Run the test suite and report results"}},"id":"run_tests"},"start":{"attrs":{"label":{"String":"Start"},"shape":{"String":"Mdiamond"}},"id":"start"}}},"host_repo_path":"[TEMP_DIR]","labels":{},"run_dir":"[STORAGE_DIR]/runs/20260403-[ULID]","settings":{"auto_approve":true,"dry_run":true,"fabro":{"root":"fabro/"},"features":{"retros":false,"session_sandboxes":false},"goal":"Run tests and report results","hooks":[{"blocking":true,"command":"cargo fmt","event":"post_tool_use","matcher":"write_file|edit_file|apply_patch","name":"cargo-fmt","sandbox":null,"timeout_ms":null}],"llm":{"fallbacks":null,"model":"claude-sonnet-4-6","provider":"anthropic"},"mode":"standalone","no_retro":true,"pull_request":{"auto_merge":false,"draft":false,"enabled":true,"merge_strategy":"squash"},"sandbox":{"daytona":{"auto_stop_interval":30,"labels":{"repo":"fabro-sh/fabro"},"network":null,"skip_clone":false,"snapshot":{"cpu":4,"disk":20,"dockerfile":"FROM ubuntu:24.04/n/nRUN apt-get update && apt-get install -y --no-install-recommends curl git ca-certificates build-essential pkg-config libssl-dev unzip python3 && rm -rf /var/lib/apt/lists/*/n/n# GitHub CLI/nRUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && echo \"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main\" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null && apt-get update && apt-get install -y --no-install-recommends gh && rm -rf /var/lib/apt/lists/*/n/n# Rust/nRUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y/nENV PATH=\"/root/.cargo/bin:${PATH}\"/nRUN cargo install cargo-nextest --locked/nENV CARGO_INCREMENTAL=0/n/n# Bun/nRUN curl -fsSL https://bun.sh/install | bash/nENV PATH=\"/root/.bun/bin:${PATH}\"/n/nWORKDIR /root/n","memory":8,"name":"fabro-v6"}},"devcontainer":null,"env":null,"local":null,"preserve":null,"provider":"local"},"storage_dir":"[STORAGE_DIR]","version":1},"workflow_slug":"simple","workflow_source":"digraph Simple {/n graph [goal=\"Run tests and report results\"]/n rankdir=LR/n/n start [shape=Mdiamond, label=\"Start\"]/n exit [shape=Msquare, label=\"Exit\"]/n/n run_tests [label=\"Run Tests\", prompt=\"Run the test suite and report results\"]/n report [label=\"Report\", prompt=\"Summarize the test results\"]/n/n start -> run_tests -> report -> exit/n}/n","working_directory":"[TEMP_DIR]"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"run.created","id":"[EVENT_ID]","properties":{"graph":{"attrs":{"goal":{"String":"Run tests and report results"},"rankdir":{"String":"LR"}},"edges":[{"attrs":{},"from":"start","to":"run_tests"},{"attrs":{},"from":"run_tests","to":"report"},{"attrs":{},"from":"report","to":"exit"}],"name":"Simple","nodes":{"exit":{"attrs":{"label":{"String":"Exit"},"shape":{"String":"Msquare"}},"id":"exit"},"report":{"attrs":{"label":{"String":"Report"},"prompt":{"String":"Summarize the test results"}},"id":"report"},"run_tests":{"attrs":{"label":{"String":"Run Tests"},"prompt":{"String":"Run the test suite and report results"}},"id":"run_tests"},"start":{"attrs":{"label":{"String":"Start"},"shape":{"String":"Mdiamond"}},"id":"start"}}},"host_repo_path":"[TEMP_DIR]","labels":{},"run_dir":"[STORAGE_DIR]/runs/20260404-[ULID]","settings":{"auto_approve":true,"dry_run":true,"fabro":{"root":"fabro/"},"features":{"retros":false,"session_sandboxes":false},"goal":"Run tests and report results","hooks":[{"blocking":true,"command":"cargo fmt","event":"post_tool_use","matcher":"write_file|edit_file|apply_patch","name":"cargo-fmt","sandbox":null,"timeout_ms":null}],"llm":{"fallbacks":null,"model":"claude-sonnet-4-6","provider":"anthropic"},"mode":"standalone","no_retro":true,"pull_request":{"auto_merge":false,"draft":false,"enabled":true,"merge_strategy":"squash"},"sandbox":{"daytona":{"auto_stop_interval":30,"labels":{"repo":"fabro-sh/fabro"},"network":null,"skip_clone":false,"snapshot":{"cpu":4,"disk":20,"dockerfile":"FROM ubuntu:24.04/n/nRUN apt-get update && apt-get install -y --no-install-recommends curl git ca-certificates build-essential pkg-config libssl-dev unzip python3 && rm -rf /var/lib/apt/lists/*/n/n# GitHub CLI/nRUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && echo \"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main\" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null && apt-get update && apt-get install -y --no-install-recommends gh && rm -rf /var/lib/apt/lists/*/n/n# Rust/nRUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y/nENV PATH=\"/root/.cargo/bin:${PATH}\"/nRUN cargo install cargo-nextest --locked/nENV CARGO_INCREMENTAL=0/n/n# Bun/nRUN curl -fsSL https://bun.sh/install | bash/nENV PATH=\"/root/.bun/bin:${PATH}\"/n/nWORKDIR /root/n","memory":8,"name":"fabro-v6"}},"devcontainer":null,"env":null,"local":null,"preserve":null,"provider":"local"},"storage_dir":"[STORAGE_DIR]","version":1},"workflow_slug":"simple","workflow_source":"digraph Simple {/n graph [goal=\"Run tests and report results\"]/n rankdir=LR/n/n start [shape=Mdiamond, label=\"Start\"]/n exit [shape=Msquare, label=\"Exit\"]/n/n run_tests [label=\"Run Tests\", prompt=\"Run the test suite and report results\"]/n report [label=\"Report\", prompt=\"Summarize the test results\"]/n/n start -> run_tests -> report -> exit/n}/n","working_directory":"[TEMP_DIR]"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"run.submitted","id":"[EVENT_ID]","properties":{},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"run.starting","id":"[EVENT_ID]","properties":{"reason":"sandbox_initializing"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"sandbox.initializing","id":"[EVENT_ID]","properties":{"provider":"local"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"sandbox.ready","id":"[EVENT_ID]","properties":{"cpu":null,"duration_ms": [DURATION_MS],"memory":null,"name":null,"provider":"local","url":null},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"sandbox.ready","id":"[EVENT_ID]","properties":{"duration_ms": [DURATION_MS],"provider":"local"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"sandbox.initialized","id":"[EVENT_ID]","properties":{"provider":"local","working_directory":"[TEMP_DIR]"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"run.started","id":"[EVENT_ID]","properties":{"goal":"Run tests and report results","name":"Simple"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"run.running","id":"[EVENT_ID]","properties":{},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"stage.started","id":"[EVENT_ID]","node_id":"start","node_label":"Start","properties":{"attempt":1,"handler_type":"start","index":0,"max_attempts":1},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"stage.completed","id":"[EVENT_ID]","node_id":"start","node_label":"Start","properties":{"attempt":1,"context_values":{"current.preamble":"Goal: Run tests and report results/n","current_node":"start","graph.goal":"Run tests and report results","graph.rankdir":"LR","internal.fidelity":"compact","internal.node_visit_count":1,"internal.run_id":"[ULID]","internal.thread_id":null},"duration_ms": [DURATION_MS],"files_touched":[],"index":0,"max_attempts":1,"node_visits":{"start":1},"notes":"[Simulated] start","preferred_label":null,"status":"success","suggested_next_ids":[],"usage":null},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"edge.selected","id":"[EVENT_ID]","properties":{"condition":null,"from_node":"start","is_jump":false,"label":null,"reason":"unconditional","stage_status":"success","to_node":"run_tests"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"stage.completed","id":"[EVENT_ID]","node_id":"start","node_label":"Start","properties":{"attempt":1,"context_values":{"current.preamble":"Goal: Run tests and report results/n","current_node":"start","graph.goal":"Run tests and report results","graph.rankdir":"LR","internal.fidelity":"compact","internal.node_visit_count":1,"internal.run_id":"[ULID]","internal.thread_id":null},"duration_ms": [DURATION_MS],"index":0,"max_attempts":1,"node_visits":{"start":1},"notes":"[Simulated] start","status":"success"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"edge.selected","id":"[EVENT_ID]","properties":{"from_node":"start","is_jump":false,"reason":"unconditional","stage_status":"success","to_node":"run_tests"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"checkpoint.completed","id":"[EVENT_ID]","node_id":"start","node_label":"start","properties":{"completed_nodes":["start"],"context_values":{"current.preamble":"Goal: Run tests and report results/n","current_node":"start","failure_class":"","failure_signature":"","graph.goal":"Run tests and report results","graph.rankdir":"LR","internal.fidelity":"compact","internal.node_visit_count":1,"internal.retry_count.start":0,"internal.run_id":"[ULID]","internal.thread_id":null,"outcome":"success"},"current_node":"start","next_node_id":"run_tests","node_outcomes":{"start":{"notes":"[Simulated] start","status":"success","usage":null}},"node_visits":{"start":1},"status":"success"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"stage.started","id":"[EVENT_ID]","node_id":"run_tests","node_label":"Run Tests","properties":{"attempt":1,"handler_type":"agent","index":1,"max_attempts":1},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"stage.completed","id":"[EVENT_ID]","node_id":"run_tests","node_label":"Run Tests","properties":{"attempt":1,"context_updates":{"last_response":"[Simulated] Response for stage: run_tests","last_stage":"run_tests","response.run_tests":"[Simulated] Response for stage: run_tests"},"context_values":{"current.preamble":"Goal: Run tests and report results/n","current_node":"run_tests","failure_class":"","failure_signature":"","graph.goal":"Run tests and report results","graph.rankdir":"LR","internal.fidelity":"compact","internal.node_visit_count":1,"internal.retry_count.start":0,"internal.run_id":"[ULID]","internal.thread_id":"start","outcome":"success","thread.start.current_node":"run_tests"},"duration_ms": [DURATION_MS],"files_touched":[],"index":1,"max_attempts":1,"node_visits":{"run_tests":1,"start":1},"notes":"[Simulated] run_tests","preferred_label":null,"response":"[Simulated] Response for stage: run_tests","status":"success","suggested_next_ids":[],"usage":null},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"edge.selected","id":"[EVENT_ID]","properties":{"condition":null,"from_node":"run_tests","is_jump":false,"label":null,"reason":"unconditional","stage_status":"success","to_node":"report"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"stage.completed","id":"[EVENT_ID]","node_id":"run_tests","node_label":"Run Tests","properties":{"attempt":1,"context_updates":{"last_response":"[Simulated] Response for stage: run_tests","last_stage":"run_tests","response.run_tests":"[Simulated] Response for stage: run_tests"},"context_values":{"current.preamble":"Goal: Run tests and report results/n","current_node":"run_tests","failure_class":"","failure_signature":"","graph.goal":"Run tests and report results","graph.rankdir":"LR","internal.fidelity":"compact","internal.node_visit_count":1,"internal.retry_count.start":0,"internal.run_id":"[ULID]","internal.thread_id":"start","outcome":"success","thread.start.current_node":"run_tests"},"duration_ms": [DURATION_MS],"index":1,"max_attempts":1,"node_visits":{"run_tests":1,"start":1},"notes":"[Simulated] run_tests","response":"[Simulated] Response for stage: run_tests","status":"success"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"edge.selected","id":"[EVENT_ID]","properties":{"from_node":"run_tests","is_jump":false,"reason":"unconditional","stage_status":"success","to_node":"report"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"checkpoint.completed","id":"[EVENT_ID]","node_id":"run_tests","node_label":"run_tests","properties":{"completed_nodes":["start","run_tests"],"context_values":{"current.preamble":"Goal: Run tests and report results/n","current_node":"run_tests","failure_class":"","failure_signature":"","graph.goal":"Run tests and report results","graph.rankdir":"LR","internal.fidelity":"compact","internal.node_visit_count":1,"internal.retry_count.run_tests":0,"internal.retry_count.start":0,"internal.run_id":"[ULID]","internal.thread_id":"start","last_response":"[Simulated] Response for stage: run_tests","last_stage":"run_tests","outcome":"success","response.run_tests":"[Simulated] Response for stage: run_tests","thread.start.current_node":"run_tests"},"current_node":"run_tests","next_node_id":"report","node_outcomes":{"run_tests":{"context_updates":{"last_response":"[Simulated] Response for stage: run_tests","last_stage":"run_tests","response.run_tests":"[Simulated] Response for stage: run_tests"},"notes":"[Simulated] run_tests","status":"success","usage":null},"start":{"notes":"[Simulated] start","status":"success","usage":null}},"node_visits":{"run_tests":1,"start":1},"status":"success"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"stage.started","id":"[EVENT_ID]","node_id":"report","node_label":"Report","properties":{"attempt":1,"handler_type":"agent","index":2,"max_attempts":1},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"stage.completed","id":"[EVENT_ID]","node_id":"report","node_label":"Report","properties":{"attempt":1,"context_updates":{"last_response":"[Simulated] Response for stage: report","last_stage":"report","response.report":"[Simulated] Response for stage: report"},"context_values":{"current.preamble":"Goal: Run tests and report results/n/n## Completed stages/n- **run_tests**: success/n","current_node":"report","failure_class":"","failure_signature":"","graph.goal":"Run tests and report results","graph.rankdir":"LR","internal.fidelity":"compact","internal.node_visit_count":1,"internal.retry_count.run_tests":0,"internal.retry_count.start":0,"internal.run_id":"[ULID]","internal.thread_id":"run_tests","last_response":"[Simulated] Response for stage: run_tests","last_stage":"run_tests","outcome":"success","response.run_tests":"[Simulated] Response for stage: run_tests","thread.run_tests.current_node":"report","thread.start.current_node":"run_tests"},"duration_ms": [DURATION_MS],"files_touched":[],"index":2,"max_attempts":1,"node_visits":{"report":1,"run_tests":1,"start":1},"notes":"[Simulated] report","preferred_label":null,"response":"[Simulated] Response for stage: report","status":"success","suggested_next_ids":[],"usage":null},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"edge.selected","id":"[EVENT_ID]","properties":{"condition":null,"from_node":"report","is_jump":false,"label":null,"reason":"unconditional","stage_status":"success","to_node":"exit"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"stage.completed","id":"[EVENT_ID]","node_id":"report","node_label":"Report","properties":{"attempt":1,"context_updates":{"last_response":"[Simulated] Response for stage: report","last_stage":"report","response.report":"[Simulated] Response for stage: report"},"context_values":{"current.preamble":"Goal: Run tests and report results/n/n## Completed stages/n- **run_tests**: success/n","current_node":"report","failure_class":"","failure_signature":"","graph.goal":"Run tests and report results","graph.rankdir":"LR","internal.fidelity":"compact","internal.node_visit_count":1,"internal.retry_count.run_tests":0,"internal.retry_count.start":0,"internal.run_id":"[ULID]","internal.thread_id":"run_tests","last_response":"[Simulated] Response for stage: run_tests","last_stage":"run_tests","outcome":"success","response.run_tests":"[Simulated] Response for stage: run_tests","thread.run_tests.current_node":"report","thread.start.current_node":"run_tests"},"duration_ms": [DURATION_MS],"index":2,"max_attempts":1,"node_visits":{"report":1,"run_tests":1,"start":1},"notes":"[Simulated] report","response":"[Simulated] Response for stage: report","status":"success"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"edge.selected","id":"[EVENT_ID]","properties":{"from_node":"report","is_jump":false,"reason":"unconditional","stage_status":"success","to_node":"exit"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"checkpoint.completed","id":"[EVENT_ID]","node_id":"report","node_label":"report","properties":{"completed_nodes":["start","run_tests","report"],"context_values":{"current.preamble":"Goal: Run tests and report results/n/n## Completed stages/n- **run_tests**: success/n","current_node":"report","failure_class":"","failure_signature":"","graph.goal":"Run tests and report results","graph.rankdir":"LR","internal.fidelity":"compact","internal.node_visit_count":1,"internal.retry_count.report":0,"internal.retry_count.run_tests":0,"internal.retry_count.start":0,"internal.run_id":"[ULID]","internal.thread_id":"run_tests","last_response":"[Simulated] Response for stage: report","last_stage":"report","outcome":"success","response.report":"[Simulated] Response for stage: report","response.run_tests":"[Simulated] Response for stage: run_tests","thread.run_tests.current_node":"report","thread.start.current_node":"run_tests"},"current_node":"report","next_node_id":"exit","node_outcomes":{"report":{"context_updates":{"last_response":"[Simulated] Response for stage: report","last_stage":"report","response.report":"[Simulated] Response for stage: report"},"notes":"[Simulated] report","status":"success","usage":null},"run_tests":{"context_updates":{"last_response":"[Simulated] Response for stage: run_tests","last_stage":"run_tests","response.run_tests":"[Simulated] Response for stage: run_tests"},"notes":"[Simulated] run_tests","status":"success","usage":null},"start":{"notes":"[Simulated] start","status":"success","usage":null}},"node_visits":{"report":1,"run_tests":1,"start":1},"status":"success"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"stage.started","id":"[EVENT_ID]","node_id":"exit","node_label":"Exit","properties":{"attempt":1,"handler_type":"exit","index":3,"max_attempts":1},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"stage.completed","id":"[EVENT_ID]","node_id":"exit","node_label":"Exit","properties":{"attempt":1,"duration_ms": [DURATION_MS],"files_touched":[],"index":3,"max_attempts":1,"notes":null,"preferred_label":null,"status":"success","suggested_next_ids":[],"usage":null},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"stage.completed","id":"[EVENT_ID]","node_id":"exit","node_label":"Exit","properties":{"attempt":1,"duration_ms": [DURATION_MS],"index":3,"max_attempts":1,"status":"success"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"run.completed","id":"[EVENT_ID]","properties":{"artifact_count":0,"duration_ms": [DURATION_MS],"reason":"completed","status":"success"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"sandbox.cleanup.started","id":"[EVENT_ID]","properties":{"provider":"local"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
{"event":"sandbox.cleanup.completed","id":"[EVENT_ID]","properties":{"duration_ms": [DURATION_MS],"provider":"local"},"run_id":"[ULID]","ts":"[TIMESTAMP]"}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ fn ps_all_json_lists_created_and_completed_runs() {
|
|||
[
|
||||
{
|
||||
"run_id": "[ULID]",
|
||||
"dir_name": "20260403-[ULID]",
|
||||
"dir_name": "20260404-[ULID]",
|
||||
"workflow_name": "Simple",
|
||||
"workflow_slug": "simple",
|
||||
"status": "submitted",
|
||||
|
|
@ -87,7 +87,7 @@ fn ps_all_json_lists_created_and_completed_runs() {
|
|||
},
|
||||
{
|
||||
"run_id": "[ULID]",
|
||||
"dir_name": "20260403-[ULID]",
|
||||
"dir_name": "20260404-[ULID]",
|
||||
"workflow_name": "Simple",
|
||||
"workflow_slug": "simple",
|
||||
"status": "succeeded",
|
||||
|
|
@ -180,7 +180,7 @@ fn ps_filters_by_workflow_and_label() {
|
|||
[
|
||||
{
|
||||
"run_id": "[ULID]",
|
||||
"dir_name": "20260403-[ULID]",
|
||||
"dir_name": "20260404-[ULID]",
|
||||
"workflow_name": "Simple",
|
||||
"workflow_slug": "simple",
|
||||
"status": "succeeded",
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ fn dry_run_simple() {
|
|||
Run: [ULID]
|
||||
Status: SUCCESS
|
||||
Duration: [DURATION]
|
||||
Run: [STORAGE_DIR]/runs/20260403-[ULID]
|
||||
Run: [STORAGE_DIR]/runs/20260404-[ULID]
|
||||
|
||||
=== Output ===
|
||||
[Simulated] Response for stage: report
|
||||
|
|
@ -400,12 +400,8 @@ fn json_run_implies_auto_approve_for_human_gates() {
|
|||
"event": "sandbox.ready",
|
||||
"id": "[EVENT_ID]",
|
||||
"properties": {
|
||||
"cpu": null,
|
||||
"duration_ms": "[DURATION_MS]",
|
||||
"memory": null,
|
||||
"name": null,
|
||||
"provider": "local",
|
||||
"url": null
|
||||
"provider": "local"
|
||||
},
|
||||
"run_id": "[ULID]",
|
||||
"ts": "[TIMESTAMP]"
|
||||
|
|
@ -468,17 +464,12 @@ fn json_run_implies_auto_approve_for_human_gates() {
|
|||
"internal.thread_id": null
|
||||
},
|
||||
"duration_ms": "[DURATION_MS]",
|
||||
"files_touched": [],
|
||||
"index": 0,
|
||||
"max_attempts": 1,
|
||||
"node_visits": {
|
||||
"start": 1
|
||||
},
|
||||
"notes": null,
|
||||
"preferred_label": null,
|
||||
"status": "success",
|
||||
"suggested_next_ids": [],
|
||||
"usage": null
|
||||
"status": "success"
|
||||
},
|
||||
"run_id": "[ULID]",
|
||||
"ts": "[TIMESTAMP]"
|
||||
|
|
@ -487,10 +478,8 @@ fn json_run_implies_auto_approve_for_human_gates() {
|
|||
"event": "edge.selected",
|
||||
"id": "[EVENT_ID]",
|
||||
"properties": {
|
||||
"condition": null,
|
||||
"from_node": "start",
|
||||
"is_jump": false,
|
||||
"label": null,
|
||||
"reason": "unconditional",
|
||||
"stage_status": "success",
|
||||
"to_node": "approve"
|
||||
|
|
@ -576,20 +565,17 @@ fn json_run_implies_auto_approve_for_human_gates() {
|
|||
"thread.start.current_node": "approve"
|
||||
},
|
||||
"duration_ms": "[DURATION_MS]",
|
||||
"files_touched": [],
|
||||
"index": 1,
|
||||
"max_attempts": 1,
|
||||
"node_visits": {
|
||||
"approve": 1,
|
||||
"start": 1
|
||||
},
|
||||
"notes": null,
|
||||
"preferred_label": "[A] Approve",
|
||||
"status": "success",
|
||||
"suggested_next_ids": [
|
||||
"ship"
|
||||
],
|
||||
"usage": null
|
||||
]
|
||||
},
|
||||
"run_id": "[ULID]",
|
||||
"ts": "[TIMESTAMP]"
|
||||
|
|
@ -598,7 +584,6 @@ fn json_run_implies_auto_approve_for_human_gates() {
|
|||
"event": "edge.selected",
|
||||
"id": "[EVENT_ID]",
|
||||
"properties": {
|
||||
"condition": null,
|
||||
"from_node": "approve",
|
||||
"is_jump": false,
|
||||
"label": "[A] Approve",
|
||||
|
|
@ -743,7 +728,6 @@ fn json_run_implies_auto_approve_for_human_gates() {
|
|||
"thread.start.current_node": "approve"
|
||||
},
|
||||
"duration_ms": "[DURATION_MS]",
|
||||
"files_touched": [],
|
||||
"index": 2,
|
||||
"max_attempts": 1,
|
||||
"node_visits": {
|
||||
|
|
@ -752,10 +736,7 @@ fn json_run_implies_auto_approve_for_human_gates() {
|
|||
"start": 1
|
||||
},
|
||||
"notes": "Script completed: echo shipped",
|
||||
"preferred_label": null,
|
||||
"status": "success",
|
||||
"suggested_next_ids": [],
|
||||
"usage": null
|
||||
"status": "success"
|
||||
},
|
||||
"run_id": "[ULID]",
|
||||
"ts": "[TIMESTAMP]"
|
||||
|
|
@ -764,10 +745,8 @@ fn json_run_implies_auto_approve_for_human_gates() {
|
|||
"event": "edge.selected",
|
||||
"id": "[EVENT_ID]",
|
||||
"properties": {
|
||||
"condition": null,
|
||||
"from_node": "ship",
|
||||
"is_jump": false,
|
||||
"label": null,
|
||||
"reason": "unconditional",
|
||||
"stage_status": "success",
|
||||
"to_node": "exit"
|
||||
|
|
@ -869,14 +848,9 @@ fn json_run_implies_auto_approve_for_human_gates() {
|
|||
"properties": {
|
||||
"attempt": 1,
|
||||
"duration_ms": "[DURATION_MS]",
|
||||
"files_touched": [],
|
||||
"index": 3,
|
||||
"max_attempts": 1,
|
||||
"notes": null,
|
||||
"preferred_label": null,
|
||||
"status": "success",
|
||||
"suggested_next_ids": [],
|
||||
"usage": null
|
||||
"status": "success"
|
||||
},
|
||||
"run_id": "[ULID]",
|
||||
"ts": "[TIMESTAMP]"
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ fn system_prune_dry_run_lists_matching_runs_without_deleting() {
|
|||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
would delete: 20260403-[ULID] (Simple)
|
||||
would delete: 20260404-[ULID] (Simple)
|
||||
----- stderr -----
|
||||
|
||||
1 run(s) would be deleted ([SIZE] freed). Pass --yes to confirm.
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ fn dry_run_branching() {
|
|||
Run: [ULID]
|
||||
Status: SUCCESS
|
||||
Duration: [DURATION]
|
||||
Run: [STORAGE_DIR]/runs/20260403-[ULID]
|
||||
Run: [STORAGE_DIR]/runs/20260404-[ULID]
|
||||
|
||||
=== Output ===
|
||||
[Simulated] Response for stage: validate
|
||||
|
|
@ -62,7 +62,7 @@ fn dry_run_conditions() {
|
|||
Run: [ULID]
|
||||
Status: SUCCESS
|
||||
Duration: [DURATION]
|
||||
Run: [STORAGE_DIR]/runs/20260403-[ULID]
|
||||
Run: [STORAGE_DIR]/runs/20260404-[ULID]
|
||||
|
||||
=== Output ===
|
||||
[Simulated] Response for stage: path_b
|
||||
|
|
@ -95,7 +95,7 @@ fn dry_run_parallel() {
|
|||
Run: [ULID]
|
||||
Status: SUCCESS
|
||||
Duration: [DURATION]
|
||||
Run: [STORAGE_DIR]/runs/20260403-[ULID]
|
||||
Run: [STORAGE_DIR]/runs/20260404-[ULID]
|
||||
|
||||
=== Output ===
|
||||
[Simulated] Response for stage: review
|
||||
|
|
@ -128,7 +128,7 @@ fn dry_run_styled() {
|
|||
Run: [ULID]
|
||||
Status: SUCCESS
|
||||
Duration: [DURATION]
|
||||
Run: [STORAGE_DIR]/runs/20260403-[ULID]
|
||||
Run: [STORAGE_DIR]/runs/20260404-[ULID]
|
||||
|
||||
=== Output ===
|
||||
[Simulated] Response for stage: critical_review
|
||||
|
|
@ -159,6 +159,6 @@ fn dry_run_legacy_tool() {
|
|||
Run: [ULID]
|
||||
Status: SUCCESS
|
||||
Duration: [DURATION]
|
||||
Run: [STORAGE_DIR]/runs/20260403-[ULID]
|
||||
Run: [STORAGE_DIR]/runs/20260404-[ULID]
|
||||
");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ use fabro_llm::types::{
|
|||
Response as LlmResponse, Role, StreamEvent, ToolChoice, ToolDefinition, Usage,
|
||||
};
|
||||
use fabro_store::StoreHandle;
|
||||
use fabro_types::{RunId, Settings};
|
||||
use fabro_types::{RunId, Settings, StoredEvent};
|
||||
use fabro_util::redact::redact_jsonl_line;
|
||||
use fabro_workflow::error::FabroError;
|
||||
use fabro_workflow::handler::HandlerRegistry;
|
||||
|
|
@ -47,7 +47,7 @@ use crate::static_files;
|
|||
use crate::web_auth;
|
||||
use fabro_interview::{Answer, Interviewer, QuestionType, WebInterviewer};
|
||||
use fabro_workflow::context::Context;
|
||||
use fabro_workflow::event::{EventEmitter, RunEventEnvelope};
|
||||
use fabro_workflow::event::EventEmitter;
|
||||
use fabro_workflow::operations::{self, CreateRunInput, WorkflowInput};
|
||||
use fabro_workflow::pipeline::Persisted;
|
||||
use fabro_workflow::records::Checkpoint;
|
||||
|
|
@ -97,7 +97,7 @@ struct ManagedRun {
|
|||
created_at: chrono::DateTime<chrono::Utc>,
|
||||
// Populated when running:
|
||||
interviewer: Option<Arc<WebInterviewer>>,
|
||||
event_tx: Option<broadcast::Sender<RunEventEnvelope>>,
|
||||
event_tx: Option<broadcast::Sender<StoredEvent>>,
|
||||
context: Option<Context>,
|
||||
checkpoint: Option<Checkpoint>,
|
||||
cancel_tx: Option<oneshot::Sender<()>>,
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@ use std::path::PathBuf;
|
|||
use std::str::FromStr;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{EventEnvelope, Result, RunSummary, StageId, StoreError};
|
||||
use fabro_types::stored_event::{RunCompletedProps, RunFailedProps, StageCompletedProps};
|
||||
use fabro_types::{
|
||||
Checkpoint, Conclusion, FailureSignature, NodeStatusRecord, Outcome, PullRequestRecord, Retro,
|
||||
RunId, RunRecord, RunStatus, RunStatusRecord, SandboxRecord, StageStatus, StageUsage,
|
||||
StartRecord, StatusReason,
|
||||
Checkpoint, Conclusion, EventBody, FailureSignature, NodeStatusRecord, Outcome,
|
||||
PullRequestRecord, Retro, RunId, RunRecord, RunStatus, RunStatusRecord, SandboxRecord,
|
||||
StageStatus, StageUsage, StartRecord, StatusReason, StoredEvent, TokenUsage,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
|
|
@ -75,190 +75,220 @@ impl RunProjection {
|
|||
}
|
||||
|
||||
pub(crate) fn apply_event(&mut self, event: &EventEnvelope) -> Result<()> {
|
||||
let value = event.payload.as_value();
|
||||
let ts = parse_ts(value)?;
|
||||
let event_name = value
|
||||
.get("event")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| StoreError::InvalidEvent("event payload missing event name".into()))?;
|
||||
let run_id = parse_run_id(value)?;
|
||||
let properties = value
|
||||
.get("properties")
|
||||
.and_then(Value::as_object)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let stored = StoredEvent::from_value(event.payload.as_value().clone())
|
||||
.map_err(|err| StoreError::InvalidEvent(format!("invalid stored event: {err}")))?;
|
||||
let ts = stored.ts;
|
||||
let run_id = stored.run_id;
|
||||
|
||||
match event_name {
|
||||
"run.created" => {
|
||||
let settings = required_json::<fabro_types::Settings>(&properties, "settings")?;
|
||||
let graph = required_json::<fabro_types::Graph>(&properties, "graph")?;
|
||||
let working_directory =
|
||||
required_string(&properties, "working_directory").map(PathBuf::from)?;
|
||||
let labels = optional_json::<BTreeMap<String, String>>(&properties, "labels")?
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.collect::<HashMap<_, _>>();
|
||||
match &stored.body {
|
||||
EventBody::RunCreated(props) => {
|
||||
let working_directory = PathBuf::from(&props.working_directory);
|
||||
let labels = props.labels.clone().into_iter().collect::<HashMap<_, _>>();
|
||||
self.run = Some(RunRecord {
|
||||
run_id,
|
||||
settings,
|
||||
graph,
|
||||
workflow_slug: optional_string(&properties, "workflow_slug"),
|
||||
settings: props.settings.clone(),
|
||||
graph: props.graph.clone(),
|
||||
workflow_slug: props.workflow_slug.clone(),
|
||||
working_directory,
|
||||
host_repo_path: optional_string(&properties, "host_repo_path"),
|
||||
base_branch: optional_string(&properties, "base_branch"),
|
||||
host_repo_path: props.host_repo_path.clone(),
|
||||
base_branch: props.base_branch.clone(),
|
||||
labels,
|
||||
});
|
||||
self.graph_source = optional_string(&properties, "workflow_source");
|
||||
self.graph_source = props.workflow_source.clone();
|
||||
}
|
||||
"run.started" => {
|
||||
EventBody::RunStarted(props) => {
|
||||
self.start = Some(StartRecord {
|
||||
run_id,
|
||||
start_time: ts,
|
||||
run_branch: optional_string(&properties, "run_branch"),
|
||||
base_sha: optional_string(&properties, "base_sha"),
|
||||
run_branch: props.run_branch.clone(),
|
||||
base_sha: props.base_sha.clone(),
|
||||
});
|
||||
}
|
||||
"run.submitted" => {
|
||||
self.status = Some(run_status_record(RunStatus::Submitted, &properties, ts)?);
|
||||
EventBody::RunSubmitted(props) => {
|
||||
self.status = Some(run_status_record(
|
||||
RunStatus::Submitted,
|
||||
props.reason.clone(),
|
||||
ts,
|
||||
));
|
||||
}
|
||||
"run.starting" => {
|
||||
self.status = Some(run_status_record(RunStatus::Starting, &properties, ts)?);
|
||||
EventBody::RunStarting(props) => {
|
||||
self.status = Some(run_status_record(
|
||||
RunStatus::Starting,
|
||||
props.reason.clone(),
|
||||
ts,
|
||||
));
|
||||
}
|
||||
"run.running" => {
|
||||
self.status = Some(run_status_record(RunStatus::Running, &properties, ts)?);
|
||||
EventBody::RunRunning(props) => {
|
||||
self.status = Some(run_status_record(
|
||||
RunStatus::Running,
|
||||
props.reason.clone(),
|
||||
ts,
|
||||
));
|
||||
}
|
||||
"run.removing" => {
|
||||
self.status = Some(run_status_record(RunStatus::Removing, &properties, ts)?);
|
||||
EventBody::RunRemoving(props) => {
|
||||
self.status = Some(run_status_record(
|
||||
RunStatus::Removing,
|
||||
props.reason.clone(),
|
||||
ts,
|
||||
));
|
||||
}
|
||||
"run.completed" => {
|
||||
self.status = Some(run_status_record(RunStatus::Succeeded, &properties, ts)?);
|
||||
self.conclusion = Some(conclusion_from_completed(&properties, ts)?);
|
||||
self.final_patch = optional_string(&properties, "final_patch");
|
||||
EventBody::RunCompleted(props) => {
|
||||
self.status = Some(run_status_record(
|
||||
RunStatus::Succeeded,
|
||||
props.reason.clone(),
|
||||
ts,
|
||||
));
|
||||
self.conclusion = Some(conclusion_from_completed(props, ts)?);
|
||||
self.final_patch = props.final_patch.clone();
|
||||
}
|
||||
"run.failed" => {
|
||||
self.status = Some(run_status_record(RunStatus::Failed, &properties, ts)?);
|
||||
self.conclusion = Some(conclusion_from_failed(&properties, ts));
|
||||
EventBody::RunFailed(props) => {
|
||||
self.status = Some(run_status_record(
|
||||
RunStatus::Failed,
|
||||
props.reason.clone(),
|
||||
ts,
|
||||
));
|
||||
self.conclusion = Some(conclusion_from_failed(props, ts));
|
||||
}
|
||||
"run.rewound" => {
|
||||
EventBody::RunRewound(_) => {
|
||||
self.reset_for_rewind();
|
||||
}
|
||||
"checkpoint.completed" => {
|
||||
let checkpoint = checkpoint_from_properties(&properties, ts)?;
|
||||
if let Some(node_id) = value.get("node_id").and_then(Value::as_str) {
|
||||
EventBody::CheckpointCompleted(props) => {
|
||||
let checkpoint = checkpoint_from_props(props, ts);
|
||||
if let Some(node_id) = stored.node_id.as_deref() {
|
||||
let visit = checkpoint
|
||||
.node_visits
|
||||
.get(node_id)
|
||||
.and_then(|visit| u32::try_from(*visit).ok())
|
||||
.unwrap_or(1);
|
||||
if let Some(diff) = optional_string(&properties, "diff") {
|
||||
if let Some(diff) = props.diff.clone() {
|
||||
self.node_mut(node_id, visit).diff = Some(diff);
|
||||
}
|
||||
}
|
||||
self.checkpoint = Some(checkpoint.clone());
|
||||
self.checkpoints.push((event.seq, checkpoint));
|
||||
}
|
||||
"sandbox.initialized" => {
|
||||
EventBody::SandboxInitialized(props) => {
|
||||
self.sandbox = Some(SandboxRecord {
|
||||
provider: required_string(&properties, "provider")?,
|
||||
working_directory: required_string(&properties, "working_directory")?,
|
||||
identifier: optional_string(&properties, "identifier"),
|
||||
host_working_directory: optional_string(&properties, "host_working_directory"),
|
||||
container_mount_point: optional_string(&properties, "container_mount_point"),
|
||||
provider: props.provider.clone(),
|
||||
working_directory: props.working_directory.clone(),
|
||||
identifier: props.identifier.clone(),
|
||||
host_working_directory: props.host_working_directory.clone(),
|
||||
container_mount_point: props.container_mount_point.clone(),
|
||||
});
|
||||
}
|
||||
"retro.started" => {
|
||||
self.retro_prompt = optional_string(&properties, "prompt");
|
||||
EventBody::RetroStarted(props) => {
|
||||
self.retro_prompt = props.prompt.clone();
|
||||
}
|
||||
"retro.completed" => {
|
||||
self.retro_response = optional_string(&properties, "response");
|
||||
self.retro = optional_json::<Retro>(&properties, "retro")?;
|
||||
EventBody::RetroCompleted(props) => {
|
||||
self.retro_response = props.response.clone();
|
||||
self.retro = props
|
||||
.retro
|
||||
.clone()
|
||||
.map(serde_json::from_value)
|
||||
.transpose()
|
||||
.map_err(|err| {
|
||||
StoreError::InvalidEvent(format!("invalid retro payload: {err}"))
|
||||
})?;
|
||||
}
|
||||
"pull_request.created" => {
|
||||
EventBody::PullRequestCreated(props) => {
|
||||
self.pull_request = Some(PullRequestRecord {
|
||||
html_url: required_string(&properties, "pr_url")?,
|
||||
number: required_u64(&properties, "pr_number")?,
|
||||
owner: required_string(&properties, "owner")?,
|
||||
repo: required_string(&properties, "repo")?,
|
||||
base_branch: required_string(&properties, "base_branch")?,
|
||||
head_branch: required_string(&properties, "head_branch")?,
|
||||
title: required_string(&properties, "title")?,
|
||||
html_url: props.pr_url.clone(),
|
||||
number: props.pr_number,
|
||||
owner: props.owner.clone(),
|
||||
repo: props.repo.clone(),
|
||||
base_branch: props.base_branch.clone(),
|
||||
head_branch: props.head_branch.clone(),
|
||||
title: props.title.clone(),
|
||||
});
|
||||
}
|
||||
"stage.prompt" => {
|
||||
let Some(node_id) = value.get("node_id").and_then(Value::as_str) else {
|
||||
EventBody::StagePrompt(props) => {
|
||||
let Some(node_id) = stored.node_id.as_deref() else {
|
||||
return Ok(());
|
||||
};
|
||||
let visit = required_u32(&properties, "visit")?;
|
||||
self.node_mut(node_id, visit).prompt = optional_string(&properties, "text");
|
||||
self.node_mut(node_id, visit).provider_used =
|
||||
provider_used_from_prompt(&properties);
|
||||
let visit = props.visit;
|
||||
self.node_mut(node_id, visit).prompt = Some(props.text.clone());
|
||||
self.node_mut(node_id, visit).provider_used = provider_used_from_prompt(props);
|
||||
}
|
||||
"prompt.completed" => {
|
||||
let Some(node_id) = value.get("node_id").and_then(Value::as_str) else {
|
||||
EventBody::PromptCompleted(props) => {
|
||||
let Some(node_id) = stored.node_id.as_deref() else {
|
||||
return Ok(());
|
||||
};
|
||||
let visit = self.current_visit_for(node_id).unwrap_or(1);
|
||||
self.node_mut(node_id, visit).response = optional_string(&properties, "response");
|
||||
self.node_mut(node_id, visit).response = Some(props.response.clone());
|
||||
}
|
||||
"stage.completed" => {
|
||||
let Some(node_id) = value.get("node_id").and_then(Value::as_str) else {
|
||||
EventBody::StageCompleted(props) => {
|
||||
let Some(node_id) = stored.node_id.as_deref() else {
|
||||
return Ok(());
|
||||
};
|
||||
let visit = stage_visit(node_id, &properties, self).unwrap_or(1);
|
||||
let response = optional_string(&properties, "response");
|
||||
let outcome = stage_outcome_from_properties(&properties)?;
|
||||
let visit = stage_visit(node_id, props.node_visits.as_ref(), self).unwrap_or(1);
|
||||
let response = props.response.clone();
|
||||
let outcome = stage_outcome_from_props(props);
|
||||
let status = node_status_from_outcome(&outcome, ts);
|
||||
let node = self.node_mut(node_id, visit);
|
||||
node.response = response;
|
||||
node.status = Some(status);
|
||||
}
|
||||
"stage.failed" => {
|
||||
let Some(node_id) = value.get("node_id").and_then(Value::as_str) else {
|
||||
EventBody::StageFailed(props) => {
|
||||
let Some(node_id) = stored.node_id.as_deref() else {
|
||||
return Ok(());
|
||||
};
|
||||
let visit = self.current_visit_for(node_id).unwrap_or(1);
|
||||
let failure = optional_json::<fabro_types::FailureDetail>(&properties, "failure")?;
|
||||
let failure_reason = failure.as_ref().map(|detail| detail.message.clone());
|
||||
let failure_reason = props.failure.as_ref().map(|detail| detail.message.clone());
|
||||
let node = self.node_mut(node_id, visit);
|
||||
node.status = Some(NodeStatusRecord {
|
||||
status: StageStatus::Fail,
|
||||
notes: None,
|
||||
failure_reason: failure_reason.clone(),
|
||||
failure_reason,
|
||||
timestamp: ts,
|
||||
});
|
||||
}
|
||||
"agent.session.started" | "agent.cli.started" => {
|
||||
let Some(node_id) = value.get("node_id").and_then(Value::as_str) else {
|
||||
EventBody::AgentSessionStarted(props) => {
|
||||
let Some(node_id) = stored.node_id.as_deref() else {
|
||||
return Ok(());
|
||||
};
|
||||
let visit = required_u32(&properties, "visit")?;
|
||||
self.node_mut(node_id, visit).provider_used =
|
||||
Some(provider_used_from_agent_event(event_name, &properties));
|
||||
self.node_mut(node_id, props.visit).provider_used =
|
||||
Some(provider_used_from_agent_session_started(props));
|
||||
}
|
||||
"command.started" => {
|
||||
let Some(node_id) = value.get("node_id").and_then(Value::as_str) else {
|
||||
EventBody::AgentCliStarted(props) => {
|
||||
let Some(node_id) = stored.node_id.as_deref() else {
|
||||
return Ok(());
|
||||
};
|
||||
self.node_mut(node_id, props.visit).provider_used =
|
||||
Some(provider_used_from_agent_cli_started(props));
|
||||
}
|
||||
EventBody::CommandStarted(props) => {
|
||||
let Some(node_id) = stored.node_id.as_deref() else {
|
||||
return Ok(());
|
||||
};
|
||||
let visit = self.current_visit_for(node_id).unwrap_or(1);
|
||||
self.node_mut(node_id, visit).script_invocation =
|
||||
Some(Value::Object(properties.clone()));
|
||||
Some(serde_json::to_value(props).map_err(|err| {
|
||||
StoreError::InvalidEvent(format!("invalid command.started payload: {err}"))
|
||||
})?);
|
||||
}
|
||||
"command.completed" => {
|
||||
let Some(node_id) = value.get("node_id").and_then(Value::as_str) else {
|
||||
EventBody::CommandCompleted(props) => {
|
||||
let Some(node_id) = stored.node_id.as_deref() else {
|
||||
return Ok(());
|
||||
};
|
||||
let visit = self.current_visit_for(node_id).unwrap_or(1);
|
||||
let node = self.node_mut(node_id, visit);
|
||||
node.stdout = optional_string(&properties, "stdout");
|
||||
node.stderr = optional_string(&properties, "stderr");
|
||||
node.script_timing = Some(Value::Object(properties.clone()));
|
||||
node.stdout = Some(props.stdout.clone());
|
||||
node.stderr = Some(props.stderr.clone());
|
||||
node.script_timing = Some(serde_json::to_value(props).map_err(|err| {
|
||||
StoreError::InvalidEvent(format!("invalid command.completed payload: {err}"))
|
||||
})?);
|
||||
}
|
||||
"parallel.completed" => {
|
||||
let Some(node_id) = value.get("node_id").and_then(Value::as_str) else {
|
||||
EventBody::ParallelCompleted(props) => {
|
||||
let Some(node_id) = stored.node_id.as_deref() else {
|
||||
return Ok(());
|
||||
};
|
||||
let visit = self.current_visit_for(node_id).unwrap_or(1);
|
||||
self.node_mut(node_id, visit).parallel_results = properties.get("results").cloned();
|
||||
self.node_mut(node_id, visit).parallel_results =
|
||||
Some(serde_json::to_value(&props.results).map_err(|err| {
|
||||
StoreError::InvalidEvent(format!(
|
||||
"invalid parallel.completed payload: {err}"
|
||||
))
|
||||
})?);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
@ -358,148 +388,65 @@ impl RunProjection {
|
|||
}
|
||||
}
|
||||
|
||||
fn parse_ts(value: &Value) -> Result<DateTime<Utc>> {
|
||||
let ts = value
|
||||
.get("ts")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| StoreError::InvalidEvent("event payload missing ts".into()))?;
|
||||
chrono::DateTime::parse_from_rfc3339(ts)
|
||||
.map(|ts| ts.with_timezone(&Utc))
|
||||
.map_err(|err| StoreError::InvalidEvent(format!("invalid event ts: {err}")))
|
||||
}
|
||||
|
||||
fn parse_run_id(value: &Value) -> Result<RunId> {
|
||||
let run_id = value
|
||||
.get("run_id")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| StoreError::InvalidEvent("event payload missing run_id".into()))?;
|
||||
run_id
|
||||
.parse()
|
||||
.map_err(|err| StoreError::InvalidEvent(format!("invalid run_id: {err}")))
|
||||
}
|
||||
|
||||
fn required_string(properties: &serde_json::Map<String, Value>, key: &str) -> Result<String> {
|
||||
properties
|
||||
.get(key)
|
||||
.and_then(Value::as_str)
|
||||
.map(ToString::to_string)
|
||||
.ok_or_else(|| StoreError::InvalidEvent(format!("event missing string property {key}")))
|
||||
}
|
||||
|
||||
fn optional_string(properties: &serde_json::Map<String, Value>, key: &str) -> Option<String> {
|
||||
properties
|
||||
.get(key)
|
||||
.and_then(Value::as_str)
|
||||
.map(ToString::to_string)
|
||||
}
|
||||
|
||||
fn required_u64(properties: &serde_json::Map<String, Value>, key: &str) -> Result<u64> {
|
||||
properties
|
||||
.get(key)
|
||||
.and_then(Value::as_u64)
|
||||
.ok_or_else(|| StoreError::InvalidEvent(format!("event missing integer property {key}")))
|
||||
}
|
||||
|
||||
fn required_u32(properties: &serde_json::Map<String, Value>, key: &str) -> Result<u32> {
|
||||
u32::try_from(required_u64(properties, key)?)
|
||||
.map_err(|_| StoreError::InvalidEvent(format!("property {key} does not fit in u32")))
|
||||
}
|
||||
|
||||
fn required_json<T: DeserializeOwned>(
|
||||
properties: &serde_json::Map<String, Value>,
|
||||
key: &str,
|
||||
) -> Result<T> {
|
||||
let value = properties
|
||||
.get(key)
|
||||
.cloned()
|
||||
.ok_or_else(|| StoreError::InvalidEvent(format!("event missing property {key}")))?;
|
||||
serde_json::from_value(value)
|
||||
.map_err(|err| StoreError::InvalidEvent(format!("invalid property {key}: {err}")))
|
||||
}
|
||||
|
||||
fn optional_json<T: DeserializeOwned>(
|
||||
properties: &serde_json::Map<String, Value>,
|
||||
key: &str,
|
||||
) -> Result<Option<T>> {
|
||||
properties
|
||||
.get(key)
|
||||
.filter(|value| !value.is_null())
|
||||
.cloned()
|
||||
.map(|value| {
|
||||
serde_json::from_value(value)
|
||||
.map_err(|err| StoreError::InvalidEvent(format!("invalid property {key}: {err}")))
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
fn parse_reason(properties: &serde_json::Map<String, Value>) -> Result<Option<StatusReason>> {
|
||||
optional_string(properties, "reason")
|
||||
.map(|reason| {
|
||||
serde_json::from_value(Value::String(reason))
|
||||
.map_err(|err| StoreError::InvalidEvent(format!("invalid status reason: {err}")))
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
fn run_status_record(
|
||||
status: RunStatus,
|
||||
properties: &serde_json::Map<String, Value>,
|
||||
reason: Option<StatusReason>,
|
||||
updated_at: DateTime<Utc>,
|
||||
) -> Result<RunStatusRecord> {
|
||||
Ok(RunStatusRecord {
|
||||
) -> RunStatusRecord {
|
||||
RunStatusRecord {
|
||||
status,
|
||||
reason: parse_reason(properties)?,
|
||||
reason,
|
||||
updated_at,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn checkpoint_from_properties(
|
||||
properties: &serde_json::Map<String, Value>,
|
||||
fn checkpoint_from_props(
|
||||
props: &fabro_types::stored_event::CheckpointCompletedProps,
|
||||
timestamp: DateTime<Utc>,
|
||||
) -> Result<Checkpoint> {
|
||||
let loop_failure_signatures =
|
||||
optional_json::<HashMap<String, usize>>(properties, "loop_failure_signatures")?
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|(key, value)| (FailureSignature(key), value))
|
||||
.collect();
|
||||
let restart_failure_signatures =
|
||||
optional_json::<HashMap<String, usize>>(properties, "restart_failure_signatures")?
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|(key, value)| (FailureSignature(key), value))
|
||||
.collect();
|
||||
) -> Checkpoint {
|
||||
let loop_failure_signatures = props
|
||||
.loop_failure_signatures
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(key, value)| (FailureSignature(key), value))
|
||||
.collect();
|
||||
let restart_failure_signatures = props
|
||||
.restart_failure_signatures
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(key, value)| (FailureSignature(key), value))
|
||||
.collect();
|
||||
|
||||
Ok(Checkpoint {
|
||||
Checkpoint {
|
||||
timestamp,
|
||||
current_node: required_string(properties, "current_node")?,
|
||||
completed_nodes: optional_json(properties, "completed_nodes")?.unwrap_or_default(),
|
||||
node_retries: optional_json(properties, "node_retries")?.unwrap_or_default(),
|
||||
context_values: optional_json(properties, "context_values")?.unwrap_or_default(),
|
||||
node_outcomes: optional_json(properties, "node_outcomes")?.unwrap_or_default(),
|
||||
next_node_id: optional_string(properties, "next_node_id"),
|
||||
git_commit_sha: optional_string(properties, "git_commit_sha"),
|
||||
current_node: props.current_node.clone(),
|
||||
completed_nodes: props.completed_nodes.clone(),
|
||||
node_retries: props.node_retries.clone().into_iter().collect(),
|
||||
context_values: props.context_values.clone().into_iter().collect(),
|
||||
node_outcomes: props.node_outcomes.clone().into_iter().collect(),
|
||||
next_node_id: props.next_node_id.clone(),
|
||||
git_commit_sha: props.git_commit_sha.clone(),
|
||||
loop_failure_signatures,
|
||||
restart_failure_signatures,
|
||||
node_visits: optional_json(properties, "node_visits")?.unwrap_or_default(),
|
||||
})
|
||||
node_visits: props.node_visits.clone().into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn conclusion_from_completed(
|
||||
properties: &serde_json::Map<String, Value>,
|
||||
props: &RunCompletedProps,
|
||||
timestamp: DateTime<Utc>,
|
||||
) -> Result<Conclusion> {
|
||||
let usage = optional_json::<RunUsage>(properties, "usage")?;
|
||||
let usage = props.usage.as_ref().map(run_usage_from_token_usage);
|
||||
Ok(Conclusion {
|
||||
timestamp,
|
||||
status: StageStatus::from_str(&required_string(properties, "status")?).map_err(|err| {
|
||||
status: StageStatus::from_str(&props.status).map_err(|err| {
|
||||
StoreError::InvalidEvent(format!("invalid completed stage status: {err}"))
|
||||
})?,
|
||||
duration_ms: required_u64(properties, "duration_ms")?,
|
||||
duration_ms: props.duration_ms,
|
||||
failure_reason: None,
|
||||
final_git_commit_sha: optional_string(properties, "final_git_commit_sha"),
|
||||
final_git_commit_sha: props.final_git_commit_sha.clone(),
|
||||
stages: Vec::new(),
|
||||
total_cost: properties.get("total_cost").and_then(Value::as_f64),
|
||||
total_cost: props.total_cost,
|
||||
total_retries: 0,
|
||||
total_input_tokens: usage.as_ref().map_or(0, |usage| usage.input_tokens),
|
||||
total_output_tokens: usage.as_ref().map_or(0, |usage| usage.output_tokens),
|
||||
|
|
@ -519,19 +466,13 @@ fn conclusion_from_completed(
|
|||
})
|
||||
}
|
||||
|
||||
fn conclusion_from_failed(
|
||||
properties: &serde_json::Map<String, Value>,
|
||||
timestamp: DateTime<Utc>,
|
||||
) -> Conclusion {
|
||||
fn conclusion_from_failed(props: &RunFailedProps, timestamp: DateTime<Utc>) -> Conclusion {
|
||||
Conclusion {
|
||||
timestamp,
|
||||
status: StageStatus::Fail,
|
||||
duration_ms: properties
|
||||
.get("duration_ms")
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or_default(),
|
||||
failure_reason: optional_string(properties, "error"),
|
||||
final_git_commit_sha: optional_string(properties, "git_commit_sha"),
|
||||
duration_ms: props.duration_ms,
|
||||
failure_reason: Some(props.error.clone()),
|
||||
final_git_commit_sha: props.git_commit_sha.clone(),
|
||||
stages: Vec::new(),
|
||||
total_cost: None,
|
||||
total_retries: 0,
|
||||
|
|
@ -546,34 +487,33 @@ fn conclusion_from_failed(
|
|||
|
||||
fn stage_visit(
|
||||
node_id: &str,
|
||||
properties: &serde_json::Map<String, Value>,
|
||||
node_visits: Option<&BTreeMap<String, usize>>,
|
||||
state: &RunProjection,
|
||||
) -> Option<u32> {
|
||||
properties
|
||||
.get("node_visits")
|
||||
.and_then(|value| serde_json::from_value::<HashMap<String, usize>>(value.clone()).ok())
|
||||
node_visits
|
||||
.and_then(|visits| visits.get(node_id).copied())
|
||||
.and_then(|visit| u32::try_from(visit).ok())
|
||||
.or_else(|| state.current_visit_for(node_id))
|
||||
}
|
||||
|
||||
fn stage_outcome_from_properties(
|
||||
properties: &serde_json::Map<String, Value>,
|
||||
) -> Result<Outcome<Option<StageUsage>>> {
|
||||
let status = StageStatus::from_str(&required_string(properties, "status")?)
|
||||
.map_err(|err| StoreError::InvalidEvent(format!("invalid stage status: {err}")))?;
|
||||
Ok(Outcome {
|
||||
status,
|
||||
preferred_label: optional_string(properties, "preferred_label"),
|
||||
suggested_next_ids: optional_json(properties, "suggested_next_ids")?.unwrap_or_default(),
|
||||
context_updates: optional_json(properties, "context_updates")?.unwrap_or_default(),
|
||||
jump_to_node: optional_string(properties, "jump_to_node"),
|
||||
notes: optional_string(properties, "notes"),
|
||||
failure: optional_json(properties, "failure")?,
|
||||
usage: optional_json(properties, "usage")?,
|
||||
files_touched: optional_json(properties, "files_touched")?.unwrap_or_default(),
|
||||
duration_ms: properties.get("duration_ms").and_then(Value::as_u64),
|
||||
})
|
||||
fn stage_outcome_from_props(props: &StageCompletedProps) -> Outcome<Option<StageUsage>> {
|
||||
Outcome {
|
||||
status: props.status.clone(),
|
||||
preferred_label: props.preferred_label.clone(),
|
||||
suggested_next_ids: props.suggested_next_ids.clone(),
|
||||
context_updates: props
|
||||
.context_updates
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.collect(),
|
||||
jump_to_node: props.jump_to_node.clone(),
|
||||
notes: props.notes.clone(),
|
||||
failure: props.failure.clone(),
|
||||
usage: props.usage.clone(),
|
||||
files_touched: props.files_touched.clone(),
|
||||
duration_ms: Some(props.duration_ms),
|
||||
}
|
||||
}
|
||||
|
||||
fn node_status_from_outcome(
|
||||
|
|
@ -591,41 +531,55 @@ fn node_status_from_outcome(
|
|||
}
|
||||
}
|
||||
|
||||
fn provider_used_from_prompt(properties: &serde_json::Map<String, Value>) -> Option<Value> {
|
||||
fn provider_used_from_prompt(props: &fabro_types::stored_event::StagePromptProps) -> Option<Value> {
|
||||
let mut provider_used = serde_json::Map::new();
|
||||
if let Some(mode) = optional_string(properties, "mode") {
|
||||
if let Some(mode) = props.mode.clone() {
|
||||
provider_used.insert("mode".to_string(), Value::String(mode));
|
||||
}
|
||||
if let Some(provider) = optional_string(properties, "provider") {
|
||||
if let Some(provider) = props.provider.clone() {
|
||||
provider_used.insert("provider".to_string(), Value::String(provider));
|
||||
}
|
||||
if let Some(model) = optional_string(properties, "model") {
|
||||
if let Some(model) = props.model.clone() {
|
||||
provider_used.insert("model".to_string(), Value::String(model));
|
||||
}
|
||||
(!provider_used.is_empty()).then_some(Value::Object(provider_used))
|
||||
}
|
||||
|
||||
fn provider_used_from_agent_event(
|
||||
event_name: &str,
|
||||
properties: &serde_json::Map<String, Value>,
|
||||
fn provider_used_from_agent_session_started(
|
||||
props: &fabro_types::stored_event::AgentSessionStartedProps,
|
||||
) -> Value {
|
||||
let mut provider_used = serde_json::Map::new();
|
||||
provider_used.insert(
|
||||
"mode".to_string(),
|
||||
Value::String(if event_name == "agent.cli.started" {
|
||||
"cli".to_string()
|
||||
} else {
|
||||
"agent".to_string()
|
||||
}),
|
||||
);
|
||||
if let Some(provider) = optional_string(properties, "provider") {
|
||||
provider_used.insert("mode".to_string(), Value::String("agent".to_string()));
|
||||
if let Some(provider) = props.provider.clone() {
|
||||
provider_used.insert("provider".to_string(), Value::String(provider));
|
||||
}
|
||||
if let Some(model) = optional_string(properties, "model") {
|
||||
if let Some(model) = props.model.clone() {
|
||||
provider_used.insert("model".to_string(), Value::String(model));
|
||||
}
|
||||
if let Some(command) = optional_string(properties, "command") {
|
||||
provider_used.insert("command".to_string(), Value::String(command));
|
||||
}
|
||||
Value::Object(provider_used)
|
||||
}
|
||||
|
||||
fn provider_used_from_agent_cli_started(
|
||||
props: &fabro_types::stored_event::AgentCliStartedProps,
|
||||
) -> Value {
|
||||
let mut provider_used = serde_json::Map::new();
|
||||
provider_used.insert("mode".to_string(), Value::String("cli".to_string()));
|
||||
provider_used.insert(
|
||||
"provider".to_string(),
|
||||
Value::String(props.provider.clone()),
|
||||
);
|
||||
provider_used.insert("model".to_string(), Value::String(props.model.clone()));
|
||||
provider_used.insert("command".to_string(), Value::String(props.command.clone()));
|
||||
Value::Object(provider_used)
|
||||
}
|
||||
|
||||
fn run_usage_from_token_usage(usage: &TokenUsage) -> RunUsage {
|
||||
RunUsage {
|
||||
input_tokens: usage.input_tokens,
|
||||
output_tokens: usage.output_tokens,
|
||||
reasoning_tokens: usage.reasoning_tokens,
|
||||
cache_read_tokens: usage.cache_read_tokens,
|
||||
cache_write_tokens: usage.cache_write_tokens,
|
||||
cost: None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -421,6 +421,7 @@ mod tests {
|
|||
node_id: Option<&str>,
|
||||
properties: serde_json::Value,
|
||||
) -> EventPayload {
|
||||
let properties = normalize_test_event_properties(run_id, event, properties);
|
||||
let mut value = serde_json::json!({
|
||||
"id": format!("evt-{run_id}-{event}"),
|
||||
"ts": ts,
|
||||
|
|
@ -434,6 +435,118 @@ mod tests {
|
|||
EventPayload::new(value, &test_run_id(run_id)).unwrap()
|
||||
}
|
||||
|
||||
fn normalize_test_event_properties(
|
||||
run_id: &str,
|
||||
event: &str,
|
||||
properties: serde_json::Value,
|
||||
) -> serde_json::Value {
|
||||
let serde_json::Value::Object(mut props) = properties else {
|
||||
return properties;
|
||||
};
|
||||
|
||||
match event {
|
||||
"run.created" => {
|
||||
props
|
||||
.entry("run_dir")
|
||||
.or_insert_with(|| serde_json::Value::String(format!("/tmp/{run_id}")));
|
||||
}
|
||||
"run.started" => {
|
||||
props
|
||||
.entry("name")
|
||||
.or_insert_with(|| serde_json::Value::String("night-sky".to_string()));
|
||||
}
|
||||
"sandbox.initialized" => {
|
||||
props
|
||||
.entry("provider")
|
||||
.or_insert_with(|| serde_json::Value::String("local".to_string()));
|
||||
}
|
||||
"checkpoint.completed" => {
|
||||
props
|
||||
.entry("completed_nodes")
|
||||
.or_insert_with(|| serde_json::Value::Array(Vec::new()));
|
||||
props
|
||||
.entry("node_retries")
|
||||
.or_insert_with(|| serde_json::json!({}));
|
||||
props
|
||||
.entry("context_values")
|
||||
.or_insert_with(|| serde_json::json!({}));
|
||||
props
|
||||
.entry("node_outcomes")
|
||||
.or_insert_with(|| serde_json::json!({}));
|
||||
props
|
||||
.entry("node_visits")
|
||||
.or_insert_with(|| serde_json::json!({}));
|
||||
}
|
||||
"parallel.completed" => {
|
||||
props
|
||||
.entry("visit")
|
||||
.or_insert_with(|| serde_json::Value::from(1));
|
||||
props
|
||||
.entry("duration_ms")
|
||||
.or_insert_with(|| serde_json::Value::from(0));
|
||||
props
|
||||
.entry("success_count")
|
||||
.or_insert_with(|| serde_json::Value::from(0));
|
||||
props
|
||||
.entry("failure_count")
|
||||
.or_insert_with(|| serde_json::Value::from(0));
|
||||
}
|
||||
"stage.completed" => {
|
||||
if let Some(visit) = props.get("visit").cloned() {
|
||||
props.entry("index").or_insert(visit);
|
||||
}
|
||||
props
|
||||
.entry("index")
|
||||
.or_insert_with(|| serde_json::Value::from(0));
|
||||
props
|
||||
.entry("duration_ms")
|
||||
.or_insert_with(|| serde_json::Value::from(0));
|
||||
props
|
||||
.entry("attempt")
|
||||
.or_insert_with(|| serde_json::Value::from(1));
|
||||
props
|
||||
.entry("max_attempts")
|
||||
.or_insert_with(|| serde_json::Value::from(1));
|
||||
}
|
||||
"command.started" => {
|
||||
let default_script = props
|
||||
.get("command")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| serde_json::Value::String(String::new()));
|
||||
props.entry("script").or_insert(default_script);
|
||||
props
|
||||
.entry("language")
|
||||
.or_insert_with(|| serde_json::Value::String("shell".to_string()));
|
||||
}
|
||||
"command.completed" => {
|
||||
props
|
||||
.entry("duration_ms")
|
||||
.or_insert_with(|| serde_json::Value::from(0));
|
||||
props
|
||||
.entry("timed_out")
|
||||
.or_insert_with(|| serde_json::Value::Bool(false));
|
||||
}
|
||||
"run.completed" => {
|
||||
props
|
||||
.entry("artifact_count")
|
||||
.or_insert_with(|| serde_json::Value::from(0));
|
||||
}
|
||||
"pull_request.created" => {
|
||||
props
|
||||
.entry("draft")
|
||||
.or_insert_with(|| serde_json::Value::Bool(false));
|
||||
}
|
||||
"retro.completed" => {
|
||||
props
|
||||
.entry("duration_ms")
|
||||
.or_insert_with(|| serde_json::Value::from(0));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
serde_json::Value::Object(props)
|
||||
}
|
||||
|
||||
fn sample_start_record(run_id: &str, created_at: DateTime<Utc>) -> StartRecord {
|
||||
StartRecord {
|
||||
run_id: test_run_id(run_id),
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use chrono::{DateTime, Utc};
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Result, StoreError};
|
||||
use fabro_types::{RunId, RunStatus, StatusReason};
|
||||
use fabro_types::{RunId, RunStatus, StatusReason, StoredEvent};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RunSummary {
|
||||
|
|
@ -70,6 +70,15 @@ impl EventPayload {
|
|||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&EventPayload> for StoredEvent {
|
||||
type Error = StoreError;
|
||||
|
||||
fn try_from(value: &EventPayload) -> Result<Self> {
|
||||
StoredEvent::from_value(value.as_value().clone())
|
||||
.map_err(|err| StoreError::InvalidEvent(format!("invalid stored event: {err}")))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct EventEnvelope {
|
||||
pub seq: u32,
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ pub fn shape_to_handler_type(shape: &str) -> Option<&'static str> {
|
|||
}
|
||||
|
||||
/// A node in the workflow graph.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Node {
|
||||
pub id: String,
|
||||
pub attrs: HashMap<String, AttrValue>,
|
||||
|
|
@ -256,7 +256,7 @@ impl Node {
|
|||
}
|
||||
|
||||
/// An edge connecting two nodes in the workflow graph.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Edge {
|
||||
pub from: String,
|
||||
pub to: String,
|
||||
|
|
@ -321,7 +321,7 @@ impl Edge {
|
|||
}
|
||||
|
||||
/// The parsed workflow graph containing nodes, edges, and graph-level attributes.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct Graph {
|
||||
pub name: String,
|
||||
pub nodes: HashMap<String, Node>,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ pub mod settings;
|
|||
pub mod stage_id;
|
||||
pub mod start;
|
||||
pub mod status;
|
||||
pub mod stored_event;
|
||||
pub mod usage;
|
||||
|
||||
pub use checkpoint::Checkpoint;
|
||||
|
|
@ -41,6 +42,7 @@ pub use start::StartRecord;
|
|||
pub use status::{
|
||||
InvalidTransition, ParseRunStatusError, RunStatus, RunStatusRecord, StatusReason,
|
||||
};
|
||||
pub use stored_event::{EventBody, RunNoticeLevel, StoredEvent, TokenUsage};
|
||||
pub use usage::StageUsage;
|
||||
|
||||
pub use fabro_macros::Combine;
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ impl FailureCategory {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct FailureDetail {
|
||||
pub message: String,
|
||||
#[serde(rename = "failure_class")]
|
||||
|
|
@ -140,7 +140,7 @@ impl FailureDetail {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(bound = "M: OutcomeMeta")]
|
||||
pub struct Outcome<M: OutcomeMeta = ()> {
|
||||
pub status: StageStatus,
|
||||
|
|
|
|||
158
lib/crates/fabro-types/src/stored_event/agent.rs
Normal file
158
lib/crates/fabro-types/src/stored_event/agent.rs
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::TokenUsage;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentSessionStartedProps {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub provider: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub model: Option<String>,
|
||||
pub visit: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentSessionEndedProps {
|
||||
pub visit: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentProcessingEndProps {
|
||||
pub visit: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentInputProps {
|
||||
pub text: String,
|
||||
pub visit: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentMessageProps {
|
||||
pub text: String,
|
||||
pub model: String,
|
||||
pub usage: TokenUsage,
|
||||
pub tool_call_count: usize,
|
||||
pub visit: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentToolStartedProps {
|
||||
pub tool_name: String,
|
||||
pub tool_call_id: String,
|
||||
pub arguments: Value,
|
||||
pub visit: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentToolCompletedProps {
|
||||
pub tool_name: String,
|
||||
pub tool_call_id: String,
|
||||
pub output: Value,
|
||||
pub is_error: bool,
|
||||
pub visit: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentErrorProps {
|
||||
pub error: Value,
|
||||
pub visit: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentWarningProps {
|
||||
pub kind: String,
|
||||
pub message: String,
|
||||
pub details: Value,
|
||||
pub visit: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentLoopDetectedProps {
|
||||
pub visit: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentTurnLimitReachedProps {
|
||||
pub max_turns: usize,
|
||||
pub visit: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentSteeringInjectedProps {
|
||||
pub text: String,
|
||||
pub visit: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentCompactionStartedProps {
|
||||
pub estimated_tokens: usize,
|
||||
pub context_window_size: usize,
|
||||
pub visit: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentCompactionCompletedProps {
|
||||
pub original_turn_count: usize,
|
||||
pub preserved_turn_count: usize,
|
||||
pub summary_token_estimate: usize,
|
||||
pub tracked_file_count: usize,
|
||||
pub visit: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentLlmRetryProps {
|
||||
pub provider: String,
|
||||
pub model: String,
|
||||
pub attempt: usize,
|
||||
pub delay_secs: f64,
|
||||
pub error: Value,
|
||||
pub visit: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentSubSpawnedProps {
|
||||
pub agent_id: String,
|
||||
pub depth: usize,
|
||||
pub task: String,
|
||||
pub visit: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentSubCompletedProps {
|
||||
pub agent_id: String,
|
||||
pub depth: usize,
|
||||
pub success: bool,
|
||||
pub turns_used: usize,
|
||||
pub visit: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentSubFailedProps {
|
||||
pub agent_id: String,
|
||||
pub depth: usize,
|
||||
pub error: Value,
|
||||
pub visit: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentSubClosedProps {
|
||||
pub agent_id: String,
|
||||
pub depth: usize,
|
||||
pub visit: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentMcpReadyProps {
|
||||
pub server_name: String,
|
||||
pub tool_count: usize,
|
||||
pub visit: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentMcpFailedProps {
|
||||
pub server_name: String,
|
||||
pub error: String,
|
||||
pub visit: u32,
|
||||
}
|
||||
192
lib/crates/fabro-types/src/stored_event/infra.rs
Normal file
192
lib/crates/fabro-types/src/stored_event/infra.rs
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SandboxInitializingProps {
|
||||
pub provider: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SandboxReadyProps {
|
||||
pub provider: String,
|
||||
pub duration_ms: u64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cpu: Option<f64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub memory: Option<f64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SandboxFailedProps {
|
||||
pub provider: String,
|
||||
pub error: String,
|
||||
pub duration_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SandboxCleanupStartedProps {
|
||||
pub provider: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SandboxCleanupCompletedProps {
|
||||
pub provider: String,
|
||||
pub duration_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SandboxCleanupFailedProps {
|
||||
pub provider: String,
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SnapshotNameProps {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SnapshotCompletedProps {
|
||||
pub name: String,
|
||||
pub duration_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SnapshotFailedProps {
|
||||
pub name: String,
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GitCloneStartedProps {
|
||||
pub url: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub branch: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GitCloneCompletedProps {
|
||||
pub url: String,
|
||||
pub duration_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GitCloneFailedProps {
|
||||
pub url: String,
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SandboxInitializedProps {
|
||||
pub working_directory: String,
|
||||
pub provider: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub identifier: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub host_working_directory: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub container_mount_point: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SetupStartedProps {
|
||||
pub command_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SetupCommandStartedProps {
|
||||
pub command: String,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SetupCommandCompletedProps {
|
||||
pub command: String,
|
||||
pub index: usize,
|
||||
pub exit_code: i32,
|
||||
pub duration_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SetupCompletedProps {
|
||||
pub duration_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SetupFailedProps {
|
||||
pub command: String,
|
||||
pub index: usize,
|
||||
pub exit_code: i32,
|
||||
pub stderr: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CliEnsureStartedProps {
|
||||
pub cli_name: String,
|
||||
pub provider: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CliEnsureCompletedProps {
|
||||
pub cli_name: String,
|
||||
pub provider: String,
|
||||
pub already_installed: bool,
|
||||
pub node_installed: bool,
|
||||
pub duration_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CliEnsureFailedProps {
|
||||
pub cli_name: String,
|
||||
pub provider: String,
|
||||
pub error: String,
|
||||
pub duration_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct DevcontainerResolvedProps {
|
||||
pub dockerfile_lines: usize,
|
||||
pub environment_count: usize,
|
||||
pub lifecycle_command_count: usize,
|
||||
pub workspace_folder: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct DevcontainerLifecycleStartedProps {
|
||||
pub phase: String,
|
||||
pub command_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct DevcontainerLifecycleCommandStartedProps {
|
||||
pub phase: String,
|
||||
pub command: String,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct DevcontainerLifecycleCommandCompletedProps {
|
||||
pub phase: String,
|
||||
pub command: String,
|
||||
pub index: usize,
|
||||
pub exit_code: i32,
|
||||
pub duration_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct DevcontainerLifecycleCompletedProps {
|
||||
pub phase: String,
|
||||
pub duration_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct DevcontainerLifecycleFailedProps {
|
||||
pub phase: String,
|
||||
pub command: String,
|
||||
pub index: usize,
|
||||
pub exit_code: i32,
|
||||
pub stderr: String,
|
||||
}
|
||||
259
lib/crates/fabro-types/src/stored_event/misc.rs
Normal file
259
lib/crates/fabro-types/src/stored_event/misc.rs
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::TokenUsage;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ParallelStartedProps {
|
||||
pub visit: u32,
|
||||
pub branch_count: usize,
|
||||
pub join_policy: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ParallelBranchStartedProps {
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ParallelBranchCompletedProps {
|
||||
pub index: usize,
|
||||
pub duration_ms: u64,
|
||||
pub status: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub head_sha: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ParallelCompletedProps {
|
||||
pub visit: u32,
|
||||
pub duration_ms: u64,
|
||||
pub success_count: usize,
|
||||
pub failure_count: usize,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub results: Vec<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct InterviewStartedProps {
|
||||
pub question: String,
|
||||
pub question_type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct InterviewCompletedProps {
|
||||
pub question: String,
|
||||
pub answer: String,
|
||||
pub duration_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct InterviewTimeoutProps {
|
||||
pub question: String,
|
||||
pub duration_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GitCommitProps {
|
||||
pub sha: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GitPushProps {
|
||||
pub branch: String,
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GitBranchProps {
|
||||
pub branch: String,
|
||||
pub sha: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GitWorktreeAddProps {
|
||||
pub path: String,
|
||||
pub branch: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GitWorktreeRemoveProps {
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GitFetchProps {
|
||||
pub branch: String,
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GitResetProps {
|
||||
pub sha: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct EdgeSelectedProps {
|
||||
pub from_node: String,
|
||||
pub to_node: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub label: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub condition: Option<String>,
|
||||
pub reason: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub preferred_label: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub suggested_next_ids: Vec<String>,
|
||||
pub stage_status: String,
|
||||
pub is_jump: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct LoopRestartProps {
|
||||
pub from_node: String,
|
||||
pub to_node: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SubgraphStartedProps {
|
||||
pub start_node: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SubgraphCompletedProps {
|
||||
pub steps_executed: usize,
|
||||
pub status: String,
|
||||
pub duration_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct StallWatchdogTimeoutProps {
|
||||
pub idle_seconds: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AssetCapturedProps {
|
||||
pub attempt: u32,
|
||||
pub node_slug: String,
|
||||
pub path: String,
|
||||
pub mime: String,
|
||||
pub content_md5: String,
|
||||
pub content_sha256: String,
|
||||
pub bytes: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SshAccessReadyProps {
|
||||
pub ssh_command: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct FailoverProps {
|
||||
pub from_provider: String,
|
||||
pub from_model: String,
|
||||
pub to_provider: String,
|
||||
pub to_model: String,
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CommandStartedProps {
|
||||
pub script: String,
|
||||
pub command: String,
|
||||
pub language: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub timeout_ms: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CommandCompletedProps {
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub exit_code: Option<i32>,
|
||||
pub duration_ms: u64,
|
||||
pub timed_out: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentCliStartedProps {
|
||||
pub visit: u32,
|
||||
pub mode: String,
|
||||
pub provider: String,
|
||||
pub model: String,
|
||||
pub command: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentCliCompletedProps {
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
pub exit_code: i32,
|
||||
pub duration_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PullRequestCreatedProps {
|
||||
pub pr_url: String,
|
||||
pub pr_number: u64,
|
||||
pub owner: String,
|
||||
pub repo: String,
|
||||
pub base_branch: String,
|
||||
pub head_branch: String,
|
||||
pub title: String,
|
||||
pub draft: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PullRequestFailedProps {
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RetroStartedProps {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub prompt: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub provider: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RetroCompletedProps {
|
||||
pub duration_ms: u64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub response: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub retro: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RetroFailedProps {
|
||||
pub error: String,
|
||||
pub duration_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AssistantUsageProps {
|
||||
pub model: String,
|
||||
pub input_tokens: u64,
|
||||
pub output_tokens: u64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub speed: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cost: Option<f64>,
|
||||
}
|
||||
|
||||
impl From<TokenUsage> for AssistantUsageProps {
|
||||
fn from(value: TokenUsage) -> Self {
|
||||
Self {
|
||||
model: String::new(),
|
||||
input_tokens: u64::try_from(value.input_tokens).unwrap_or_default(),
|
||||
output_tokens: u64::try_from(value.output_tokens).unwrap_or_default(),
|
||||
speed: value.speed,
|
||||
cost: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
494
lib/crates/fabro-types/src/stored_event/mod.rs
Normal file
494
lib/crates/fabro-types/src/stored_event/mod.rs
Normal file
|
|
@ -0,0 +1,494 @@
|
|||
pub mod agent;
|
||||
pub mod infra;
|
||||
pub mod misc;
|
||||
pub mod run;
|
||||
pub mod stage;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::de::Error as DeError;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
use crate::RunId;
|
||||
|
||||
pub use agent::*;
|
||||
pub use infra::*;
|
||||
pub use misc::*;
|
||||
pub use run::*;
|
||||
pub use stage::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RunNoticeLevel {
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct TokenUsage {
|
||||
pub input_tokens: i64,
|
||||
pub output_tokens: i64,
|
||||
pub total_tokens: i64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub reasoning_tokens: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_tokens: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cache_write_tokens: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub speed: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub raw: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct StoredEvent {
|
||||
pub id: String,
|
||||
pub ts: DateTime<Utc>,
|
||||
pub run_id: RunId,
|
||||
pub event: String,
|
||||
pub node_id: Option<String>,
|
||||
pub node_label: Option<String>,
|
||||
pub session_id: Option<String>,
|
||||
pub parent_session_id: Option<String>,
|
||||
pub properties: Value,
|
||||
pub body: EventBody,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(tag = "event", content = "properties")]
|
||||
pub enum EventBody {
|
||||
#[serde(rename = "run.created")]
|
||||
RunCreated(RunCreatedProps),
|
||||
#[serde(rename = "run.started")]
|
||||
RunStarted(RunStartedProps),
|
||||
#[serde(rename = "run.submitted")]
|
||||
RunSubmitted(RunStatusTransitionProps),
|
||||
#[serde(rename = "run.starting")]
|
||||
RunStarting(RunStatusTransitionProps),
|
||||
#[serde(rename = "run.running")]
|
||||
RunRunning(RunStatusTransitionProps),
|
||||
#[serde(rename = "run.removing")]
|
||||
RunRemoving(RunStatusTransitionProps),
|
||||
#[serde(rename = "run.rewound")]
|
||||
RunRewound(RunRewoundProps),
|
||||
#[serde(rename = "run.completed")]
|
||||
RunCompleted(RunCompletedProps),
|
||||
#[serde(rename = "run.failed")]
|
||||
RunFailed(RunFailedProps),
|
||||
#[serde(rename = "run.notice")]
|
||||
RunNotice(RunNoticeProps),
|
||||
#[serde(rename = "stage.started")]
|
||||
StageStarted(StageStartedProps),
|
||||
#[serde(rename = "stage.completed")]
|
||||
StageCompleted(StageCompletedProps),
|
||||
#[serde(rename = "stage.failed")]
|
||||
StageFailed(StageFailedProps),
|
||||
#[serde(rename = "stage.retrying")]
|
||||
StageRetrying(StageRetryingProps),
|
||||
#[serde(rename = "parallel.started")]
|
||||
ParallelStarted(ParallelStartedProps),
|
||||
#[serde(rename = "parallel.branch.started")]
|
||||
ParallelBranchStarted(ParallelBranchStartedProps),
|
||||
#[serde(rename = "parallel.branch.completed")]
|
||||
ParallelBranchCompleted(ParallelBranchCompletedProps),
|
||||
#[serde(rename = "parallel.completed")]
|
||||
ParallelCompleted(ParallelCompletedProps),
|
||||
#[serde(rename = "interview.started")]
|
||||
InterviewStarted(InterviewStartedProps),
|
||||
#[serde(rename = "interview.completed")]
|
||||
InterviewCompleted(InterviewCompletedProps),
|
||||
#[serde(rename = "interview.timeout")]
|
||||
InterviewTimeout(InterviewTimeoutProps),
|
||||
#[serde(rename = "checkpoint.completed")]
|
||||
CheckpointCompleted(CheckpointCompletedProps),
|
||||
#[serde(rename = "checkpoint.failed")]
|
||||
CheckpointFailed(CheckpointFailedProps),
|
||||
#[serde(rename = "git.commit")]
|
||||
GitCommit(GitCommitProps),
|
||||
#[serde(rename = "git.push")]
|
||||
GitPush(GitPushProps),
|
||||
#[serde(rename = "git.branch")]
|
||||
GitBranch(GitBranchProps),
|
||||
#[serde(rename = "git.worktree.added")]
|
||||
GitWorktreeAdd(GitWorktreeAddProps),
|
||||
#[serde(rename = "git.worktree.removed")]
|
||||
GitWorktreeRemove(GitWorktreeRemoveProps),
|
||||
#[serde(rename = "git.fetch")]
|
||||
GitFetch(GitFetchProps),
|
||||
#[serde(rename = "git.reset")]
|
||||
GitReset(GitResetProps),
|
||||
#[serde(rename = "edge.selected")]
|
||||
EdgeSelected(EdgeSelectedProps),
|
||||
#[serde(rename = "loop.restart")]
|
||||
LoopRestart(LoopRestartProps),
|
||||
#[serde(rename = "stage.prompt")]
|
||||
StagePrompt(StagePromptProps),
|
||||
#[serde(rename = "prompt.completed")]
|
||||
PromptCompleted(PromptCompletedProps),
|
||||
#[serde(rename = "agent.session.started")]
|
||||
AgentSessionStarted(AgentSessionStartedProps),
|
||||
#[serde(rename = "agent.session.ended")]
|
||||
AgentSessionEnded(AgentSessionEndedProps),
|
||||
#[serde(rename = "agent.processing.end")]
|
||||
AgentProcessingEnd(AgentProcessingEndProps),
|
||||
#[serde(rename = "agent.input")]
|
||||
AgentInput(AgentInputProps),
|
||||
#[serde(rename = "agent.message")]
|
||||
AgentMessage(AgentMessageProps),
|
||||
#[serde(rename = "agent.tool.started")]
|
||||
AgentToolStarted(AgentToolStartedProps),
|
||||
#[serde(rename = "agent.tool.completed")]
|
||||
AgentToolCompleted(AgentToolCompletedProps),
|
||||
#[serde(rename = "agent.error")]
|
||||
AgentError(AgentErrorProps),
|
||||
#[serde(rename = "agent.warning")]
|
||||
AgentWarning(AgentWarningProps),
|
||||
#[serde(rename = "agent.loop.detected")]
|
||||
AgentLoopDetected(AgentLoopDetectedProps),
|
||||
#[serde(rename = "agent.turn.limit")]
|
||||
AgentTurnLimitReached(AgentTurnLimitReachedProps),
|
||||
#[serde(rename = "agent.steering.injected")]
|
||||
AgentSteeringInjected(AgentSteeringInjectedProps),
|
||||
#[serde(rename = "agent.compaction.started")]
|
||||
AgentCompactionStarted(AgentCompactionStartedProps),
|
||||
#[serde(rename = "agent.compaction.completed")]
|
||||
AgentCompactionCompleted(AgentCompactionCompletedProps),
|
||||
#[serde(rename = "agent.llm.retry")]
|
||||
AgentLlmRetry(AgentLlmRetryProps),
|
||||
#[serde(rename = "agent.sub.spawned")]
|
||||
AgentSubSpawned(AgentSubSpawnedProps),
|
||||
#[serde(rename = "agent.sub.completed")]
|
||||
AgentSubCompleted(AgentSubCompletedProps),
|
||||
#[serde(rename = "agent.sub.failed")]
|
||||
AgentSubFailed(AgentSubFailedProps),
|
||||
#[serde(rename = "agent.sub.closed")]
|
||||
AgentSubClosed(AgentSubClosedProps),
|
||||
#[serde(rename = "agent.mcp.ready")]
|
||||
AgentMcpReady(AgentMcpReadyProps),
|
||||
#[serde(rename = "agent.mcp.failed")]
|
||||
AgentMcpFailed(AgentMcpFailedProps),
|
||||
#[serde(rename = "subgraph.started")]
|
||||
SubgraphStarted(SubgraphStartedProps),
|
||||
#[serde(rename = "subgraph.completed")]
|
||||
SubgraphCompleted(SubgraphCompletedProps),
|
||||
#[serde(rename = "sandbox.initializing")]
|
||||
SandboxInitializing(SandboxInitializingProps),
|
||||
#[serde(rename = "sandbox.ready")]
|
||||
SandboxReady(SandboxReadyProps),
|
||||
#[serde(rename = "sandbox.failed")]
|
||||
SandboxFailed(SandboxFailedProps),
|
||||
#[serde(rename = "sandbox.cleanup.started")]
|
||||
SandboxCleanupStarted(SandboxCleanupStartedProps),
|
||||
#[serde(rename = "sandbox.cleanup.completed")]
|
||||
SandboxCleanupCompleted(SandboxCleanupCompletedProps),
|
||||
#[serde(rename = "sandbox.cleanup.failed")]
|
||||
SandboxCleanupFailed(SandboxCleanupFailedProps),
|
||||
#[serde(rename = "sandbox.snapshot.pulling")]
|
||||
SnapshotPulling(SnapshotNameProps),
|
||||
#[serde(rename = "sandbox.snapshot.pulled")]
|
||||
SnapshotPulled(SnapshotCompletedProps),
|
||||
#[serde(rename = "sandbox.snapshot.ensuring")]
|
||||
SnapshotEnsuring(SnapshotNameProps),
|
||||
#[serde(rename = "sandbox.snapshot.creating")]
|
||||
SnapshotCreating(SnapshotNameProps),
|
||||
#[serde(rename = "sandbox.snapshot.ready")]
|
||||
SnapshotReady(SnapshotCompletedProps),
|
||||
#[serde(rename = "sandbox.snapshot.failed")]
|
||||
SnapshotFailed(SnapshotFailedProps),
|
||||
#[serde(rename = "sandbox.git.started")]
|
||||
GitCloneStarted(GitCloneStartedProps),
|
||||
#[serde(rename = "sandbox.git.completed")]
|
||||
GitCloneCompleted(GitCloneCompletedProps),
|
||||
#[serde(rename = "sandbox.git.failed")]
|
||||
GitCloneFailed(GitCloneFailedProps),
|
||||
#[serde(rename = "sandbox.initialized")]
|
||||
SandboxInitialized(SandboxInitializedProps),
|
||||
#[serde(rename = "setup.started")]
|
||||
SetupStarted(SetupStartedProps),
|
||||
#[serde(rename = "setup.command.started")]
|
||||
SetupCommandStarted(SetupCommandStartedProps),
|
||||
#[serde(rename = "setup.command.completed")]
|
||||
SetupCommandCompleted(SetupCommandCompletedProps),
|
||||
#[serde(rename = "setup.completed")]
|
||||
SetupCompleted(SetupCompletedProps),
|
||||
#[serde(rename = "setup.failed")]
|
||||
SetupFailed(SetupFailedProps),
|
||||
#[serde(rename = "watchdog.timeout")]
|
||||
StallWatchdogTimeout(StallWatchdogTimeoutProps),
|
||||
#[serde(rename = "asset.captured")]
|
||||
AssetCaptured(AssetCapturedProps),
|
||||
#[serde(rename = "ssh.ready")]
|
||||
SshAccessReady(SshAccessReadyProps),
|
||||
#[serde(rename = "agent.failover")]
|
||||
Failover(FailoverProps),
|
||||
#[serde(rename = "cli.ensure.started")]
|
||||
CliEnsureStarted(CliEnsureStartedProps),
|
||||
#[serde(rename = "cli.ensure.completed")]
|
||||
CliEnsureCompleted(CliEnsureCompletedProps),
|
||||
#[serde(rename = "cli.ensure.failed")]
|
||||
CliEnsureFailed(CliEnsureFailedProps),
|
||||
#[serde(rename = "command.started")]
|
||||
CommandStarted(CommandStartedProps),
|
||||
#[serde(rename = "command.completed")]
|
||||
CommandCompleted(CommandCompletedProps),
|
||||
#[serde(rename = "agent.cli.started")]
|
||||
AgentCliStarted(AgentCliStartedProps),
|
||||
#[serde(rename = "agent.cli.completed")]
|
||||
AgentCliCompleted(AgentCliCompletedProps),
|
||||
#[serde(rename = "pull_request.created")]
|
||||
PullRequestCreated(PullRequestCreatedProps),
|
||||
#[serde(rename = "pull_request.failed")]
|
||||
PullRequestFailed(PullRequestFailedProps),
|
||||
#[serde(rename = "devcontainer.resolved")]
|
||||
DevcontainerResolved(DevcontainerResolvedProps),
|
||||
#[serde(rename = "devcontainer.lifecycle.started")]
|
||||
DevcontainerLifecycleStarted(DevcontainerLifecycleStartedProps),
|
||||
#[serde(rename = "devcontainer.lifecycle.command.started")]
|
||||
DevcontainerLifecycleCommandStarted(DevcontainerLifecycleCommandStartedProps),
|
||||
#[serde(rename = "devcontainer.lifecycle.command.completed")]
|
||||
DevcontainerLifecycleCommandCompleted(DevcontainerLifecycleCommandCompletedProps),
|
||||
#[serde(rename = "devcontainer.lifecycle.completed")]
|
||||
DevcontainerLifecycleCompleted(DevcontainerLifecycleCompletedProps),
|
||||
#[serde(rename = "devcontainer.lifecycle.failed")]
|
||||
DevcontainerLifecycleFailed(DevcontainerLifecycleFailedProps),
|
||||
#[serde(rename = "retro.started")]
|
||||
RetroStarted(RetroStartedProps),
|
||||
#[serde(rename = "retro.completed")]
|
||||
RetroCompleted(RetroCompletedProps),
|
||||
#[serde(rename = "retro.failed")]
|
||||
RetroFailed(RetroFailedProps),
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct StoredEventRaw {
|
||||
id: String,
|
||||
ts: DateTime<Utc>,
|
||||
run_id: RunId,
|
||||
#[serde(default)]
|
||||
node_id: Option<String>,
|
||||
#[serde(default)]
|
||||
node_label: Option<String>,
|
||||
#[serde(default)]
|
||||
session_id: Option<String>,
|
||||
#[serde(default)]
|
||||
parent_session_id: Option<String>,
|
||||
event: String,
|
||||
#[serde(default = "default_properties")]
|
||||
properties: Value,
|
||||
}
|
||||
|
||||
fn default_properties() -> Value {
|
||||
Value::Object(Map::new())
|
||||
}
|
||||
|
||||
impl StoredEvent {
|
||||
pub fn from_value(value: Value) -> serde_json::Result<Self> {
|
||||
let raw: StoredEventRaw = serde_json::from_value(value)?;
|
||||
let body = serde_json::from_value(json!({
|
||||
"event": raw.event,
|
||||
"properties": raw.properties,
|
||||
}))?;
|
||||
Ok(Self {
|
||||
id: raw.id,
|
||||
ts: raw.ts,
|
||||
run_id: raw.run_id,
|
||||
event: event_name_from_body(&body),
|
||||
node_id: raw.node_id,
|
||||
node_label: raw.node_label,
|
||||
session_id: raw.session_id,
|
||||
parent_session_id: raw.parent_session_id,
|
||||
properties: raw.properties,
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_json_str(line: &str) -> serde_json::Result<Self> {
|
||||
Self::from_value(serde_json::from_str(line)?)
|
||||
}
|
||||
|
||||
pub fn to_value(&self) -> serde_json::Result<Value> {
|
||||
let mut map = Map::new();
|
||||
map.insert("id".to_string(), serde_json::to_value(&self.id)?);
|
||||
map.insert("ts".to_string(), serde_json::to_value(self.ts)?);
|
||||
map.insert("run_id".to_string(), serde_json::to_value(self.run_id)?);
|
||||
map.insert(
|
||||
"event".to_string(),
|
||||
Value::String(event_name_from_body(&self.body)),
|
||||
);
|
||||
if let Some(value) = &self.session_id {
|
||||
map.insert("session_id".to_string(), Value::String(value.clone()));
|
||||
}
|
||||
if let Some(value) = &self.parent_session_id {
|
||||
map.insert(
|
||||
"parent_session_id".to_string(),
|
||||
Value::String(value.clone()),
|
||||
);
|
||||
}
|
||||
if let Some(value) = &self.node_id {
|
||||
map.insert("node_id".to_string(), Value::String(value.clone()));
|
||||
}
|
||||
if let Some(value) = &self.node_label {
|
||||
map.insert("node_label".to_string(), Value::String(value.clone()));
|
||||
}
|
||||
map.insert("properties".to_string(), properties_from_body(&self.body));
|
||||
Ok(Value::Object(map))
|
||||
}
|
||||
|
||||
pub fn event_name(&self) -> String {
|
||||
self.event.clone()
|
||||
}
|
||||
|
||||
pub fn properties(&self) -> Value {
|
||||
self.properties.clone()
|
||||
}
|
||||
|
||||
pub fn refresh_cache(&mut self) {
|
||||
self.event = event_name_from_body(&self.body);
|
||||
self.properties = properties_from_body(&self.body);
|
||||
}
|
||||
}
|
||||
|
||||
fn event_name_from_body(body: &EventBody) -> String {
|
||||
serde_json::to_value(body)
|
||||
.ok()
|
||||
.and_then(|value| {
|
||||
value
|
||||
.get("event")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_owned)
|
||||
})
|
||||
.unwrap_or_else(|| "unknown".to_string())
|
||||
}
|
||||
|
||||
fn properties_from_body(body: &EventBody) -> Value {
|
||||
serde_json::to_value(body)
|
||||
.ok()
|
||||
.and_then(|value| value.get("properties").cloned())
|
||||
.unwrap_or_else(default_properties)
|
||||
}
|
||||
|
||||
impl Serialize for StoredEvent {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
self.to_value()
|
||||
.map_err(serde::ser::Error::custom)?
|
||||
.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for StoredEvent {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = Value::deserialize(deserializer)?;
|
||||
Self::from_value(value).map_err(D::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{Edge, Graph, Node, Settings, fixtures};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn stored_event_round_trips_json() {
|
||||
let event = StoredEvent {
|
||||
id: "evt_1".to_string(),
|
||||
ts: DateTime::parse_from_rfc3339("2026-04-04T12:00:00.000Z")
|
||||
.unwrap()
|
||||
.with_timezone(&Utc),
|
||||
run_id: fixtures::RUN_1,
|
||||
event: "stage.completed".to_string(),
|
||||
node_id: Some("build".to_string()),
|
||||
node_label: Some("Build".to_string()),
|
||||
session_id: None,
|
||||
parent_session_id: None,
|
||||
properties: json!({
|
||||
"index": 1,
|
||||
"duration_ms": 1234,
|
||||
"status": "success",
|
||||
"suggested_next_ids": ["next"],
|
||||
"notes": "done",
|
||||
"files_touched": ["src/main.rs"],
|
||||
"attempt": 1,
|
||||
"max_attempts": 1
|
||||
}),
|
||||
body: EventBody::StageCompleted(StageCompletedProps {
|
||||
index: 1,
|
||||
duration_ms: 1234,
|
||||
status: crate::StageStatus::Success,
|
||||
preferred_label: None,
|
||||
suggested_next_ids: vec!["next".to_string()],
|
||||
usage: None,
|
||||
failure: None,
|
||||
notes: Some("done".to_string()),
|
||||
files_touched: vec!["src/main.rs".to_string()],
|
||||
context_updates: None,
|
||||
jump_to_node: None,
|
||||
context_values: None,
|
||||
node_visits: None,
|
||||
loop_failure_signatures: None,
|
||||
restart_failure_signatures: None,
|
||||
response: None,
|
||||
attempt: 1,
|
||||
max_attempts: 1,
|
||||
}),
|
||||
};
|
||||
|
||||
let value = event.to_value().unwrap();
|
||||
let parsed = StoredEvent::from_value(value).unwrap();
|
||||
|
||||
assert_eq!(parsed, event);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stored_event_deserializes_adjacent_layout() {
|
||||
let settings = Settings::default();
|
||||
let graph = Graph {
|
||||
name: "test".to_string(),
|
||||
nodes: HashMap::from([(
|
||||
"start".to_string(),
|
||||
Node {
|
||||
id: "start".to_string(),
|
||||
attrs: HashMap::new(),
|
||||
classes: Vec::new(),
|
||||
},
|
||||
)]),
|
||||
edges: vec![Edge {
|
||||
from: "start".to_string(),
|
||||
to: "done".to_string(),
|
||||
attrs: HashMap::new(),
|
||||
}],
|
||||
attrs: HashMap::new(),
|
||||
};
|
||||
|
||||
let line = json!({
|
||||
"id": "evt_2",
|
||||
"ts": "2026-04-04T12:00:00.000Z",
|
||||
"run_id": fixtures::RUN_1,
|
||||
"event": "run.created",
|
||||
"properties": {
|
||||
"settings": settings,
|
||||
"graph": graph,
|
||||
"labels": {},
|
||||
"run_dir": "/tmp/run",
|
||||
"working_directory": "/tmp/run"
|
||||
}
|
||||
});
|
||||
|
||||
let parsed = StoredEvent::from_value(line).unwrap();
|
||||
assert!(matches!(parsed.body, EventBody::RunCreated(_)));
|
||||
}
|
||||
}
|
||||
110
lib/crates/fabro-types/src/stored_event/run.rs
Normal file
110
lib/crates/fabro-types/src/stored_event/run.rs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Graph, RunId, Settings, StatusReason};
|
||||
|
||||
use super::{RunNoticeLevel, TokenUsage};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RunCreatedProps {
|
||||
pub settings: Settings,
|
||||
pub graph: Graph,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub workflow_source: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub workflow_config: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub labels: BTreeMap<String, String>,
|
||||
pub run_dir: String,
|
||||
pub working_directory: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub host_repo_path: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub base_branch: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub workflow_slug: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub db_prefix: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RunStartedProps {
|
||||
pub name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub base_branch: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub base_sha: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub run_branch: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub worktree_dir: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub goal: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RunStatusTransitionProps {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub reason: Option<StatusReason>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RunRewoundProps {
|
||||
pub target_checkpoint_ordinal: usize,
|
||||
pub target_node_id: String,
|
||||
pub target_visit: usize,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub previous_status: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub run_commit_sha: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RunCompletedProps {
|
||||
pub duration_ms: u64,
|
||||
pub artifact_count: usize,
|
||||
pub status: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub reason: Option<StatusReason>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub total_cost: Option<f64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub final_git_commit_sha: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub final_patch: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub usage: Option<TokenUsage>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RunFailedProps {
|
||||
pub error: String,
|
||||
pub duration_ms: u64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub reason: Option<StatusReason>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub git_commit_sha: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RunNoticeProps {
|
||||
pub level: RunNoticeLevel,
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct StoredEventHeader {
|
||||
pub id: String,
|
||||
pub ts: chrono::DateTime<chrono::Utc>,
|
||||
pub run_id: RunId,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub node_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub node_label: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub session_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub parent_session_id: Option<String>,
|
||||
}
|
||||
117
lib/crates/fabro-types/src/stored_event/stage.rs
Normal file
117
lib/crates/fabro-types/src/stored_event/stage.rs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{FailureDetail, Outcome, StageStatus, StageUsage};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct StageStartedProps {
|
||||
pub index: usize,
|
||||
pub handler_type: String,
|
||||
pub attempt: usize,
|
||||
pub max_attempts: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct StageCompletedProps {
|
||||
pub index: usize,
|
||||
pub duration_ms: u64,
|
||||
pub status: StageStatus,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub preferred_label: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub suggested_next_ids: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub usage: Option<StageUsage>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub failure: Option<FailureDetail>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub notes: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub files_touched: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub context_updates: Option<BTreeMap<String, Value>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub jump_to_node: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub context_values: Option<BTreeMap<String, Value>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub node_visits: Option<BTreeMap<String, usize>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub loop_failure_signatures: Option<BTreeMap<String, usize>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub restart_failure_signatures: Option<BTreeMap<String, usize>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub response: Option<String>,
|
||||
pub attempt: usize,
|
||||
pub max_attempts: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct StageFailedProps {
|
||||
pub index: usize,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub failure: Option<FailureDetail>,
|
||||
pub will_retry: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct StageRetryingProps {
|
||||
pub index: usize,
|
||||
pub attempt: usize,
|
||||
pub max_attempts: usize,
|
||||
pub delay_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct StagePromptProps {
|
||||
pub visit: u32,
|
||||
pub text: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub mode: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub provider: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PromptCompletedProps {
|
||||
pub response: String,
|
||||
pub model: String,
|
||||
pub provider: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub usage: Option<StageUsage>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CheckpointCompletedProps {
|
||||
pub status: String,
|
||||
pub current_node: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub completed_nodes: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub node_retries: BTreeMap<String, u32>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub context_values: BTreeMap<String, Value>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub node_outcomes: BTreeMap<String, Outcome<Option<StageUsage>>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub next_node_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub git_commit_sha: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub loop_failure_signatures: BTreeMap<String, usize>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub restart_failure_signatures: BTreeMap<String, usize>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub node_visits: BTreeMap<String, usize>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub diff: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CheckpointFailedProps {
|
||||
pub error: String,
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct StageUsage {
|
||||
pub model: String,
|
||||
pub input_tokens: i64,
|
||||
|
|
|
|||
|
|
@ -381,7 +381,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn emits_started_and_completed_events() {
|
||||
let emitter = EventEmitter::default();
|
||||
let events = Arc::new(Mutex::new(Vec::<crate::event::RunEventEnvelope>::new()));
|
||||
let events = Arc::new(Mutex::new(Vec::<fabro_types::StoredEvent>::new()));
|
||||
let events_clone = Arc::clone(&events);
|
||||
emitter.on_event(move |event| {
|
||||
events_clone.lock().unwrap().push(event.clone());
|
||||
|
|
@ -392,27 +392,33 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
let events = events.lock().unwrap();
|
||||
assert_eq!(events[0].event, "devcontainer.lifecycle.started");
|
||||
assert_eq!(events[0].properties["phase"], "on_create");
|
||||
assert_eq!(events[0].properties["command_count"], 1);
|
||||
assert_eq!(events[0].event_name(), "devcontainer.lifecycle.started");
|
||||
assert_eq!(events[0].properties()["phase"], "on_create");
|
||||
assert_eq!(events[0].properties()["command_count"], 1);
|
||||
|
||||
assert_eq!(events[1].event, "devcontainer.lifecycle.command.started");
|
||||
assert_eq!(events[1].properties["phase"], "on_create");
|
||||
assert_eq!(events[1].properties["index"], 0);
|
||||
assert_eq!(
|
||||
events[1].event_name(),
|
||||
"devcontainer.lifecycle.command.started"
|
||||
);
|
||||
assert_eq!(events[1].properties()["phase"], "on_create");
|
||||
assert_eq!(events[1].properties()["index"], 0);
|
||||
|
||||
assert_eq!(events[2].event, "devcontainer.lifecycle.command.completed");
|
||||
assert_eq!(events[2].properties["phase"], "on_create");
|
||||
assert_eq!(events[2].properties["index"], 0);
|
||||
assert_eq!(events[2].properties["exit_code"], 0);
|
||||
assert_eq!(
|
||||
events[2].event_name(),
|
||||
"devcontainer.lifecycle.command.completed"
|
||||
);
|
||||
assert_eq!(events[2].properties()["phase"], "on_create");
|
||||
assert_eq!(events[2].properties()["index"], 0);
|
||||
assert_eq!(events[2].properties()["exit_code"], 0);
|
||||
|
||||
assert_eq!(events[3].event, "devcontainer.lifecycle.completed");
|
||||
assert_eq!(events[3].properties["phase"], "on_create");
|
||||
assert_eq!(events[3].event_name(), "devcontainer.lifecycle.completed");
|
||||
assert_eq!(events[3].properties()["phase"], "on_create");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn failed_command_emits_failed_and_returns_error() {
|
||||
let emitter = EventEmitter::default();
|
||||
let events = Arc::new(Mutex::new(Vec::<crate::event::RunEventEnvelope>::new()));
|
||||
let events = Arc::new(Mutex::new(Vec::<fabro_types::StoredEvent>::new()));
|
||||
let events_clone = Arc::clone(&events);
|
||||
emitter.on_event(move |event| {
|
||||
events_clone.lock().unwrap().push(event.clone());
|
||||
|
|
@ -424,9 +430,9 @@ mod tests {
|
|||
assert!(result.is_err());
|
||||
let events = events.lock().unwrap();
|
||||
assert!(events.iter().any(|event| {
|
||||
event.event == "devcontainer.lifecycle.failed"
|
||||
&& event.properties["phase"] == "on_create"
|
||||
&& event.properties["exit_code"] == 1
|
||||
event.event_name() == "devcontainer.lifecycle.failed"
|
||||
&& event.properties()["phase"] == "on_create"
|
||||
&& event.properties()["exit_code"] == 1
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use std::sync::atomic::{AtomicI64, Ordering};
|
|||
use anyhow::{Context, Result};
|
||||
use chrono::{SecondsFormat, Utc};
|
||||
use fabro_store::{EventPayload, SlateRunStore};
|
||||
use fabro_types::RunId;
|
||||
use fabro_types::{RunId, StoredEvent};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
use std::collections::BTreeMap;
|
||||
|
|
@ -18,13 +18,7 @@ use fabro_llm::types::Usage as LlmUsage;
|
|||
use fabro_types::StatusReason;
|
||||
use fabro_util::redact::redact_jsonl_line;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RunNoticeLevel {
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
pub use fabro_types::{EventBody, RunNoticeLevel};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RunEventEnvelope {
|
||||
|
|
@ -43,6 +37,13 @@ pub struct RunEventEnvelope {
|
|||
pub properties: serde_json::Value,
|
||||
}
|
||||
|
||||
impl From<&RunEventEnvelope> for StoredEvent {
|
||||
fn from(value: &RunEventEnvelope) -> Self {
|
||||
StoredEvent::from_value(serde_json::to_value(value).expect("event envelope serializes"))
|
||||
.expect("event envelope converts to stored event")
|
||||
}
|
||||
}
|
||||
|
||||
/// Events emitted during workflow run execution for observability.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
|
|
@ -1452,21 +1453,44 @@ pub fn canonicalize_event_at(
|
|||
}
|
||||
}
|
||||
|
||||
pub fn build_redacted_event_payload(
|
||||
envelope: &RunEventEnvelope,
|
||||
pub fn to_stored_event(run_id: &RunId, event: &WorkflowRunEvent) -> StoredEvent {
|
||||
to_stored_event_at(run_id, event, Utc::now())
|
||||
}
|
||||
|
||||
pub fn to_stored_event_at(
|
||||
run_id: &RunId,
|
||||
) -> Result<EventPayload> {
|
||||
let line = redacted_event_json(envelope)?;
|
||||
event: &WorkflowRunEvent,
|
||||
ts: chrono::DateTime<Utc>,
|
||||
) -> StoredEvent {
|
||||
let envelope = canonicalize_event_at(run_id, event, ts);
|
||||
let mut stored = StoredEvent::from(&envelope);
|
||||
|
||||
match (event, &mut stored.body) {
|
||||
(WorkflowRunEvent::StageCompleted { failure, .. }, EventBody::StageCompleted(props)) => {
|
||||
props.failure = failure.clone();
|
||||
}
|
||||
(WorkflowRunEvent::StageFailed { failure, .. }, EventBody::StageFailed(props)) => {
|
||||
props.failure = Some(failure.clone());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
stored.refresh_cache();
|
||||
stored
|
||||
}
|
||||
|
||||
pub fn build_redacted_event_payload(event: &StoredEvent, run_id: &RunId) -> Result<EventPayload> {
|
||||
let line = redacted_event_json(event)?;
|
||||
event_payload_from_redacted_json(&line, run_id)
|
||||
}
|
||||
|
||||
pub fn redacted_event_json(envelope: &RunEventEnvelope) -> Result<String> {
|
||||
let line = serde_json::to_string(&normalized_envelope_value(envelope)?)?;
|
||||
pub fn redacted_event_json(event: &StoredEvent) -> Result<String> {
|
||||
let line = serde_json::to_string(&normalized_event_value(event)?)?;
|
||||
Ok(redact_jsonl_line(&line))
|
||||
}
|
||||
|
||||
fn normalized_envelope_value(envelope: &RunEventEnvelope) -> Result<Value> {
|
||||
let value = serde_json::to_value(envelope)?;
|
||||
fn normalized_event_value(event: &StoredEvent) -> Result<Value> {
|
||||
let value = event.to_value()?;
|
||||
Ok(normalize_json_value(value))
|
||||
}
|
||||
|
||||
|
|
@ -1498,8 +1522,8 @@ pub async fn append_workflow_event(
|
|||
run_id: &RunId,
|
||||
event: &WorkflowRunEvent,
|
||||
) -> Result<()> {
|
||||
let envelope = canonicalize_event(run_id, event);
|
||||
let payload = build_redacted_event_payload(&envelope, run_id)?;
|
||||
let stored = to_stored_event(run_id, event);
|
||||
let payload = build_redacted_event_payload(&stored, run_id)?;
|
||||
run_store
|
||||
.append_event(&payload)
|
||||
.await
|
||||
|
|
@ -1542,12 +1566,8 @@ impl StoreProgressLogger {
|
|||
|
||||
pub fn register(&self, emitter: &EventEmitter) {
|
||||
let tx = self.tx.clone();
|
||||
emitter.on_event(move |event| {
|
||||
let Ok(run_id) = event.run_id.parse::<RunId>() else {
|
||||
tracing::warn!(run_id = %event.run_id, "Invalid run id on event envelope");
|
||||
return;
|
||||
};
|
||||
match build_redacted_event_payload(event, &run_id) {
|
||||
emitter.on_event(
|
||||
move |event| match build_redacted_event_payload(event, &event.run_id) {
|
||||
Ok(payload) => {
|
||||
if tx.send(StoreProgressCommand::Event(payload)).is_err() {
|
||||
tracing::warn!(
|
||||
|
|
@ -1558,8 +1578,8 @@ impl StoreProgressLogger {
|
|||
Err(err) => {
|
||||
tracing::warn!(error = %err, "Failed to build store event payload");
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn flush(&self) {
|
||||
|
|
@ -1584,7 +1604,7 @@ fn epoch_millis() -> i64 {
|
|||
}
|
||||
|
||||
/// Listener callback type for workflow run events.
|
||||
type EventListener = Arc<dyn Fn(&RunEventEnvelope) + Send + Sync>;
|
||||
type EventListener = Arc<dyn Fn(&StoredEvent) + Send + Sync>;
|
||||
|
||||
/// Callback-based event emitter for workflow run events.
|
||||
pub struct EventEmitter {
|
||||
|
|
@ -1626,7 +1646,7 @@ impl EventEmitter {
|
|||
self.run_id
|
||||
}
|
||||
|
||||
pub fn on_event(&self, listener: impl Fn(&RunEventEnvelope) + Send + Sync + 'static) {
|
||||
pub fn on_event(&self, listener: impl Fn(&StoredEvent) + Send + Sync + 'static) {
|
||||
self.listeners
|
||||
.lock()
|
||||
.expect("listeners lock poisoned")
|
||||
|
|
@ -1642,11 +1662,11 @@ impl EventEmitter {
|
|||
"workflow run started event must match emitter run_id"
|
||||
);
|
||||
}
|
||||
let envelope = canonicalize_event(&self.run_id, event);
|
||||
self.dispatch_envelope(&envelope);
|
||||
let stored = to_stored_event(&self.run_id, event);
|
||||
self.dispatch_stored_event(&stored);
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_envelope(&self, envelope: &RunEventEnvelope) {
|
||||
pub(crate) fn dispatch_stored_event(&self, event: &StoredEvent) {
|
||||
self.last_event_at.store(epoch_millis(), Ordering::Relaxed);
|
||||
// Clone the listener list so we don't hold the lock during dispatch.
|
||||
// This prevents deadlocks if a listener calls emit() reentrantly.
|
||||
|
|
@ -1657,7 +1677,7 @@ impl EventEmitter {
|
|||
.expect("listeners lock poisoned")
|
||||
.clone();
|
||||
for listener in &snapshot {
|
||||
listener(envelope);
|
||||
listener(event);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1720,8 +1740,8 @@ mod tests {
|
|||
});
|
||||
let events = received.lock().unwrap();
|
||||
assert_eq!(events.len(), 1);
|
||||
assert_eq!(events[0].event, "run.started");
|
||||
assert_eq!(events[0].run_id, fixtures::RUN_1.to_string());
|
||||
assert_eq!(events[0].event_name(), "run.started");
|
||||
assert_eq!(events[0].run_id, fixtures::RUN_1);
|
||||
assert!(events[0].id.len() >= 32);
|
||||
}
|
||||
|
||||
|
|
@ -1910,7 +1930,8 @@ mod tests {
|
|||
},
|
||||
);
|
||||
|
||||
let payload = build_redacted_event_payload(&envelope, &fixtures::RUN_7).unwrap();
|
||||
let stored = StoredEvent::from(&envelope);
|
||||
let payload = build_redacted_event_payload(&stored, &fixtures::RUN_7).unwrap();
|
||||
run_store.append_event(&payload).await.unwrap();
|
||||
|
||||
let events = run_store.list_events().await.unwrap();
|
||||
|
|
@ -1935,7 +1956,8 @@ mod tests {
|
|||
},
|
||||
);
|
||||
|
||||
let payload = build_redacted_event_payload(&envelope, &fixtures::RUN_8).unwrap();
|
||||
let stored = StoredEvent::from(&envelope);
|
||||
let payload = build_redacted_event_payload(&stored, &fixtures::RUN_8).unwrap();
|
||||
assert_eq!(payload.as_value()["id"], envelope.id);
|
||||
assert_eq!(payload.as_value()["event"], "retro.started");
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -411,64 +411,125 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn scan_node_files_from_state_reconstructs_allowlisted_entries() {
|
||||
use fabro_store::EventPayload;
|
||||
use crate::event::{WorkflowRunEvent, append_workflow_event};
|
||||
|
||||
let store = test_store();
|
||||
let run = store.create_run(&fixtures::RUN_1).await.unwrap();
|
||||
let run_id_str = fixtures::RUN_1.to_string();
|
||||
|
||||
let event = |event_name: &str, props: serde_json::Value| -> EventPayload {
|
||||
let value = serde_json::json!({
|
||||
"id": format!("evt-{event_name}"),
|
||||
"ts": "2026-03-27T12:01:00Z",
|
||||
"run_id": run_id_str,
|
||||
"event": event_name,
|
||||
"node_id": "work",
|
||||
"properties": props,
|
||||
});
|
||||
EventPayload::new(value, &fixtures::RUN_1).unwrap()
|
||||
};
|
||||
|
||||
run.append_event(&event(
|
||||
"stage.prompt",
|
||||
serde_json::json!({"text": "hello", "visit": 2, "mode": "prompt", "provider": "openai"}),
|
||||
))
|
||||
append_workflow_event(
|
||||
&run,
|
||||
&fixtures::RUN_1,
|
||||
&WorkflowRunEvent::Prompt {
|
||||
stage: "work".into(),
|
||||
visit: 2,
|
||||
text: "hello".into(),
|
||||
mode: Some("prompt".into()),
|
||||
provider: Some("openai".into()),
|
||||
model: Some("gpt-5.4".into()),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
run.append_event(&event(
|
||||
"prompt.completed",
|
||||
serde_json::json!({"response": "world"}),
|
||||
))
|
||||
append_workflow_event(
|
||||
&run,
|
||||
&fixtures::RUN_1,
|
||||
&WorkflowRunEvent::PromptCompleted {
|
||||
node_id: "work".into(),
|
||||
response: "world".into(),
|
||||
model: "gpt-5.4".into(),
|
||||
provider: "openai".into(),
|
||||
usage: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
run.append_event(&event(
|
||||
"stage.completed",
|
||||
serde_json::json!({"response": "world", "status": "success", "visit": 2}),
|
||||
))
|
||||
append_workflow_event(
|
||||
&run,
|
||||
&fixtures::RUN_1,
|
||||
&WorkflowRunEvent::StageCompleted {
|
||||
node_id: "work".into(),
|
||||
name: "Work".into(),
|
||||
index: 2,
|
||||
duration_ms: 100,
|
||||
status: "success".into(),
|
||||
preferred_label: None,
|
||||
suggested_next_ids: Vec::new(),
|
||||
usage: None,
|
||||
failure: None,
|
||||
notes: None,
|
||||
files_touched: Vec::new(),
|
||||
context_updates: None,
|
||||
jump_to_node: None,
|
||||
context_values: None,
|
||||
node_visits: Some(std::collections::BTreeMap::from([("work".into(), 2)])),
|
||||
loop_failure_signatures: None,
|
||||
restart_failure_signatures: None,
|
||||
response: Some("world".into()),
|
||||
attempt: 1,
|
||||
max_attempts: 1,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
run.append_event(&event(
|
||||
"command.started",
|
||||
serde_json::json!({"command": "echo hi"}),
|
||||
))
|
||||
append_workflow_event(
|
||||
&run,
|
||||
&fixtures::RUN_1,
|
||||
&WorkflowRunEvent::CommandStarted {
|
||||
node_id: "work".into(),
|
||||
script: "echo hi".into(),
|
||||
command: "echo hi".into(),
|
||||
language: "shell".into(),
|
||||
timeout_ms: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
run.append_event(&event(
|
||||
"command.completed",
|
||||
serde_json::json!({"stdout": "hi\n", "stderr": "", "exit_code": 0}),
|
||||
))
|
||||
append_workflow_event(
|
||||
&run,
|
||||
&fixtures::RUN_1,
|
||||
&WorkflowRunEvent::CommandCompleted {
|
||||
node_id: "work".into(),
|
||||
stdout: "hi\n".into(),
|
||||
stderr: String::new(),
|
||||
exit_code: Some(0),
|
||||
duration_ms: 10,
|
||||
timed_out: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
run.append_event(&event(
|
||||
"parallel.completed",
|
||||
serde_json::json!({"results": [{"id": "a"}], "duration_ms": 100, "success_count": 1, "failure_count": 0}),
|
||||
))
|
||||
append_workflow_event(
|
||||
&run,
|
||||
&fixtures::RUN_1,
|
||||
&WorkflowRunEvent::ParallelCompleted {
|
||||
node_id: "work".into(),
|
||||
visit: 2,
|
||||
duration_ms: 100,
|
||||
success_count: 1,
|
||||
failure_count: 0,
|
||||
results: vec![serde_json::json!({"id": "a"})],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
run.append_event(&event(
|
||||
"checkpoint.completed",
|
||||
serde_json::json!({"diff": "diff --git a/story.txt b/story.txt", "ordinal": 1, "current_node": "work", "node_visits": {"work": 2}}),
|
||||
))
|
||||
append_workflow_event(
|
||||
&run,
|
||||
&fixtures::RUN_1,
|
||||
&WorkflowRunEvent::CheckpointCompleted {
|
||||
node_id: "work".into(),
|
||||
status: "success".into(),
|
||||
current_node: "work".into(),
|
||||
completed_nodes: Vec::new(),
|
||||
node_retries: std::collections::BTreeMap::new(),
|
||||
context_values: std::collections::BTreeMap::new(),
|
||||
node_outcomes: std::collections::BTreeMap::new(),
|
||||
next_node_id: None,
|
||||
git_commit_sha: None,
|
||||
loop_failure_signatures: std::collections::BTreeMap::new(),
|
||||
restart_failure_signatures: std::collections::BTreeMap::new(),
|
||||
node_visits: std::collections::BTreeMap::from([("work".into(), 2)]),
|
||||
diff: Some("diff --git a/story.txt b/story.txt".into()),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ use fabro_types::{RunId, Settings};
|
|||
use crate::context::Context;
|
||||
use crate::error::FabroError;
|
||||
use crate::event::{
|
||||
EventEmitter, RunNoticeLevel, StoreProgressLogger, WorkflowRunEvent, append_workflow_event,
|
||||
canonicalize_event, event_payload_from_redacted_json, redacted_event_json,
|
||||
EventBody, EventEmitter, RunNoticeLevel, StoreProgressLogger, WorkflowRunEvent,
|
||||
append_workflow_event, event_payload_from_redacted_json, redacted_event_json, to_stored_event,
|
||||
};
|
||||
use crate::git::MetadataStore;
|
||||
use crate::handler::HandlerRegistry;
|
||||
|
|
@ -453,31 +453,23 @@ impl RunSession {
|
|||
{
|
||||
let sha_clone = Arc::clone(&last_git_sha);
|
||||
self.emitter.on_event(move |event| match event {
|
||||
envelope if envelope.event == "checkpoint.completed" => {
|
||||
if let Some(sha) = envelope
|
||||
.properties
|
||||
.get("git_commit_sha")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
{
|
||||
*sha_clone.lock().unwrap() = Some(sha.to_string());
|
||||
event if matches!(&event.body, EventBody::CheckpointCompleted(_)) => {
|
||||
if let EventBody::CheckpointCompleted(props) = &event.body {
|
||||
if let Some(sha) = props.git_commit_sha.as_ref() {
|
||||
*sha_clone.lock().unwrap() = Some(sha.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
envelope if envelope.event == "run.completed" => {
|
||||
if let Some(sha) = envelope
|
||||
.properties
|
||||
.get("final_git_commit_sha")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
{
|
||||
*sha_clone.lock().unwrap() = Some(sha.to_string());
|
||||
event if matches!(&event.body, EventBody::RunCompleted(_)) => {
|
||||
if let EventBody::RunCompleted(props) = &event.body {
|
||||
if let Some(sha) = props.final_git_commit_sha.as_ref() {
|
||||
*sha_clone.lock().unwrap() = Some(sha.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
envelope if envelope.event == "git.commit" => {
|
||||
if let Some(sha) = envelope
|
||||
.properties
|
||||
.get("sha")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
{
|
||||
*sha_clone.lock().unwrap() = Some(sha.to_string());
|
||||
event if matches!(&event.body, EventBody::GitCommit(_)) => {
|
||||
if let EventBody::GitCommit(props) = &event.body {
|
||||
*sha_clone.lock().unwrap() = Some(props.sha.to_string());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -695,7 +687,7 @@ impl Drop for DetachedRunCompletionGuard {
|
|||
};
|
||||
|
||||
let serialized_notice = {
|
||||
let envelope = canonicalize_event(
|
||||
let stored = to_stored_event(
|
||||
&self.run_id,
|
||||
&WorkflowRunEvent::RunNotice {
|
||||
level: RunNoticeLevel::Error,
|
||||
|
|
@ -703,7 +695,7 @@ impl Drop for DetachedRunCompletionGuard {
|
|||
message: message.to_string(),
|
||||
},
|
||||
);
|
||||
let line = match redacted_event_json(&envelope) {
|
||||
let line = match redacted_event_json(&stored) {
|
||||
Ok(line) => line,
|
||||
Err(err) => {
|
||||
tracing::warn!(error = %err, "Failed to serialize post-run abort event");
|
||||
|
|
@ -732,7 +724,7 @@ impl Drop for DetachedRunCompletionGuard {
|
|||
)
|
||||
.await;
|
||||
if let Some((run_id, line)) = serialized_notice.or_else(|| {
|
||||
let envelope = canonicalize_event(
|
||||
let stored = to_stored_event(
|
||||
&run_id,
|
||||
&WorkflowRunEvent::RunNotice {
|
||||
level: RunNoticeLevel::Error,
|
||||
|
|
@ -740,9 +732,7 @@ impl Drop for DetachedRunCompletionGuard {
|
|||
message: message.to_string(),
|
||||
},
|
||||
);
|
||||
redacted_event_json(&envelope)
|
||||
.ok()
|
||||
.map(|line| (run_id, line))
|
||||
redacted_event_json(&stored).ok().map(|line| (run_id, line))
|
||||
}) {
|
||||
match event_payload_from_redacted_json(&line, &run_id) {
|
||||
Ok(payload) => {
|
||||
|
|
@ -791,8 +781,8 @@ async fn persist_detached_failure(
|
|||
code: format!("{phase}_failed"),
|
||||
message: message.clone(),
|
||||
};
|
||||
let envelope = canonicalize_event(&run_id, &event);
|
||||
let line = redacted_event_json(&envelope).map_err(|err| FabroError::Io(err.to_string()))?;
|
||||
let stored = to_stored_event(&run_id, &event);
|
||||
let line = redacted_event_json(&stored).map_err(|err| FabroError::Io(err.to_string()))?;
|
||||
match event_payload_from_redacted_json(&line, &run_id) {
|
||||
Ok(payload) => {
|
||||
if let Err(err) = run_store.append_event(&payload).await {
|
||||
|
|
@ -911,7 +901,9 @@ mod tests {
|
|||
if injected.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
if event.event == "stage.started" && event.node_id.as_deref() == Some("start") {
|
||||
if matches!(&event.body, EventBody::StageStarted(_))
|
||||
&& event.node_id.as_deref() == Some("start")
|
||||
{
|
||||
injected.store(true, Ordering::SeqCst);
|
||||
emitter_for_injection.emit(&WorkflowRunEvent::CheckpointCompleted {
|
||||
node_id: "start".to_string(),
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ use object_store::memory::InMemory;
|
|||
use super::*;
|
||||
use crate::context::{self, Context};
|
||||
use crate::error::FabroError;
|
||||
use crate::event::{EventEmitter, RunEventEnvelope, StoreProgressLogger};
|
||||
use crate::event::{EventEmitter, StoreProgressLogger};
|
||||
use crate::handler::start::StartHandler;
|
||||
use crate::handler::{Handler as HandlerTrait, HandlerRegistry};
|
||||
use crate::outcome::{Outcome, OutcomeExt, StageStatus};
|
||||
|
|
@ -894,7 +894,7 @@ async fn retry_emits_stage_started_per_attempt() {
|
|||
g.edges.push(Edge::new("start", "work"));
|
||||
g.edges.push(Edge::new("work", "exit"));
|
||||
|
||||
let events = Arc::new(std::sync::Mutex::new(Vec::<RunEventEnvelope>::new()));
|
||||
let events = Arc::new(std::sync::Mutex::new(Vec::<fabro_types::StoredEvent>::new()));
|
||||
let events_clone = Arc::clone(&events);
|
||||
let emitter = test_emitter("retry-events-test");
|
||||
emitter.on_event(move |event| {
|
||||
|
|
@ -923,8 +923,10 @@ async fn retry_emits_stage_started_per_attempt() {
|
|||
let collected = events.lock().unwrap();
|
||||
let work_started: Vec<_> = collected
|
||||
.iter()
|
||||
.filter(|event| event.event == "stage.started" && event.node_id.as_deref() == Some("work"))
|
||||
.map(|event| event.properties["attempt"].as_u64().unwrap())
|
||||
.filter(|event| {
|
||||
event.event_name() == "stage.started" && event.node_id.as_deref() == Some("work")
|
||||
})
|
||||
.map(|event| event.properties()["attempt"].as_u64().unwrap())
|
||||
.collect();
|
||||
assert_eq!(work_started, vec![1, 2]);
|
||||
}
|
||||
|
|
@ -936,7 +938,7 @@ async fn run_with_lifecycle_emits_initialize_and_setup_events() {
|
|||
let events_clone = Arc::clone(&events);
|
||||
let emitter = test_emitter("order-test");
|
||||
emitter.on_event(move |event| {
|
||||
let name = match event.event.as_str() {
|
||||
let name = match event.event_name().as_str() {
|
||||
"sandbox.initialized" => "SandboxInitialized",
|
||||
"setup.started" => "SetupStarted",
|
||||
"setup.completed" => "SetupCompleted",
|
||||
|
|
@ -1017,7 +1019,7 @@ async fn git_checkpoint_skips_start_node() {
|
|||
g.edges.push(Edge::new("start", "work"));
|
||||
g.edges.push(Edge::new("work", "exit"));
|
||||
|
||||
let events = Arc::new(std::sync::Mutex::new(Vec::<RunEventEnvelope>::new()));
|
||||
let events = Arc::new(std::sync::Mutex::new(Vec::<fabro_types::StoredEvent>::new()));
|
||||
let events_clone = Arc::clone(&events);
|
||||
let emitter = test_emitter("git-cp-test");
|
||||
emitter.on_event(move |event| {
|
||||
|
|
@ -1049,9 +1051,9 @@ async fn git_checkpoint_skips_start_node() {
|
|||
let checkpoint_node_ids: Vec<&str> = collected
|
||||
.iter()
|
||||
.filter(|event| {
|
||||
event.event == "checkpoint.completed"
|
||||
event.event_name() == "checkpoint.completed"
|
||||
&& event
|
||||
.properties
|
||||
.properties()
|
||||
.get("git_commit_sha")
|
||||
.and_then(|value| value.as_str())
|
||||
.is_some()
|
||||
|
|
|
|||
|
|
@ -835,7 +835,7 @@ mod tests {
|
|||
let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
emitter.on_event({
|
||||
let seen = Arc::clone(&seen);
|
||||
move |event| seen.lock().unwrap().push(event.event.clone())
|
||||
move |event| seen.lock().unwrap().push(event.event_name())
|
||||
});
|
||||
store_logger.register(&emitter);
|
||||
|
||||
|
|
|
|||
|
|
@ -390,22 +390,25 @@ mod tests {
|
|||
let seen = seen.lock().unwrap();
|
||||
let retro_started = seen
|
||||
.iter()
|
||||
.find(|event| event.event == "retro.started")
|
||||
.find(|event| event.event_name() == "retro.started")
|
||||
.unwrap();
|
||||
assert_eq!(retro_started.properties["provider"], "anthropic");
|
||||
assert_eq!(retro_started.properties["model"], "test-model");
|
||||
assert_eq!(retro_started.properties()["provider"], "anthropic");
|
||||
assert_eq!(retro_started.properties()["model"], "test-model");
|
||||
assert!(
|
||||
retro_started.properties["prompt"]
|
||||
retro_started.properties()["prompt"]
|
||||
.as_str()
|
||||
.is_some_and(|prompt| prompt.contains("/tmp/retro_data/progress.jsonl"))
|
||||
);
|
||||
|
||||
let retro_completed = seen
|
||||
.iter()
|
||||
.find(|event| event.event == "retro.completed")
|
||||
.find(|event| event.event_name() == "retro.completed")
|
||||
.unwrap();
|
||||
assert_eq!(retro_completed.properties["response"], "");
|
||||
assert!(retro_completed.properties.get("retro").is_some());
|
||||
assert_eq!(retro_completed.properties["retro"]["smoothness"], "smooth");
|
||||
assert_eq!(retro_completed.properties()["response"], "");
|
||||
assert!(retro_completed.properties().get("retro").is_some());
|
||||
assert_eq!(
|
||||
retro_completed.properties()["retro"]["smoothness"],
|
||||
"smooth"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ struct InitializedState {
|
|||
fn bound_emitter(run_id: fabro_types::RunId, observer: &Arc<EventEmitter>) -> Arc<EventEmitter> {
|
||||
let emitter = Arc::new(EventEmitter::new(run_id));
|
||||
let observer_clone = Arc::clone(observer);
|
||||
emitter.on_event(move |event| observer_clone.dispatch_envelope(event));
|
||||
emitter.on_event(move |event| observer_clone.dispatch_stored_event(event));
|
||||
emitter
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,11 +25,11 @@ use fabro_interview::{
|
|||
};
|
||||
use fabro_llm::provider::Provider;
|
||||
use fabro_store::{RuntimeState, SlateStore};
|
||||
use fabro_types::{RunId, Settings};
|
||||
use fabro_types::{RunId, Settings, StoredEvent};
|
||||
use fabro_validate::{Severity, validate, validate_or_raise};
|
||||
use fabro_workflow::context::Context;
|
||||
use fabro_workflow::error::{FabroError, FailureSignatureExt};
|
||||
use fabro_workflow::event::{EventEmitter, RunEventEnvelope, WorkflowRunEvent};
|
||||
use fabro_workflow::event::{EventEmitter, WorkflowRunEvent};
|
||||
use fabro_workflow::handler::agent::{AgentHandler, CodergenBackend, CodergenResult};
|
||||
use fabro_workflow::handler::command::CommandHandler;
|
||||
use fabro_workflow::handler::conditional::ConditionalHandler;
|
||||
|
|
@ -1545,7 +1545,7 @@ impl Handler for ContextSetterHandler {
|
|||
}
|
||||
}
|
||||
|
||||
fn collect_events(emitter: &EventEmitter) -> Arc<std::sync::Mutex<Vec<RunEventEnvelope>>> {
|
||||
fn collect_events(emitter: &EventEmitter) -> Arc<std::sync::Mutex<Vec<StoredEvent>>> {
|
||||
let events = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
let events_clone = Arc::clone(&events);
|
||||
emitter.on_event(move |event| {
|
||||
|
|
@ -7316,10 +7316,7 @@ impl HookTestRunner {
|
|||
}
|
||||
}
|
||||
|
||||
fn emitter_with_events() -> (
|
||||
Arc<EventEmitter>,
|
||||
Arc<std::sync::Mutex<Vec<RunEventEnvelope>>>,
|
||||
) {
|
||||
fn emitter_with_events() -> (Arc<EventEmitter>, Arc<std::sync::Mutex<Vec<StoredEvent>>>) {
|
||||
let emitter = EventEmitter::default();
|
||||
let events = collect_events(&emitter);
|
||||
(Arc::new(emitter), events)
|
||||
|
|
@ -7334,7 +7331,7 @@ fn engine_with_hooks(hooks: Vec<fabro_hooks::HookDefinition>) -> HookTestRunner
|
|||
|
||||
fn engine_with_hooks_and_events(
|
||||
hooks: Vec<fabro_hooks::HookDefinition>,
|
||||
) -> (HookTestRunner, Arc<std::sync::Mutex<Vec<RunEventEnvelope>>>) {
|
||||
) -> (HookTestRunner, Arc<std::sync::Mutex<Vec<StoredEvent>>>) {
|
||||
let (emitter, events) = emitter_with_events();
|
||||
(
|
||||
HookTestRunner {
|
||||
|
|
@ -12446,7 +12443,7 @@ async fn asset_collection_local_sandbox_success() {
|
|||
|
||||
// Check that AssetCaptured events were emitted
|
||||
let captured_events = events.lock().unwrap();
|
||||
let asset_events: Vec<&RunEventEnvelope> = captured_events
|
||||
let asset_events: Vec<&StoredEvent> = captured_events
|
||||
.iter()
|
||||
.filter(|e| e.event == "asset.captured")
|
||||
.collect();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue