mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-11 22:53:00 +00:00
feat(api): flatten EventEnvelope wire JSON (schema v2)
Wire EventEnvelope now inlines the RunEvent payload fields alongside
seq at the top level of the JSON object. The internal Rust
EventEnvelope { seq, payload } stays structurally unchanged; only the
API/SSE serialization layer flattens for clients.
- OpenAPI spec: add stage_id, parallel_group_id, parallel_branch_id,
tool_call_id, actor to RunEvent; model EventEnvelope as allOf(seq,
RunEvent); introduce ActorRef/ActorKind schemas.
- fabro-server: rewrite api_event_envelope_from_store to merge seq
into the payload JSON value before returning the generated flat
type; remove the now-unused nested ApiRunEvent conversion helper.
- fabro-cli server_client: add wire_event_envelope_into_store helper
that turns flat wire JSON back into fabro_store::EventEnvelope
{ seq, payload } for internal consumers.
- Regenerate progenitor Rust types and typescript-axios client.
- Update demo stubs, SSE tests, CLI test helpers, and insta
snapshots to expect the flattened shape and the new stage_id field.
Incidental: the typescript regeneration also picked up prior-merged
spec fields (ApiQuestion stage/timeout/context, upload manifest
batches, web-settings) that were stale in the TS client.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b6eb462a05
commit
51dc4350a5
22 changed files with 346 additions and 114 deletions
|
|
@ -2620,6 +2620,32 @@ components:
|
|||
items:
|
||||
$ref: "#/components/schemas/ErrorResponseEntry"
|
||||
|
||||
ActorKind:
|
||||
description: High-level category of an event actor.
|
||||
type: string
|
||||
enum:
|
||||
- user
|
||||
- agent
|
||||
- system
|
||||
|
||||
ActorRef:
|
||||
description: >
|
||||
Optional primary actor associated with a run event. Present on control
|
||||
actions and durable agent output where a stable user or agent identity
|
||||
matters; omitted on routine runtime lifecycle events.
|
||||
type: object
|
||||
required:
|
||||
- kind
|
||||
properties:
|
||||
kind:
|
||||
$ref: "#/components/schemas/ActorKind"
|
||||
id:
|
||||
type: string
|
||||
description: Stable actor identifier when available.
|
||||
display:
|
||||
type: string
|
||||
description: Display-friendly label for the actor.
|
||||
|
||||
RunEvent:
|
||||
description: >
|
||||
Internal RunEvent-compatible JSON payload. The server validates this
|
||||
|
|
@ -2644,12 +2670,39 @@ components:
|
|||
node_label:
|
||||
type: string
|
||||
nullable: true
|
||||
stage_id:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Stage execution identity, formatted as "{node_id}@{visit}".
|
||||
parallel_group_id:
|
||||
type: string
|
||||
nullable: true
|
||||
description: >
|
||||
Durable identity of one execution of a parallel node, formatted as
|
||||
"{node_id}@{visit}".
|
||||
parallel_branch_id:
|
||||
type: string
|
||||
nullable: true
|
||||
description: >
|
||||
Durable identity of one branch within a parallel execution,
|
||||
formatted as "{parallel_group_id}:{index}".
|
||||
session_id:
|
||||
type: string
|
||||
nullable: true
|
||||
parent_session_id:
|
||||
type: string
|
||||
nullable: true
|
||||
tool_call_id:
|
||||
type: string
|
||||
nullable: true
|
||||
description: >
|
||||
Stable identifier for a tool call, present on agent.tool.* events
|
||||
and other durable events that directly describe the same tool
|
||||
call.
|
||||
actor:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/ActorRef"
|
||||
nullable: true
|
||||
event:
|
||||
type: string
|
||||
description: Event type discriminator.
|
||||
|
|
@ -2660,18 +2713,20 @@ components:
|
|||
additionalProperties: true
|
||||
|
||||
EventEnvelope:
|
||||
description: Stored event envelope with assigned sequence number.
|
||||
type: object
|
||||
required:
|
||||
- seq
|
||||
- payload
|
||||
properties:
|
||||
seq:
|
||||
type: integer
|
||||
description: Assigned event sequence number.
|
||||
example: 42
|
||||
payload:
|
||||
$ref: "#/components/schemas/RunEvent"
|
||||
description: >
|
||||
Stored event envelope with assigned sequence number. On the wire the
|
||||
envelope is flattened: seq sits alongside the RunEvent payload fields
|
||||
at the top level of the JSON object.
|
||||
allOf:
|
||||
- type: object
|
||||
required:
|
||||
- seq
|
||||
properties:
|
||||
seq:
|
||||
type: integer
|
||||
description: Assigned event sequence number.
|
||||
example: 42
|
||||
- $ref: "#/components/schemas/RunEvent"
|
||||
|
||||
PaginatedEventList:
|
||||
description: Paginated list of stored run events.
|
||||
|
|
|
|||
|
|
@ -72,13 +72,47 @@ impl RunAttachEventStream {
|
|||
|
||||
fn buffer_sse_events(&mut self, finalize: bool) -> Result<()> {
|
||||
for payload in sse::drain_sse_payloads(&mut self.pending_bytes, finalize) {
|
||||
let event: types::EventEnvelope = serde_json::from_str(&payload)?;
|
||||
self.buffered_events.push_back(convert_type(event)?);
|
||||
let value: serde_json::Value = serde_json::from_str(&payload)?;
|
||||
self.buffered_events
|
||||
.push_back(wire_event_envelope_into_store(value)?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a flattened wire `EventEnvelope` JSON value (seq alongside the
|
||||
/// RunEvent payload fields at the top level) into the internal
|
||||
/// `fabro_store::EventEnvelope` which keeps seq and payload separate.
|
||||
fn wire_event_envelope_into_store(value: serde_json::Value) -> Result<EventEnvelope> {
|
||||
let serde_json::Value::Object(mut obj) = value else {
|
||||
bail!("expected wire EventEnvelope JSON object");
|
||||
};
|
||||
let seq_value = obj
|
||||
.remove("seq")
|
||||
.context("wire EventEnvelope missing seq field")?;
|
||||
let seq: u32 = match seq_value {
|
||||
serde_json::Value::Number(n) => n
|
||||
.as_u64()
|
||||
.and_then(|v| u32::try_from(v).ok())
|
||||
.context("wire EventEnvelope seq is out of u32 range")?,
|
||||
_ => bail!("wire EventEnvelope seq is not a number"),
|
||||
};
|
||||
let run_id_str = obj
|
||||
.get("run_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.context("wire EventEnvelope missing run_id")?;
|
||||
let run_id: RunId = run_id_str.parse().context("invalid run_id in wire event")?;
|
||||
let payload = fabro_store::EventPayload::new(serde_json::Value::Object(obj), &run_id)
|
||||
.map_err(|err| anyhow!("wire EventEnvelope payload failed store validation: {err}"))?;
|
||||
Ok(EventEnvelope { seq, payload })
|
||||
}
|
||||
|
||||
fn wire_event_envelope_from_generated(value: types::EventEnvelope) -> Result<EventEnvelope> {
|
||||
let value =
|
||||
serde_json::to_value(value).context("failed to serialize generated EventEnvelope")?;
|
||||
wire_event_envelope_into_store(value)
|
||||
}
|
||||
|
||||
pub(crate) use fabro_store::RunProjection;
|
||||
|
||||
pub(crate) async fn connect_server(storage_dir: &Path) -> Result<ServerStoreClient> {
|
||||
|
|
@ -407,7 +441,7 @@ impl ServerStoreClient {
|
|||
let page_events = parsed
|
||||
.data
|
||||
.into_iter()
|
||||
.map(convert_type)
|
||||
.map(wire_event_envelope_from_generated)
|
||||
.collect::<Result<Vec<EventEnvelope>>>()?;
|
||||
let next_page_since_seq = page_events.last().map(|event| event.seq.saturating_add(1));
|
||||
all_events.extend(page_events);
|
||||
|
|
|
|||
|
|
@ -647,6 +647,7 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"max_attempts": 1
|
||||
},
|
||||
"run_id": "[ULID]",
|
||||
"stage_id": "start@1",
|
||||
"ts": "[TIMESTAMP]"
|
||||
},
|
||||
{
|
||||
|
|
@ -674,6 +675,7 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"status": "success"
|
||||
},
|
||||
"run_id": "[ULID]",
|
||||
"stage_id": "start@1",
|
||||
"ts": "[TIMESTAMP]"
|
||||
},
|
||||
{
|
||||
|
|
@ -738,6 +740,7 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"max_attempts": 1
|
||||
},
|
||||
"run_id": "[ULID]",
|
||||
"stage_id": "approve@1",
|
||||
"ts": "[TIMESTAMP]"
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -74,16 +74,14 @@ fn remote_run_state_response() -> serde_json::Value {
|
|||
fn run_completed_event(run_id: &str) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"seq": 1,
|
||||
"payload": {
|
||||
"event": "run.completed",
|
||||
"id": "evt-run-completed",
|
||||
"run_id": run_id,
|
||||
"ts": "2026-04-05T12:00:01Z",
|
||||
"properties": {
|
||||
"duration_ms": 12,
|
||||
"artifact_count": 0,
|
||||
"status": "success"
|
||||
}
|
||||
"event": "run.completed",
|
||||
"id": "evt-run-completed",
|
||||
"run_id": run_id,
|
||||
"ts": "2026-04-05T12:00:01Z",
|
||||
"properties": {
|
||||
"duration_ms": 12,
|
||||
"artifact_count": 0,
|
||||
"status": "success"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -91,13 +89,11 @@ fn run_completed_event(run_id: &str) -> serde_json::Value {
|
|||
fn run_running_event(run_id: &str, seq: u32) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"seq": seq,
|
||||
"payload": {
|
||||
"event": "run.running",
|
||||
"id": format!("evt-run-running-{seq}"),
|
||||
"run_id": run_id,
|
||||
"ts": "2026-04-05T12:00:00Z",
|
||||
"properties": {}
|
||||
}
|
||||
"event": "run.running",
|
||||
"id": format!("evt-run-running-{seq}"),
|
||||
"run_id": run_id,
|
||||
"ts": "2026-04-05T12:00:00Z",
|
||||
"properties": {}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -956,6 +952,7 @@ fn json_run_implies_auto_approve_for_human_gates() {
|
|||
"max_attempts": 1
|
||||
},
|
||||
"run_id": "[ULID]",
|
||||
"stage_id": "start@1",
|
||||
"ts": "[TIMESTAMP]"
|
||||
},
|
||||
{
|
||||
|
|
@ -983,6 +980,7 @@ fn json_run_implies_auto_approve_for_human_gates() {
|
|||
"status": "success"
|
||||
},
|
||||
"run_id": "[ULID]",
|
||||
"stage_id": "start@1",
|
||||
"ts": "[TIMESTAMP]"
|
||||
},
|
||||
{
|
||||
|
|
@ -1047,6 +1045,7 @@ fn json_run_implies_auto_approve_for_human_gates() {
|
|||
"max_attempts": 1
|
||||
},
|
||||
"run_id": "[ULID]",
|
||||
"stage_id": "approve@1",
|
||||
"ts": "[TIMESTAMP]"
|
||||
},
|
||||
{
|
||||
|
|
@ -1125,6 +1124,7 @@ fn json_run_implies_auto_approve_for_human_gates() {
|
|||
]
|
||||
},
|
||||
"run_id": "[ULID]",
|
||||
"stage_id": "approve@1",
|
||||
"ts": "[TIMESTAMP]"
|
||||
},
|
||||
{
|
||||
|
|
@ -1213,6 +1213,7 @@ fn json_run_implies_auto_approve_for_human_gates() {
|
|||
"max_attempts": 1
|
||||
},
|
||||
"run_id": "[ULID]",
|
||||
"stage_id": "ship@1",
|
||||
"ts": "[TIMESTAMP]"
|
||||
},
|
||||
{
|
||||
|
|
@ -1285,6 +1286,7 @@ fn json_run_implies_auto_approve_for_human_gates() {
|
|||
"status": "success"
|
||||
},
|
||||
"run_id": "[ULID]",
|
||||
"stage_id": "ship@1",
|
||||
"ts": "[TIMESTAMP]"
|
||||
},
|
||||
{
|
||||
|
|
@ -1383,6 +1385,7 @@ fn json_run_implies_auto_approve_for_human_gates() {
|
|||
"max_attempts": 1
|
||||
},
|
||||
"run_id": "[ULID]",
|
||||
"stage_id": "exit@1",
|
||||
"ts": "[TIMESTAMP]"
|
||||
},
|
||||
{
|
||||
|
|
@ -1398,6 +1401,7 @@ fn json_run_implies_auto_approve_for_human_gates() {
|
|||
"status": "success"
|
||||
},
|
||||
"run_id": "[ULID]",
|
||||
"stage_id": "exit@1",
|
||||
"ts": "[TIMESTAMP]"
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -661,7 +661,37 @@ pub(crate) fn run_events(run_dir: &Path) -> Vec<EventEnvelope> {
|
|||
run_dir,
|
||||
&format!("/api/v1/runs/{run_id}/events"),
|
||||
));
|
||||
serde_json::from_value(response["data"].clone()).expect("event list should parse")
|
||||
let items = response["data"]
|
||||
.as_array()
|
||||
.cloned()
|
||||
.expect("event list response should contain a data array");
|
||||
items
|
||||
.into_iter()
|
||||
.map(wire_event_envelope_value_into_store)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.expect("wire event envelope list should parse")
|
||||
}
|
||||
|
||||
fn wire_event_envelope_value_into_store(value: serde_json::Value) -> Result<EventEnvelope, String> {
|
||||
let mut obj = match value {
|
||||
serde_json::Value::Object(obj) => obj,
|
||||
_ => return Err("wire envelope is not an object".to_string()),
|
||||
};
|
||||
let seq = obj
|
||||
.remove("seq")
|
||||
.and_then(|v| v.as_u64())
|
||||
.and_then(|v| u32::try_from(v).ok())
|
||||
.ok_or_else(|| "wire envelope missing valid seq".to_string())?;
|
||||
let run_id_str = obj
|
||||
.get("run_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| "wire envelope missing run_id".to_string())?;
|
||||
let run_id: RunId = run_id_str
|
||||
.parse()
|
||||
.map_err(|err| format!("invalid run_id in wire envelope: {err}"))?;
|
||||
let payload = fabro_store::EventPayload::new(serde_json::Value::Object(obj), &run_id)
|
||||
.map_err(|err| format!("wire envelope payload failed store validation: {err}"))?;
|
||||
Ok(EventEnvelope { seq, payload })
|
||||
}
|
||||
|
||||
pub(crate) fn wait_for_event_names(run_dir: &Path, expected: &[&str]) {
|
||||
|
|
|
|||
|
|
@ -29,16 +29,14 @@ fn live_run_state_response() -> serde_json::Value {
|
|||
fn run_sse_body(run_id: &str) -> String {
|
||||
let completed = serde_json::json!({
|
||||
"seq": 2,
|
||||
"payload": {
|
||||
"event": "run.completed",
|
||||
"id": "evt-run-completed",
|
||||
"run_id": run_id,
|
||||
"ts": "2026-04-05T12:00:01Z",
|
||||
"properties": {
|
||||
"duration_ms": 12,
|
||||
"artifact_count": 0,
|
||||
"status": "success"
|
||||
}
|
||||
"event": "run.completed",
|
||||
"id": "evt-run-completed",
|
||||
"run_id": run_id,
|
||||
"ts": "2026-04-05T12:00:01Z",
|
||||
"properties": {
|
||||
"duration_ms": 12,
|
||||
"artifact_count": 0,
|
||||
"status": "success"
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -267,13 +265,11 @@ fn attach_smoke_covers_arg_validation_and_remote_server_behaviors() {
|
|||
serde_json::json!({
|
||||
"data": [{
|
||||
"seq": 1,
|
||||
"payload": {
|
||||
"event": "run.running",
|
||||
"id": "evt-run-running",
|
||||
"run_id": success_run_id,
|
||||
"ts": "2026-04-05T12:00:00Z",
|
||||
"properties": {}
|
||||
}
|
||||
"event": "run.running",
|
||||
"id": "evt-run-running",
|
||||
"run_id": success_run_id,
|
||||
"ts": "2026-04-05T12:00:00Z",
|
||||
"properties": {}
|
||||
}],
|
||||
"meta": { "has_more": false }
|
||||
})
|
||||
|
|
@ -367,13 +363,11 @@ fn attach_smoke_covers_arg_validation_and_remote_server_behaviors() {
|
|||
serde_json::json!({
|
||||
"data": [{
|
||||
"seq": 1,
|
||||
"payload": {
|
||||
"event": "run.running",
|
||||
"id": "evt-run-running",
|
||||
"run_id": eof_run_id,
|
||||
"ts": "2026-04-05T12:00:00Z",
|
||||
"properties": {}
|
||||
}
|
||||
"event": "run.running",
|
||||
"id": "evt-run-running",
|
||||
"run_id": eof_run_id,
|
||||
"ts": "2026-04-05T12:00:00Z",
|
||||
"properties": {}
|
||||
}],
|
||||
"meta": { "has_more": false }
|
||||
})
|
||||
|
|
|
|||
|
|
@ -240,16 +240,14 @@ pub(crate) async fn run_events_stub(
|
|||
Event::default().data(
|
||||
json!({
|
||||
"seq": 2,
|
||||
"payload": {
|
||||
"id": "evt_demo_attach_completed",
|
||||
"ts": "2026-04-06T15:00:02Z",
|
||||
"run_id": "01JQ0000000000000000000001",
|
||||
"event": "run.completed",
|
||||
"properties": {
|
||||
"duration_ms": 42,
|
||||
"artifact_count": 0,
|
||||
"status": "success"
|
||||
}
|
||||
"id": "evt_demo_attach_completed",
|
||||
"ts": "2026-04-06T15:00:02Z",
|
||||
"run_id": "01JQ0000000000000000000001",
|
||||
"event": "run.completed",
|
||||
"properties": {
|
||||
"duration_ms": 42,
|
||||
"artifact_count": 0,
|
||||
"status": "success"
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
|
|
@ -507,12 +505,10 @@ pub(crate) async fn attach_events_stub(
|
|||
Event::default().data(
|
||||
json!({
|
||||
"seq": 1,
|
||||
"payload": {
|
||||
"id": "evt_demo_1",
|
||||
"ts": "2026-04-06T15:00:00Z",
|
||||
"run_id": "01JQ0000000000000000000001",
|
||||
"event": "run.started"
|
||||
}
|
||||
"id": "evt_demo_1",
|
||||
"ts": "2026-04-06T15:00:00Z",
|
||||
"run_id": "01JQ0000000000000000000001",
|
||||
"event": "run.started"
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
|
|
@ -521,12 +517,10 @@ pub(crate) async fn attach_events_stub(
|
|||
Event::default().data(
|
||||
json!({
|
||||
"seq": 2,
|
||||
"payload": {
|
||||
"id": "evt_demo_2",
|
||||
"ts": "2026-04-06T15:00:01Z",
|
||||
"run_id": "01JQ0000000000000000000001",
|
||||
"event": "stage.started"
|
||||
}
|
||||
"id": "evt_demo_2",
|
||||
"ts": "2026-04-06T15:00:01Z",
|
||||
"run_id": "01JQ0000000000000000000001",
|
||||
"event": "stage.started"
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -111,10 +111,10 @@ pub use fabro_api::types::{
|
|||
QuestionType as ApiQuestionType, RenderWorkflowGraphDirection, RenderWorkflowGraphFormat,
|
||||
RenderWorkflowGraphRequest, RunArtifactEntry, RunArtifactListResponse, RunBilling,
|
||||
RunBillingStage, RunBillingTotals, RunControlAction as ApiRunControlAction, RunError,
|
||||
RunEvent as ApiRunEvent, RunManifest, RunStatus, RunStatusResponse, SandboxFileEntry,
|
||||
SandboxFileListResponse, ServerSettings, SetSecretRequest, SshAccessRequest, SshAccessResponse,
|
||||
StartRunRequest, StatusReason as ApiStatusReason, SubmitAnswerRequest, SystemInfoResponse,
|
||||
SystemRunCounts, WriteBlobResponse,
|
||||
RunManifest, RunStatus, RunStatusResponse, SandboxFileEntry, SandboxFileListResponse,
|
||||
ServerSettings, SetSecretRequest, SshAccessRequest, SshAccessResponse, StartRunRequest,
|
||||
StatusReason as ApiStatusReason, SubmitAnswerRequest, SystemInfoResponse, SystemRunCounts,
|
||||
WriteBlobResponse,
|
||||
};
|
||||
use fabro_graphviz::render::GraphFormat;
|
||||
|
||||
|
|
@ -2380,8 +2380,28 @@ fn octet_stream_response(bytes: Bytes) -> Response {
|
|||
}
|
||||
|
||||
#[allow(clippy::result_large_err)]
|
||||
fn api_run_event_from_store(payload: &EventPayload) -> Result<ApiRunEvent, Response> {
|
||||
serde_json::from_value(payload.as_value().clone()).map_err(|err| {
|
||||
fn api_event_envelope_from_store(event: &EventEnvelope) -> Result<ApiEventEnvelope, Response> {
|
||||
// Wire EventEnvelope is flattened: seq sits alongside the RunEvent
|
||||
// payload fields at the top level. The progenitor-generated type
|
||||
// reflects that shape, so we merge seq into the payload value and
|
||||
// deserialize directly.
|
||||
let mut value = event.payload.as_value().clone();
|
||||
match value.as_object_mut() {
|
||||
Some(map) => {
|
||||
map.insert(
|
||||
"seq".to_string(),
|
||||
serde_json::Value::Number(i64::from(event.seq).into()),
|
||||
);
|
||||
}
|
||||
None => {
|
||||
return Err(ApiError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"stored event payload is not a JSON object".to_string(),
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
}
|
||||
serde_json::from_value(value).map_err(|err| {
|
||||
ApiError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to serialize stored event: {err}"),
|
||||
|
|
@ -2390,14 +2410,6 @@ fn api_run_event_from_store(payload: &EventPayload) -> Result<ApiRunEvent, Respo
|
|||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::result_large_err)]
|
||||
fn api_event_envelope_from_store(event: &EventEnvelope) -> Result<ApiEventEnvelope, Response> {
|
||||
Ok(ApiEventEnvelope {
|
||||
payload: api_run_event_from_store(&event.payload)?,
|
||||
seq: i64::from(event.seq),
|
||||
})
|
||||
}
|
||||
|
||||
fn clear_live_run_state(run: &mut ManagedRun) {
|
||||
run.answer_transport = None;
|
||||
run.accepted_questions.clear();
|
||||
|
|
|
|||
|
|
@ -270,14 +270,12 @@ async fn cancel_at_human_gate_persists_cancelled_terminal_event() {
|
|||
.unwrap()
|
||||
.iter()
|
||||
.filter_map(|event| {
|
||||
(event["payload"]["event"] == "run.failed").then(|| {
|
||||
(event["event"] == "run.failed").then(|| {
|
||||
(
|
||||
event["payload"]["properties"]["reason"]
|
||||
.as_str()
|
||||
.map(ToOwned::to_owned),
|
||||
event["payload"]["properties"]["error"]
|
||||
event["properties"]["reason"]
|
||||
.as_str()
|
||||
.map(ToOwned::to_owned),
|
||||
event["properties"]["error"].as_str().map(ToOwned::to_owned),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ async fn attach_run_events_replays_terminal_event_after_completion() {
|
|||
.lines()
|
||||
.filter_map(|line| line.strip_prefix("data:"))
|
||||
.filter_map(|line| serde_json::from_str::<serde_json::Value>(line.trim()).ok())
|
||||
.filter_map(|event| event["payload"]["event"].as_str().map(ToString::to_string))
|
||||
.filter_map(|event| event["event"].as_str().map(ToString::to_string))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert!(
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ async fn sse_stream_contains_expected_event_types() {
|
|||
if let Some(json_str) = line.strip_prefix("data:") {
|
||||
let json_str = json_str.trim();
|
||||
if let Ok(event) = serde_json::from_str::<serde_json::Value>(json_str) {
|
||||
if let Some(event_name) = event["payload"]["event"].as_str() {
|
||||
if let Some(event_name) = event["event"].as_str() {
|
||||
event_types.push(event_name.to_string());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ base.ts
|
|||
common.ts
|
||||
configuration.ts
|
||||
index.ts
|
||||
models/actor-kind.ts
|
||||
models/actor-ref.ts
|
||||
models/aggregate-billing-totals.ts
|
||||
models/aggregate-billing.ts
|
||||
models/api-question-option.ts
|
||||
|
|
|
|||
|
|
@ -481,7 +481,7 @@ export const RunInternalsApiAxiosParamCreator = function (configuration?: Config
|
|||
};
|
||||
},
|
||||
/**
|
||||
* Uploads one or more artifacts for a stage. Intended for trusted internal callers. The server accepts both: - `application/octet-stream` for single-file uploads with the `filename` query parameter - strict manifest-first `multipart/form-data` uploads documented by `ArtifactBatchUploadManifest` The generated Rust client currently exposes the octet-stream variant because the OpenAPI code generator in this repo does not support multiple request media types on one operation.
|
||||
* Uploads one or more artifacts for a stage. Intended for trusted internal callers. The server accepts both: - `application/octet-stream` for single-file uploads with the `filename` query parameter - strict manifest-first `multipart/form-data` uploads documented by `ArtifactBatchUploadManifest` The generated Rust client currently exposes the octet-stream variant because the OpenAPI code generator in this repo does not support multiple request media types on one operation.
|
||||
* @summary Put Stage Artifact
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} stageId Identifier of a stage within a run\'s workflow graph, serialized as `node_id@visit`.
|
||||
|
|
@ -847,7 +847,7 @@ export const RunInternalsApiFp = function(configuration?: Configuration) {
|
|||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Uploads one or more artifacts for a stage. Intended for trusted internal callers. The server accepts both: - `application/octet-stream` for single-file uploads with the `filename` query parameter - strict manifest-first `multipart/form-data` uploads documented by `ArtifactBatchUploadManifest` The generated Rust client currently exposes the octet-stream variant because the OpenAPI code generator in this repo does not support multiple request media types on one operation.
|
||||
* Uploads one or more artifacts for a stage. Intended for trusted internal callers. The server accepts both: - `application/octet-stream` for single-file uploads with the `filename` query parameter - strict manifest-first `multipart/form-data` uploads documented by `ArtifactBatchUploadManifest` The generated Rust client currently exposes the octet-stream variant because the OpenAPI code generator in this repo does not support multiple request media types on one operation.
|
||||
* @summary Put Stage Artifact
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} stageId Identifier of a stage within a run\'s workflow graph, serialized as `node_id@visit`.
|
||||
|
|
@ -1028,7 +1028,7 @@ export const RunInternalsApiFactory = function (configuration?: Configuration, b
|
|||
return localVarFp.listStageTurns(id, stageId, pageLimit, pageOffset, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Uploads one or more artifacts for a stage. Intended for trusted internal callers. The server accepts both: - `application/octet-stream` for single-file uploads with the `filename` query parameter - strict manifest-first `multipart/form-data` uploads documented by `ArtifactBatchUploadManifest` The generated Rust client currently exposes the octet-stream variant because the OpenAPI code generator in this repo does not support multiple request media types on one operation.
|
||||
* Uploads one or more artifacts for a stage. Intended for trusted internal callers. The server accepts both: - `application/octet-stream` for single-file uploads with the `filename` query parameter - strict manifest-first `multipart/form-data` uploads documented by `ArtifactBatchUploadManifest` The generated Rust client currently exposes the octet-stream variant because the OpenAPI code generator in this repo does not support multiple request media types on one operation.
|
||||
* @summary Put Stage Artifact
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} stageId Identifier of a stage within a run\'s workflow graph, serialized as `node_id@visit`.
|
||||
|
|
@ -1201,7 +1201,7 @@ export class RunInternalsApi extends BaseAPI {
|
|||
}
|
||||
|
||||
/**
|
||||
* Uploads one or more artifacts for a stage. Intended for trusted internal callers. The server accepts both: - `application/octet-stream` for single-file uploads with the `filename` query parameter - strict manifest-first `multipart/form-data` uploads documented by `ArtifactBatchUploadManifest` The generated Rust client currently exposes the octet-stream variant because the OpenAPI code generator in this repo does not support multiple request media types on one operation.
|
||||
* Uploads one or more artifacts for a stage. Intended for trusted internal callers. The server accepts both: - `application/octet-stream` for single-file uploads with the `filename` query parameter - strict manifest-first `multipart/form-data` uploads documented by `ArtifactBatchUploadManifest` The generated Rust client currently exposes the octet-stream variant because the OpenAPI code generator in this repo does not support multiple request media types on one operation.
|
||||
* @summary Put Stage Artifact
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} stageId Identifier of a stage within a run\'s workflow graph, serialized as `node_id@visit`.
|
||||
|
|
@ -1260,3 +1260,4 @@ export class RunInternalsApi extends BaseAPI {
|
|||
return RunInternalsApiFp(this.configuration).writeRunBlob(id, body, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
30
lib/packages/fabro-api-client/src/models/actor-kind.ts
Normal file
30
lib/packages/fabro-api-client/src/models/actor-kind.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* High-level category of an event actor.
|
||||
*/
|
||||
|
||||
export const ActorKind = {
|
||||
USER: 'user',
|
||||
AGENT: 'agent',
|
||||
SYSTEM: 'system'
|
||||
} as const;
|
||||
|
||||
export type ActorKind = typeof ActorKind[keyof typeof ActorKind];
|
||||
|
||||
|
||||
|
||||
36
lib/packages/fabro-api-client/src/models/actor-ref.ts
Normal file
36
lib/packages/fabro-api-client/src/models/actor-ref.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { ActorKind } from './actor-kind';
|
||||
|
||||
/**
|
||||
* Optional primary actor associated with a run event. Present on control actions and durable agent output where a stable user or agent identity matters; omitted on routine runtime lifecycle events.
|
||||
*/
|
||||
export interface ActorRef {
|
||||
'kind': ActorKind;
|
||||
/**
|
||||
* Stable actor identifier when available.
|
||||
*/
|
||||
'id'?: string;
|
||||
/**
|
||||
* Display-friendly label for the actor.
|
||||
*/
|
||||
'display'?: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -32,6 +32,10 @@ export interface ApiQuestion {
|
|||
* The question text displayed to the user.
|
||||
*/
|
||||
'text': string;
|
||||
/**
|
||||
* Workflow stage identifier that produced the question.
|
||||
*/
|
||||
'stage': string;
|
||||
'question_type': QuestionType;
|
||||
/**
|
||||
* Available options for selection-based questions. Empty for freeform questions.
|
||||
|
|
@ -41,6 +45,14 @@ export interface ApiQuestion {
|
|||
* Whether the user may provide freeform text in addition to selecting options.
|
||||
*/
|
||||
'allow_freeform': boolean;
|
||||
/**
|
||||
* Timeout for the question when configured by the workflow.
|
||||
*/
|
||||
'timeout_seconds'?: number;
|
||||
/**
|
||||
* Optional contextual text shown alongside the question.
|
||||
*/
|
||||
'context_display'?: string;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
|
|
@ -39,3 +39,4 @@ export interface ArtifactBatchUploadEntry {
|
|||
*/
|
||||
'content_type'?: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
|
|
@ -23,3 +23,4 @@ import type { ArtifactBatchUploadEntry } from './artifact-batch-upload-entry';
|
|||
export interface ArtifactBatchUploadManifest {
|
||||
'entries': Array<ArtifactBatchUploadEntry>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,18 +13,17 @@
|
|||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { ActorRef } from './actor-ref';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { RunEvent } from './run-event';
|
||||
|
||||
/**
|
||||
* Stored event envelope with assigned sequence number.
|
||||
* @type EventEnvelope
|
||||
* Stored event envelope with assigned sequence number. On the wire the envelope is flattened: seq sits alongside the RunEvent payload fields at the top level of the JSON object.
|
||||
*/
|
||||
export interface EventEnvelope {
|
||||
/**
|
||||
* Assigned event sequence number.
|
||||
*/
|
||||
'seq': number;
|
||||
'payload': RunEvent;
|
||||
}
|
||||
export type EventEnvelope = RunEvent;
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
export * from './actor-kind';
|
||||
export * from './actor-ref';
|
||||
export * from './aggregate-billing';
|
||||
export * from './aggregate-billing-totals';
|
||||
export * from './api-question';
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@
|
|||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { ActorRef } from './actor-ref';
|
||||
|
||||
/**
|
||||
* Internal RunEvent-compatible JSON payload. The server validates this body by deserializing into the typed RunEvent struct.
|
||||
|
|
@ -25,8 +28,25 @@ export interface RunEvent {
|
|||
'run_id': string;
|
||||
'node_id'?: string;
|
||||
'node_label'?: string;
|
||||
/**
|
||||
* Stage execution identity, formatted as \"{node_id}@{visit}\".
|
||||
*/
|
||||
'stage_id'?: string;
|
||||
/**
|
||||
* Durable identity of one execution of a parallel node, formatted as \"{node_id}@{visit}\".
|
||||
*/
|
||||
'parallel_group_id'?: string;
|
||||
/**
|
||||
* Durable identity of one branch within a parallel execution, formatted as \"{parallel_group_id}:{index}\".
|
||||
*/
|
||||
'parallel_branch_id'?: string;
|
||||
'session_id'?: string;
|
||||
'parent_session_id'?: string;
|
||||
/**
|
||||
* Stable identifier for a tool call, present on agent.tool.* events and other durable events that directly describe the same tool call.
|
||||
*/
|
||||
'tool_call_id'?: string;
|
||||
'actor'?: ActorRef;
|
||||
/**
|
||||
* Event type discriminator.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ import type { AuthSettings } from './auth-settings';
|
|||
* Web UI configuration.
|
||||
*/
|
||||
export interface WebSettings {
|
||||
/**
|
||||
* Whether the embedded web UI and browser-oriented routes are enabled.
|
||||
*/
|
||||
'enabled'?: boolean;
|
||||
/**
|
||||
* Web UI URL.
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue