diff --git a/apps/fabro-web/app/lib/run-events.ts b/apps/fabro-web/app/lib/run-events.ts index 25f232949..990cd9970 100644 --- a/apps/fabro-web/app/lib/run-events.ts +++ b/apps/fabro-web/app/lib/run-events.ts @@ -96,8 +96,8 @@ const STEERING_EVENTS = new Set([ "agent.steer.buffered", "agent.steer.dropped", ]); -// Todo / task mutation events refresh `getRunState` consumers (so the -// projected `todos_by_list` map updates live) and the run events list. +// Todo / task mutation events refresh `getRunState` consumers (so per-stage +// todo projections update live) and the run events list. const TODO_EVENTS = new Set([ "todo.created", "todo.updated", @@ -252,4 +252,4 @@ export function useRunEvents(runId: string | undefined) { if (!runId) return; return subscribeToRunEvents(runId, mutate as MutateFn); }, [mutate, runId]); -} \ No newline at end of file +} diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index 3668b4230..da9fe7f23 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -7758,10 +7758,218 @@ components: oneOf: - $ref: "#/components/schemas/BillingModelRef" - type: "null" + todos: + oneOf: + - $ref: "#/components/schemas/TodoListProjection" + - type: "null" + description: Projected todo / task list for this stage. + subagents: + type: array + description: Subagents spawned by this stage, in replay/insertion order. + items: + $ref: "#/components/schemas/SubAgentProjection" + skills: + $ref: "#/components/schemas/SkillsProjection" + description: Agent skills discovered and activated during this stage. + mcp_servers: + type: array + description: MCP servers observed by this stage. + items: + $ref: "#/components/schemas/McpServerProjection" state: $ref: "#/components/schemas/StageState" description: Lifecycle state of the stage projection. + SubAgentProjection: + description: Current projected state for one subagent spawned by an agent stage. + type: object + required: + - agent_id + - depth + - task + - status + properties: + agent_id: + type: string + depth: + type: integer + minimum: 0 + task: + type: string + status: + $ref: "#/components/schemas/SubAgentStatus" + + SubAgentStatus: + description: Projected lifecycle status for a subagent. + oneOf: + - $ref: "#/components/schemas/SubAgentStatusRunning" + - $ref: "#/components/schemas/SubAgentStatusCompleted" + - $ref: "#/components/schemas/SubAgentStatusFailed" + - $ref: "#/components/schemas/SubAgentStatusClosed" + discriminator: + propertyName: kind + mapping: + running: "#/components/schemas/SubAgentStatusRunning" + completed: "#/components/schemas/SubAgentStatusCompleted" + failed: "#/components/schemas/SubAgentStatusFailed" + closed: "#/components/schemas/SubAgentStatusClosed" + + SubAgentStatusRunning: + type: object + required: + - kind + properties: + kind: + type: string + enum: [running] + + SubAgentStatusCompleted: + type: object + required: + - kind + - success + - turns_used + properties: + kind: + type: string + enum: [completed] + success: + type: boolean + turns_used: + type: integer + minimum: 0 + + SubAgentStatusFailed: + type: object + required: + - kind + - error + properties: + kind: + type: string + enum: [failed] + error: + description: Provider/tool error payload captured by the subagent event. + + SubAgentStatusClosed: + type: object + required: + - kind + properties: + kind: + type: string + enum: [closed] + + SkillsProjection: + description: Agent skills discovered and activated during a stage. + type: object + required: + - available + - activated + properties: + available: + type: array + items: + $ref: "#/components/schemas/AgentSkillSummary" + activated: + type: array + items: + $ref: "#/components/schemas/ActivatedSkill" + + AgentSkillSummary: + description: Summary of an available agent skill. + type: object + required: + - name + - description + properties: + name: + type: string + description: + type: string + + ActivatedSkill: + description: One observed agent skill activation. + type: object + required: + - name + - source + properties: + name: + type: string + source: + $ref: "#/components/schemas/AgentSkillActivationSource" + + AgentSkillActivationSource: + description: Source that activated an agent skill. + type: string + enum: [slash, tool] + + McpServerProjection: + description: Projected state for one MCP server observed by an agent stage. + type: object + required: + - server_name + - tool_count + - status + properties: + server_name: + type: string + tool_count: + type: integer + minimum: 0 + status: + $ref: "#/components/schemas/McpServerStatus" + + McpServerStatus: + description: Projected MCP server readiness status. + oneOf: + - $ref: "#/components/schemas/McpServerStatusReady" + - $ref: "#/components/schemas/McpServerStatusFailed" + discriminator: + propertyName: kind + mapping: + ready: "#/components/schemas/McpServerStatusReady" + failed: "#/components/schemas/McpServerStatusFailed" + + McpServerStatusReady: + type: object + required: + - kind + - tools + properties: + kind: + type: string + enum: [ready] + tools: + type: array + items: + $ref: "#/components/schemas/AgentMcpToolSummary" + + McpServerStatusFailed: + type: object + required: + - kind + - error + properties: + kind: + type: string + enum: [failed] + error: + type: string + + AgentMcpToolSummary: + description: Summary of one tool exposed by an MCP server. + type: object + required: + - name + - original_name + properties: + name: + type: string + original_name: + type: string + StageModelUsage: description: Provider, model, and request-control metadata recorded for a stage attempt. type: object @@ -8069,15 +8277,6 @@ components: type: object additionalProperties: $ref: "#/components/schemas/PendingInterviewRecord" - todos_by_list: - type: object - description: |- - Projected todo / task lists, keyed by `list_id` - (`openai_plan:` or - `anthropic_tasks:`). Built by replaying - `todo.created`, `todo.updated`, and `todo.deleted` events. - additionalProperties: - $ref: "#/components/schemas/TodoListProjection" stages: type: object description: Map from StageId (`node_id@visit`) to stage projection data. diff --git a/lib/crates/fabro-api/build.rs b/lib/crates/fabro-api/build.rs index bceed7000..483dad5e2 100644 --- a/lib/crates/fabro-api/build.rs +++ b/lib/crates/fabro-api/build.rs @@ -344,6 +344,28 @@ fn main() { ("CommandTermination", "fabro_types::CommandTermination", &[]), ("StageModelUsage", "fabro_types::StageModelUsage", &[]), ("StageProjection", "fabro_types::StageProjection", &[]), + ("TodoListProjection", "fabro_types::TodoListProjection", &[]), + ("SubAgentProjection", "fabro_types::SubAgentProjection", &[]), + ("SubAgentStatus", "fabro_types::SubAgentStatus", &[]), + ("SkillsProjection", "fabro_types::SkillsProjection", &[]), + ("ActivatedSkill", "fabro_types::ActivatedSkill", &[]), + ("AgentSkillSummary", "fabro_types::AgentSkillSummary", &[]), + ( + "AgentSkillActivationSource", + "fabro_types::AgentSkillActivationSource", + &[], + ), + ( + "McpServerProjection", + "fabro_types::McpServerProjection", + &[], + ), + ("McpServerStatus", "fabro_types::McpServerStatus", &[]), + ( + "AgentMcpToolSummary", + "fabro_types::AgentMcpToolSummary", + &[], + ), ("SecretMetadata", "fabro_types::SecretMetadata", &[]), ("InterviewOption", "fabro_types::InterviewOption", &[]), ( diff --git a/lib/crates/fabro-api/src/lib.rs b/lib/crates/fabro-api/src/lib.rs index 4767d21d6..93f35e54a 100644 --- a/lib/crates/fabro-api/src/lib.rs +++ b/lib/crates/fabro-api/src/lib.rs @@ -32,22 +32,24 @@ pub mod types { BlockedReason, FailureReason, RunControlAction, RunStatus, SuccessReason, }; pub use fabro_types::{ + ActivatedSkill, AgentMcpToolSummary, AgentSkillActivationSource, AgentSkillSummary, AskFabro, AuthMethod, BilledTokenCounts, CommandTermination, Conclusion, DiffStats, DiffSummary, DirtyStatus, EventEnvelope, ExecOutputTail, FailureCategory, FailureDetail, FailureSignature, GitContext, IdpIdentity, InterviewOption, InterviewQuestionRecord, - PairId, PairMessageId, PairMessageRecord, PairMessageRequest, PairRecord, PairStartRequest, - PairStatus, PairTarget, PairTranscriptEntry, PairTranscriptResponse, - PendingInterviewRecord, PreRunPushOutcome, Principal, PullRequest, PullRequestDetails, - PullRequestDetailsStatus, PullRequestDetailsUnavailableReason, PullRequestLink, - PullRequestMeta, PullRequestResponse, QuestionType, RepositoryRef, Run, - RunClientProvenance, RunEvent, RunEventDetailContentKind, RunEventDetailResponse, - RunFailure, RunPairStatusResponse, RunProjection, RunProvenance, RunSandbox, - RunSandboxRuntime, RunServerProvenance, SandboxDetails, SandboxNetwork, + McpServerProjection, McpServerStatus, PairId, PairMessageId, PairMessageRecord, + PairMessageRequest, PairRecord, PairStartRequest, PairStatus, PairTarget, + PairTranscriptEntry, PairTranscriptResponse, PendingInterviewRecord, PreRunPushOutcome, + Principal, PullRequest, PullRequestDetails, PullRequestDetailsStatus, + PullRequestDetailsUnavailableReason, PullRequestLink, PullRequestMeta, PullRequestResponse, + QuestionType, RepositoryRef, Run, RunClientProvenance, RunEvent, RunEventDetailContentKind, + RunEventDetailResponse, RunFailure, RunPairStatusResponse, RunProjection, RunProvenance, + RunSandbox, RunSandboxRuntime, RunServerProvenance, SandboxDetails, SandboxNetwork, SandboxNetworkPolicy, SandboxNetworkPolicyMode, SandboxProvider, SandboxResources, SandboxService, SandboxServiceListResponse, SandboxState, SandboxTimestamps, SecretMetadata, SecretType, ServerSettings, SessionDetail, SessionId, SessionMessage, - SessionRecord, SessionStatus, SessionSummary, SessionTurn, StageCompletion, StageHandler, - StageModelUsage, StageOutcome, StageProjection, StageState, SystemActorKind, TurnId, + SessionRecord, SessionStatus, SessionSummary, SessionTurn, SkillsProjection, + StageCompletion, StageHandler, StageModelUsage, StageOutcome, StageProjection, StageState, + SubAgentProjection, SubAgentStatus, SystemActorKind, TodoListProjection, TurnId, UserPrincipal, WorkflowSettings, }; diff --git a/lib/crates/fabro-api/tests/stage_projection_round_trip.rs b/lib/crates/fabro-api/tests/stage_projection_round_trip.rs index bc746d15e..87f9afd95 100644 --- a/lib/crates/fabro-api/tests/stage_projection_round_trip.rs +++ b/lib/crates/fabro-api/tests/stage_projection_round_trip.rs @@ -1,7 +1,18 @@ use std::any::{TypeId, type_name}; -use fabro_api::types::StageProjection as ApiStageProjection; -use fabro_types::StageProjection; +use fabro_api::types::{ + ActivatedSkill as ApiActivatedSkill, AgentMcpToolSummary as ApiAgentMcpToolSummary, + AgentSkillActivationSource as ApiAgentSkillActivationSource, + AgentSkillSummary as ApiAgentSkillSummary, McpServerProjection as ApiMcpServerProjection, + McpServerStatus as ApiMcpServerStatus, SkillsProjection as ApiSkillsProjection, + StageProjection as ApiStageProjection, SubAgentProjection as ApiSubAgentProjection, + SubAgentStatus as ApiSubAgentStatus, TodoListProjection as ApiTodoListProjection, +}; +use fabro_types::{ + ActivatedSkill, AgentMcpToolSummary, AgentSkillActivationSource, AgentSkillSummary, + McpServerProjection, McpServerStatus, SkillsProjection, StageProjection, SubAgentProjection, + SubAgentStatus, TodoListKind, TodoListProjection, +}; use serde_json::json; #[test] @@ -9,6 +20,20 @@ fn stage_projection_reuses_canonical_type() { assert_same_type::(); } +#[test] +fn stage_projection_reuses_nested_agent_state_types() { + assert_same_type::(); + assert_same_type::(); + assert_same_type::(); + assert_same_type::(); + assert_same_type::(); + assert_same_type::(); + assert_same_type::(); + assert_same_type::(); + assert_same_type::(); + assert_same_type::(); +} + #[test] fn stage_projection_round_trips_representative_json() { let value = json!({ @@ -49,6 +74,60 @@ fn stage_projection_round_trips_representative_json() { "cache_read_tokens": 0, "cache_write_tokens": 0 }, + "todos": { + "kind": "openai_plan", + "list_id": "openai_plan:ses_root", + "items": [ + { + "id": "todo-1", + "status": "in_progress", + "order": 0, + "subject": "Write tests", + "active_form": "Writing tests" + } + ] + }, + "subagents": [ + { + "agent_id": "sub-1", + "depth": 1, + "task": "Investigate failing test", + "status": { + "kind": "completed", + "success": true, + "turns_used": 3 + } + } + ], + "skills": { + "available": [ + { + "name": "rust", + "description": "Rust workflow help" + } + ], + "activated": [ + { + "name": "rust", + "source": "slash" + } + ] + }, + "mcp_servers": [ + { + "server_name": "filesystem", + "tool_count": 1, + "status": { + "kind": "ready", + "tools": [ + { + "name": "read_file", + "original_name": "read_file" + } + ] + } + } + ], "state": "succeeded" }); @@ -56,6 +135,138 @@ fn stage_projection_round_trips_representative_json() { assert_eq!(serde_json::to_value(state).unwrap(), value); } +#[test] +fn nested_agent_state_types_match_openapi_json_shape() { + let todo_list = TodoListProjection::new(TodoListKind::OpenAiPlan, "openai_plan:ses_root"); + let todo_json = serde_json::to_value(&todo_list).unwrap(); + assert_eq!( + todo_json, + json!({ + "kind": "openai_plan", + "list_id": "openai_plan:ses_root", + "items": [] + }) + ); + let api_todo_list: ApiTodoListProjection = serde_json::from_value(todo_json).unwrap(); + assert_eq!(api_todo_list, todo_list); + + let subagent = SubAgentProjection { + agent_id: "sub-1".to_string(), + depth: 1, + task: "Investigate failing test".to_string(), + status: SubAgentStatus::Completed { + success: true, + turns_used: 3, + }, + }; + let subagent_json = serde_json::to_value(&subagent).unwrap(); + assert_eq!( + subagent_json, + json!({ + "agent_id": "sub-1", + "depth": 1, + "task": "Investigate failing test", + "status": { + "kind": "completed", + "success": true, + "turns_used": 3 + } + }) + ); + let api_subagent: ApiSubAgentProjection = serde_json::from_value(subagent_json).unwrap(); + assert_eq!(api_subagent, subagent); + + let skill = AgentSkillSummary { + name: "rust".to_string(), + description: "Rust workflow help".to_string(), + }; + let skill_json = serde_json::to_value(&skill).unwrap(); + assert_eq!( + skill_json, + json!({ + "name": "rust", + "description": "Rust workflow help" + }) + ); + let api_skill: ApiAgentSkillSummary = serde_json::from_value(skill_json).unwrap(); + assert_eq!(api_skill, skill); + + let source_json = serde_json::to_value(AgentSkillActivationSource::Slash).unwrap(); + assert_eq!(source_json, json!("slash")); + let api_source: ApiAgentSkillActivationSource = serde_json::from_value(source_json).unwrap(); + assert_eq!(api_source, AgentSkillActivationSource::Slash); + + let activated = ActivatedSkill { + name: "rust".to_string(), + source: AgentSkillActivationSource::Slash, + }; + let skills = SkillsProjection { + available: vec![skill], + activated: vec![activated], + }; + let skills_json = serde_json::to_value(&skills).unwrap(); + assert_eq!( + skills_json, + json!({ + "available": [ + { + "name": "rust", + "description": "Rust workflow help" + } + ], + "activated": [ + { + "name": "rust", + "source": "slash" + } + ] + }) + ); + let api_skills: ApiSkillsProjection = serde_json::from_value(skills_json).unwrap(); + assert_eq!(api_skills, skills); + + let tool = AgentMcpToolSummary { + name: "read_file".to_string(), + original_name: "read_file".to_string(), + }; + let tool_json = serde_json::to_value(&tool).unwrap(); + assert_eq!( + tool_json, + json!({ + "name": "read_file", + "original_name": "read_file" + }) + ); + let api_tool: ApiAgentMcpToolSummary = serde_json::from_value(tool_json).unwrap(); + assert_eq!(api_tool, tool); + + let mcp_server = McpServerProjection { + server_name: "filesystem".to_string(), + tool_count: 1, + status: McpServerStatus::Ready { tools: vec![tool] }, + }; + let mcp_json = serde_json::to_value(&mcp_server).unwrap(); + assert_eq!( + mcp_json, + json!({ + "server_name": "filesystem", + "tool_count": 1, + "status": { + "kind": "ready", + "tools": [ + { + "name": "read_file", + "original_name": "read_file" + } + ] + } + }) + ); + let api_mcp: ApiMcpServerProjection = serde_json::from_value(mcp_json).unwrap(); + assert_eq!(api_mcp, mcp_server); + assert_eq!(mcp_server.tool_count, 1); +} + fn assert_same_type() { assert_eq!( TypeId::of::(), diff --git a/lib/crates/fabro-store/src/run_state.rs b/lib/crates/fabro-store/src/run_state.rs index cf6e0bb97..0fdd4dd3a 100644 --- a/lib/crates/fabro-store/src/run_state.rs +++ b/lib/crates/fabro-store/src/run_state.rs @@ -8,12 +8,13 @@ use fabro_types::run_event::{ }; use fabro_types::settings::run::{EnvironmentProvider, RunEnvironmentSettings}; use fabro_types::{ - AskFabro, BilledModelUsage, Checkpoint, CheckpointRecord, CommandTermination, Conclusion, - EventBody, FailureSignature, InterviewQuestionRecord, Outcome, PendingInterviewRecord, - PullRequestLink, RepositoryRef, Run, RunBillingSummary, RunControlAction, RunDiff, RunEvent, - RunId, RunLifecycle, RunLinks, RunModel, RunOrigin, RunProjection, RunSandbox, - RunSandboxRuntime, RunSpec, RunStatus, RunTimestamps, SandboxProvider, StageCompletion, - StageHandler, StageId, StageModelUsage, StageOutcome, StageProjection, StageState, StartRecord, + ActivatedSkill, AskFabro, BilledModelUsage, Checkpoint, CheckpointRecord, CommandTermination, + Conclusion, EventBody, FailureSignature, InterviewQuestionRecord, McpServerProjection, + McpServerStatus, Outcome, PendingInterviewRecord, PullRequestLink, RepositoryRef, Run, + RunBillingSummary, RunControlAction, RunDiff, RunEvent, RunId, RunLifecycle, RunLinks, + RunModel, RunOrigin, RunProjection, RunSandbox, RunSandboxRuntime, RunSpec, RunStatus, + RunTimestamps, SandboxProvider, StageCompletion, StageHandler, StageId, StageModelUsage, + StageOutcome, StageProjection, StageState, StartRecord, SubAgentProjection, SubAgentStatus, TodoListProjection, TodoProjection, WorkflowRef, first_event_seq, }; use fabro_util::error::render_compact_with_causes; @@ -449,13 +450,109 @@ impl RunProjectionReducer for RunProjection { stage.parallel_results = Some(parallel_results); } EventBody::TodoCreated(props) => { - apply_todo_created(self, props); + let Some(stage) = stage_at_stored_or_current_visit(self, stored, event.seq) else { + return Ok(()); + }; + apply_todo_created(stage, props); } EventBody::TodoUpdated(props) => { - apply_todo_updated(self, props); + let Some(stage) = stage_at_stored_or_current_visit(self, stored, event.seq) else { + return Ok(()); + }; + apply_todo_updated(stage, props); } EventBody::TodoDeleted(props) => { - apply_todo_deleted(self, props); + let Some(stage) = stage_at_stored_or_current_visit(self, stored, event.seq) else { + return Ok(()); + }; + apply_todo_deleted(stage, props); + } + EventBody::AgentSubSpawned(props) => { + let Some(stage) = stage_at_stored_or_visit(self, stored, props.visit, event.seq) + else { + return Ok(()); + }; + stage.subagents.push(SubAgentProjection { + agent_id: props.agent_id.clone(), + depth: props.depth, + task: props.task.clone(), + status: SubAgentStatus::Running, + }); + } + EventBody::AgentSubCompleted(props) => { + let Some(stage) = stage_at_stored_or_visit(self, stored, props.visit, event.seq) + else { + return Ok(()); + }; + if let Some(subagent) = subagent_mut(stage, &props.agent_id) { + subagent.status = SubAgentStatus::Completed { + success: props.success, + turns_used: props.turns_used, + }; + } + } + EventBody::AgentSubFailed(props) => { + let Some(stage) = stage_at_stored_or_visit(self, stored, props.visit, event.seq) + else { + return Ok(()); + }; + if let Some(subagent) = subagent_mut(stage, &props.agent_id) { + subagent.status = SubAgentStatus::Failed { + error: props.error.clone(), + }; + } + } + EventBody::AgentSubClosed(props) => { + let Some(stage) = stage_at_stored_or_visit(self, stored, props.visit, event.seq) + else { + return Ok(()); + }; + if let Some(subagent) = subagent_mut(stage, &props.agent_id) { + subagent.status = SubAgentStatus::Closed; + } + } + EventBody::AgentSkillsDiscovered(props) => { + let Some(stage) = stage_at_stored_or_visit(self, stored, props.visit, event.seq) + else { + return Ok(()); + }; + stage.skills.available.clone_from(&props.skills); + } + EventBody::AgentSkillActivated(props) => { + let Some(stage) = stage_at_stored_or_visit(self, stored, props.visit, event.seq) + else { + return Ok(()); + }; + stage.skills.activated.push(ActivatedSkill { + name: props.skill_name.clone(), + source: props.source, + }); + } + EventBody::AgentMcpReady(props) => { + let Some(stage) = stage_at_stored_or_visit(self, stored, props.visit, event.seq) + else { + return Ok(()); + }; + upsert_mcp_server(stage, McpServerProjection { + server_name: props.server_name.clone(), + tool_count: props.tool_count, + status: McpServerStatus::Ready { + tools: props.tools.clone(), + }, + }); + } + EventBody::AgentMcpFailed(props) => { + let Some(stage) = stage_at_stored_or_visit(self, stored, props.visit, event.seq) + else { + return Ok(()); + }; + upsert_mcp_server(stage, McpServerProjection { + server_name: props.server_name.clone(), + tool_count: 0, + status: McpServerStatus::Failed { + error: props.error.clone(), + }, + }); } _ => {} } @@ -464,11 +561,18 @@ impl RunProjectionReducer for RunProjection { } } -fn apply_todo_created(state: &mut RunProjection, props: &TodoCreatedProps) { - let list = state - .todos_by_list - .entry(props.list_id.clone()) - .or_insert_with(|| TodoListProjection::new(props.list_kind, props.list_id.clone())); +fn apply_todo_created(stage: &mut StageProjection, props: &TodoCreatedProps) { + if stage + .todos + .as_ref() + .is_none_or(|list| list.list_id != props.list_id || list.kind != props.list_kind) + { + stage.todos = Some(TodoListProjection::new( + props.list_kind, + props.list_id.clone(), + )); + } + let list = stage.todos.as_mut().expect("todo list was just inserted"); list.upsert(TodoProjection { id: props.todo_id.clone(), status: props.status, @@ -483,18 +587,49 @@ fn apply_todo_created(state: &mut RunProjection, props: &TodoCreatedProps) { }); } -fn apply_todo_updated(state: &mut RunProjection, props: &TodoUpdatedProps) { - if let Some(list) = state.todos_by_list.get_mut(&props.list_id) { +fn apply_todo_updated(stage: &mut StageProjection, props: &TodoUpdatedProps) { + if let Some(list) = stage + .todos + .as_mut() + .filter(|list| list.list_id == props.list_id) + { list.apply_patch(&props.todo_id, &fabro_types::TodoPatch::from_props(props)); } } -fn apply_todo_deleted(state: &mut RunProjection, props: &TodoDeletedProps) { - if let Some(list) = state.todos_by_list.get_mut(&props.list_id) { - list.remove(&props.todo_id); - if list.items.is_empty() { - state.todos_by_list.remove(&props.list_id); - } +fn apply_todo_deleted(stage: &mut StageProjection, props: &TodoDeletedProps) { + let Some(list) = stage + .todos + .as_mut() + .filter(|list| list.list_id == props.list_id) + else { + return; + }; + list.remove(&props.todo_id); + if list.items.is_empty() { + stage.todos = None; + } +} + +fn subagent_mut<'a>( + stage: &'a mut StageProjection, + agent_id: &str, +) -> Option<&'a mut SubAgentProjection> { + stage + .subagents + .iter_mut() + .find(|subagent| subagent.agent_id == agent_id) +} + +fn upsert_mcp_server(stage: &mut StageProjection, server: McpServerProjection) { + if let Some(existing) = stage + .mcp_servers + .iter_mut() + .find(|existing| existing.server_name == server.server_name) + { + *existing = server; + } else { + stage.mcp_servers.push(server); } } @@ -892,8 +1027,11 @@ mod tests { use fabro_types::run_event::run::RunFailedProps; use fabro_types::run_event::{ AgentAcpCancelledProps, AgentAcpCompletedProps, AgentAcpStartedProps, - AgentAcpTimedOutProps, AgentMessageProps, AgentSessionActivatedProps, - AgentSessionEndedProps, AgentSessionStartedProps, CheckpointCompletedProps, + AgentAcpTimedOutProps, AgentMcpFailedProps, AgentMcpReadyProps, AgentMcpToolSummary, + AgentMessageProps, AgentSessionActivatedProps, AgentSessionEndedProps, + AgentSessionStartedProps, AgentSkillActivatedProps, AgentSkillActivationSource, + AgentSkillSummary, AgentSkillsDiscoveredProps, AgentSubClosedProps, AgentSubCompletedProps, + AgentSubFailedProps, AgentSubSpawnedProps, CheckpointCompletedProps, InterviewCompletedProps, InterviewOption, InterviewStartedProps, RunControlEffectProps, StageCompletedProps, StageFailedProps, StagePromptProps, StageRetryingProps, StageStartedProps, @@ -901,9 +1039,10 @@ mod tests { use fabro_types::{ AgentBackend, BilledModelUsage, BilledTokenCounts, BlockedReason, Checkpoint, CheckpointRecord, CommandTermination, EventBody, FailureCategory, FailureDetail, - FailureReason, Graph, Outcome, PullRequestLink, QuestionType, ReasoningEffort, RunBlobId, - RunControlAction, RunDiff, RunEvent, RunSpec, RunStatus, Speed, StageModelUsage, - StageOutcome, StageState, SuccessReason, WorkflowSettings, first_event_seq, fixtures, + FailureReason, Graph, McpServerStatus, Outcome, PullRequestLink, QuestionType, + ReasoningEffort, RunBlobId, RunControlAction, RunDiff, RunEvent, RunSpec, RunStatus, Speed, + StageModelUsage, StageOutcome, StageState, SubAgentStatus, SuccessReason, WorkflowSettings, + first_event_seq, fixtures, }; use serde_json::json; @@ -3104,10 +3243,21 @@ mod tests { mod todo_reducer { use fabro_types::run_event::{TodoCreatedProps, TodoDeletedProps, TodoUpdatedProps}; - use fabro_types::{TodoListKind, TodoStatus}; + use fabro_types::{TodoListKind, TodoListProjection, TodoStatus}; use super::*; + fn stage_id() -> StageId { + StageId::new("code", 1) + } + + fn stage_todos<'a>(state: &'a RunProjection, stage_id: &StageId) -> &'a TodoListProjection { + state + .stage(stage_id) + .and_then(|stage| stage.todos.as_ref()) + .expect("stage todos present") + } + fn created( list: &str, list_kind: TodoListKind, @@ -3164,30 +3314,32 @@ mod tests { #[test] fn replay_reconstructs_current_list() { let mut state = initialized_projection(); + let stage_id = stage_id(); let list = "openai_plan:ses_a"; state - .apply_event(&test_event( + .apply_event(&test_stage_event( 1, created(list, TodoListKind::OpenAiPlan, "a", 0, "first"), - None, + stage_id.clone(), )) .unwrap(); state - .apply_event(&test_event( + .apply_event(&test_stage_event( 2, created(list, TodoListKind::OpenAiPlan, "b", 1, "second"), - None, + stage_id.clone(), )) .unwrap(); state - .apply_event(&test_event( + .apply_event(&test_stage_event( 3, updated_status(list, TodoListKind::OpenAiPlan, "a", TodoStatus::InProgress), - None, + stage_id.clone(), )) .unwrap(); - let projection = state.todos_by_list.get(list).expect("list present"); + let projection = stage_todos(&state, &stage_id); + assert_eq!(projection.list_id, list); assert_eq!(projection.items.len(), 2); assert_eq!(projection.items[0].id, "a"); assert_eq!(projection.items[0].status, TodoStatus::InProgress); @@ -3197,53 +3349,57 @@ mod tests { #[test] fn deleted_todos_are_absent() { let mut state = initialized_projection(); + let stage_id = stage_id(); let list = "openai_plan:ses_a"; state - .apply_event(&test_event( + .apply_event(&test_stage_event( 1, created(list, TodoListKind::OpenAiPlan, "a", 0, "first"), - None, + stage_id.clone(), )) .unwrap(); state - .apply_event(&test_event( + .apply_event(&test_stage_event( 2, created(list, TodoListKind::OpenAiPlan, "b", 1, "second"), - None, + stage_id.clone(), )) .unwrap(); state - .apply_event(&test_event( + .apply_event(&test_stage_event( 3, deleted(list, TodoListKind::OpenAiPlan, "a"), - None, + stage_id.clone(), )) .unwrap(); - let projection = state.todos_by_list.get(list).expect("list present"); + let projection = stage_todos(&state, &stage_id); assert_eq!(projection.items.len(), 1); assert_eq!(projection.items[0].id, "b"); } #[test] - fn multiple_lists_stay_isolated() { + fn stage_todo_lists_stay_isolated() { let mut state = initialized_projection(); + let plan_one = StageId::new("plan_one", 1); + let plan_two = StageId::new("plan_two", 1); + let claude = StageId::new("claude", 1); state - .apply_event(&test_event( + .apply_event(&test_stage_event( 1, created("openai_plan:s1", TodoListKind::OpenAiPlan, "a", 0, "p1"), - None, + plan_one.clone(), )) .unwrap(); state - .apply_event(&test_event( + .apply_event(&test_stage_event( 2, created("openai_plan:s2", TodoListKind::OpenAiPlan, "a", 0, "p2"), - None, + plan_two.clone(), )) .unwrap(); state - .apply_event(&test_event( + .apply_event(&test_stage_event( 3, created( "anthropic_tasks:s_root", @@ -3252,35 +3408,32 @@ mod tests { 0, "claude task", ), - None, + claude.clone(), )) .unwrap(); - assert_eq!(state.todos_by_list.len(), 3); - assert_eq!(state.todos_by_list["openai_plan:s1"].items[0].subject, "p1"); - assert_eq!(state.todos_by_list["openai_plan:s2"].items[0].subject, "p2"); - assert_eq!( - state.todos_by_list["anthropic_tasks:s_root"].items[0].subject, - "claude task" - ); + assert_eq!(stage_todos(&state, &plan_one).items[0].subject, "p1"); + assert_eq!(stage_todos(&state, &plan_two).items[0].subject, "p2"); + assert_eq!(stage_todos(&state, &claude).items[0].subject, "claude task"); } #[test] fn metadata_patch_merges_and_null_deletes() { let mut state = initialized_projection(); + let stage_id = stage_id(); let list = "anthropic_tasks:r"; state - .apply_event(&test_event( + .apply_event(&test_stage_event( 1, created(list, TodoListKind::AnthropicTasks, "1", 0, "t"), - None, + stage_id.clone(), )) .unwrap(); let mut meta = BTreeMap::new(); meta.insert("k1".to_string(), serde_json::json!("v1")); meta.insert("k2".to_string(), serde_json::json!("v2")); state - .apply_event(&test_event( + .apply_event(&test_stage_event( 2, EventBody::TodoUpdated(TodoUpdatedProps { list_id: list.to_string(), @@ -3296,13 +3449,13 @@ mod tests { add_blocked_by: None, metadata_patch: meta, }), - None, + stage_id.clone(), )) .unwrap(); let mut delete = BTreeMap::new(); delete.insert("k1".to_string(), serde_json::Value::Null); state - .apply_event(&test_event( + .apply_event(&test_stage_event( 3, EventBody::TodoUpdated(TodoUpdatedProps { list_id: list.to_string(), @@ -3318,13 +3471,242 @@ mod tests { add_blocked_by: None, metadata_patch: delete, }), - None, + stage_id.clone(), )) .unwrap(); - let todo = &state.todos_by_list[list].items[0]; + let todo = &stage_todos(&state, &stage_id).items[0]; assert!(!todo.metadata.contains_key("k1")); assert_eq!(todo.metadata.get("k2"), Some(&serde_json::json!("v2"))); } } + + mod agent_state_reducer { + use super::*; + + fn stage_id() -> StageId { + StageId::new("code", 1) + } + + #[test] + fn subagent_events_update_stage_projection() { + let mut state = initialized_projection(); + let stage_id = stage_id(); + + state + .apply_event(&test_stage_event( + 1, + EventBody::AgentSubSpawned(AgentSubSpawnedProps { + agent_id: "sub-1".to_string(), + depth: 1, + task: "write tests".to_string(), + visit: 1, + }), + stage_id.clone(), + )) + .unwrap(); + let stage = state.stage(&stage_id).unwrap(); + assert_eq!(stage.subagents.len(), 1); + assert_eq!(stage.subagents[0].agent_id, "sub-1"); + assert_eq!(stage.subagents[0].depth, 1); + assert_eq!(stage.subagents[0].task, "write tests"); + assert_eq!(stage.subagents[0].status, SubAgentStatus::Running); + + state + .apply_event(&test_stage_event( + 2, + EventBody::AgentSubCompleted(AgentSubCompletedProps { + agent_id: "sub-1".to_string(), + depth: 1, + success: true, + turns_used: 3, + visit: 1, + }), + stage_id.clone(), + )) + .unwrap(); + let stage = state.stage(&stage_id).unwrap(); + assert_eq!(stage.subagents[0].status, SubAgentStatus::Completed { + success: true, + turns_used: 3, + }); + + state + .apply_event(&test_stage_event( + 3, + EventBody::AgentSubSpawned(AgentSubSpawnedProps { + agent_id: "sub-2".to_string(), + depth: 2, + task: "debug failure".to_string(), + visit: 1, + }), + stage_id.clone(), + )) + .unwrap(); + state + .apply_event(&test_stage_event( + 4, + EventBody::AgentSubFailed(AgentSubFailedProps { + agent_id: "sub-2".to_string(), + depth: 2, + error: json!({ "message": "boom" }), + visit: 1, + }), + stage_id.clone(), + )) + .unwrap(); + let stage = state.stage(&stage_id).unwrap(); + assert_eq!(stage.subagents[1].status, SubAgentStatus::Failed { + error: json!({ "message": "boom" }), + }); + + state + .apply_event(&test_stage_event( + 5, + EventBody::AgentSubClosed(AgentSubClosedProps { + agent_id: "sub-2".to_string(), + depth: 2, + visit: 1, + }), + stage_id.clone(), + )) + .unwrap(); + let stage = state.stage(&stage_id).unwrap(); + assert_eq!(stage.subagents[1].status, SubAgentStatus::Closed); + } + + #[test] + fn skill_events_update_stage_projection() { + let mut state = initialized_projection(); + let stage_id = stage_id(); + + state + .apply_event(&test_stage_event( + 1, + EventBody::AgentSkillsDiscovered(AgentSkillsDiscoveredProps { + provider_profile: "claude".to_string(), + source_dirs: vec![".claude/skills".to_string()], + skills: vec![ + AgentSkillSummary { + name: "rust".to_string(), + description: "Rust help".to_string(), + }, + AgentSkillSummary { + name: "docs".to_string(), + description: "Docs help".to_string(), + }, + ], + visit: 1, + }), + stage_id.clone(), + )) + .unwrap(); + state + .apply_event(&test_stage_event( + 2, + EventBody::AgentSkillActivated(AgentSkillActivatedProps { + skill_name: "rust".to_string(), + source: AgentSkillActivationSource::Slash, + visit: 1, + }), + stage_id.clone(), + )) + .unwrap(); + state + .apply_event(&test_stage_event( + 3, + EventBody::AgentSkillActivated(AgentSkillActivatedProps { + skill_name: "rust".to_string(), + source: AgentSkillActivationSource::Tool, + visit: 1, + }), + stage_id.clone(), + )) + .unwrap(); + + let stage = state.stage(&stage_id).unwrap(); + assert_eq!(stage.skills.available.len(), 2); + assert_eq!(stage.skills.available[0].name, "rust"); + assert_eq!(stage.skills.activated.len(), 2); + assert_eq!(stage.skills.activated[0].name, "rust"); + assert_eq!( + stage.skills.activated[0].source, + AgentSkillActivationSource::Slash + ); + assert_eq!( + stage.skills.activated[1].source, + AgentSkillActivationSource::Tool + ); + } + + #[test] + fn mcp_server_events_update_stage_projection() { + let mut state = initialized_projection(); + let stage_id = stage_id(); + + state + .apply_event(&test_stage_event( + 1, + EventBody::AgentMcpReady(AgentMcpReadyProps { + server_name: "filesystem".to_string(), + tool_count: 2, + tools: vec![ + AgentMcpToolSummary { + name: "read_file".to_string(), + original_name: "read_file".to_string(), + }, + AgentMcpToolSummary { + name: "write_file".to_string(), + original_name: "write_file".to_string(), + }, + ], + visit: 1, + }), + stage_id.clone(), + )) + .unwrap(); + state + .apply_event(&test_stage_event( + 2, + EventBody::AgentMcpFailed(AgentMcpFailedProps { + server_name: "github".to_string(), + error: "missing token".to_string(), + visit: 1, + }), + stage_id.clone(), + )) + .unwrap(); + state + .apply_event(&test_stage_event( + 3, + EventBody::AgentMcpReady(AgentMcpReadyProps { + server_name: "filesystem".to_string(), + tool_count: 1, + tools: vec![AgentMcpToolSummary { + name: "read_file".to_string(), + original_name: "read_file".to_string(), + }], + visit: 1, + }), + stage_id.clone(), + )) + .unwrap(); + + let stage = state.stage(&stage_id).unwrap(); + assert_eq!(stage.mcp_servers.len(), 2); + assert_eq!(stage.mcp_servers[0].server_name, "filesystem"); + assert_eq!(stage.mcp_servers[0].tool_count, 1); + assert_eq!(stage.mcp_servers[0].status, McpServerStatus::Ready { + tools: vec![AgentMcpToolSummary { + name: "read_file".to_string(), + original_name: "read_file".to_string(), + }], + }); + assert_eq!(stage.mcp_servers[1].server_name, "github"); + assert_eq!(stage.mcp_servers[1].tool_count, 0); + assert_eq!(stage.mcp_servers[1].status, McpServerStatus::Failed { + error: "missing token".to_string(), + }); + } + } } diff --git a/lib/crates/fabro-types/src/lib.rs b/lib/crates/fabro-types/src/lib.rs index c41d64b24..3c46fc819 100644 --- a/lib/crates/fabro-types/src/lib.rs +++ b/lib/crates/fabro-types/src/lib.rs @@ -103,8 +103,9 @@ pub use run_event::{ pub use run_failure::RunFailure; pub use run_id::{RunId, fixtures}; pub use run_projection::{ - CheckpointRecord, PendingInterviewRecord, RunProjection, StageModelUsage, StageProjection, - first_event_seq, + ActivatedSkill, CheckpointRecord, McpServerProjection, McpServerStatus, PendingInterviewRecord, + RunProjection, SkillsProjection, StageModelUsage, StageProjection, SubAgentProjection, + SubAgentStatus, first_event_seq, }; pub use run_sandbox::{RunSandbox, RunSandboxRuntime}; pub use run_summary::{ diff --git a/lib/crates/fabro-types/src/run_projection.rs b/lib/crates/fabro-types/src/run_projection.rs index 80d5141e8..39d331746 100644 --- a/lib/crates/fabro-types/src/run_projection.rs +++ b/lib/crates/fabro-types/src/run_projection.rs @@ -7,10 +7,11 @@ use fabro_model::{ReasoningEffort, Speed}; use crate::run_event::{AgentSessionActivatedProps, StagePromptProps}; use crate::{ - AgentBackend, BilledTokenCounts, Checkpoint, Conclusion, InterviewQuestionRecord, - InvalidTransition, ModelRef, PullRequestLink, RunControlAction, RunDiff, RunId, RunSandbox, - RunSpec, RunStatus, StageCompletion, StageHandler, StageId, StageState, StageTiming, - StartRecord, TodoListProjection, + AgentBackend, AgentMcpToolSummary, AgentSkillActivationSource, AgentSkillSummary, + BilledTokenCounts, Checkpoint, Conclusion, InterviewQuestionRecord, InvalidTransition, + ModelRef, PullRequestLink, RunControlAction, RunDiff, RunId, RunSandbox, RunSpec, RunStatus, + StageCompletion, StageHandler, StageId, StageState, StageTiming, StartRecord, + TodoListProjection, }; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -35,11 +36,6 @@ pub struct RunProjection { pub pull_request: Option, pub superseded_by: Option, pub pending_interviews: BTreeMap, - /// Projected todo / task lists, keyed by `list_id` (`openai_plan:` - /// or `anthropic_tasks:`). Maintained by replaying - /// `todo.created`, `todo.updated`, and `todo.deleted` events. - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub todos_by_list: BTreeMap, stages: HashMap, } @@ -150,9 +146,67 @@ pub struct StageProjection { pub usage: BilledTokenCounts, #[serde(default, skip_serializing_if = "Option::is_none")] pub model: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub todos: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub subagents: Vec, + #[serde(default, skip_serializing_if = "SkillsProjection::is_empty")] + pub skills: SkillsProjection, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub mcp_servers: Vec, pub state: StageState, } +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct SubAgentProjection { + pub agent_id: String, + pub depth: usize, + pub task: String, + pub status: SubAgentStatus, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum SubAgentStatus { + Running, + Completed { success: bool, turns_used: usize }, + Failed { error: serde_json::Value }, + Closed, +} + +#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct SkillsProjection { + pub available: Vec, + pub activated: Vec, +} + +impl SkillsProjection { + #[must_use] + pub fn is_empty(&self) -> bool { + self.available.is_empty() && self.activated.is_empty() + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct ActivatedSkill { + pub name: String, + pub source: AgentSkillActivationSource, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct McpServerProjection { + pub server_name: String, + pub tool_count: usize, + pub status: McpServerStatus, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum McpServerStatus { + Ready { tools: Vec }, + Failed { error: String }, +} + /// Convert a 1-based event sequence number into the `NonZeroU32` form used for /// `StageProjection::first_event_seq`. Run event seqs always start at 1. #[must_use] @@ -171,6 +225,10 @@ impl StageProjection { timing: None, usage: BilledTokenCounts::default(), model: None, + todos: None, + subagents: Vec::new(), + skills: SkillsProjection::default(), + mcp_servers: Vec::new(), provider_used: None, diff: None, script_invocation: None, @@ -247,7 +305,6 @@ impl RunProjection { pull_request: None, superseded_by: None, pending_interviews: BTreeMap::new(), - todos_by_list: BTreeMap::new(), stages: HashMap::new(), } } diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES index 04ff5564a..8deeefba6 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES +++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES @@ -21,7 +21,11 @@ base.ts common.ts configuration.ts index.ts +models/activated-skill.ts +models/agent-mcp-tool-summary.ts models/agent-permissions.ts +models/agent-skill-activation-source.ts +models/agent-skill-summary.ts models/aggregate-billing-totals.ts models/aggregate-billing.ts models/api-question-option.ts @@ -162,7 +166,11 @@ models/manifest-goal.ts models/manifest-target.ts models/manifest-workflow-config.ts models/manifest-workflow.ts +models/mcp-server-projection.ts models/mcp-server-settings.ts +models/mcp-server-status-failed.ts +models/mcp-server-status-ready.ts +models/mcp-server-status.ts models/mcp-transport-http.ts models/mcp-transport-sandbox.ts models/mcp-transport-stdio.ts @@ -253,6 +261,7 @@ models/pull-request-user.ts models/pull-request.ts models/question-type.ts models/reasoning-effort-feature.ts +models/reasoning-effort.ts models/related-workflow-diagnostic.ts models/render-workflow-graph-direction.ts models/render-workflow-graph-format.ts @@ -377,11 +386,13 @@ models/session-record.ts models/session-status.ts models/session-summary.ts models/session-turn.ts +models/skills-projection.ts models/slack-integration-settings.ts models/ssh-access-request.ts models/ssh-access-response.ts models/stage-completion.ts models/stage-handler.ts +models/stage-model-usage.ts models/stage-outcome.ts models/stage-projection.ts models/stage-state.ts @@ -390,6 +401,12 @@ models/stage-timing.ts models/start-record.ts models/start-run-request.ts models/steer-run-request.ts +models/sub-agent-projection.ts +models/sub-agent-status-closed.ts +models/sub-agent-status-completed.ts +models/sub-agent-status-failed.ts +models/sub-agent-status-running.ts +models/sub-agent-status.ts models/submit-answer-multi-selected-request.ts models/submit-answer-no-request.ts models/submit-answer-request.ts diff --git a/lib/packages/fabro-api-client/src/models/activated-skill.ts b/lib/packages/fabro-api-client/src/models/activated-skill.ts new file mode 100644 index 000000000..2a8cd0a07 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/activated-skill.ts @@ -0,0 +1,26 @@ +/* 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 { AgentSkillActivationSource } from './agent-skill-activation-source'; + +/** + * One observed agent skill activation. + */ +export interface ActivatedSkill { + 'name': string; + 'source': AgentSkillActivationSource; +} diff --git a/lib/packages/fabro-api-client/src/models/agent-mcp-tool-summary.ts b/lib/packages/fabro-api-client/src/models/agent-mcp-tool-summary.ts new file mode 100644 index 000000000..294ed6e81 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/agent-mcp-tool-summary.ts @@ -0,0 +1,23 @@ +/* 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. + */ + + + +/** + * Summary of one tool exposed by an MCP server. + */ +export interface AgentMcpToolSummary { + 'name': string; + 'original_name': string; +} diff --git a/lib/packages/fabro-api-client/src/models/agent-skill-activation-source.ts b/lib/packages/fabro-api-client/src/models/agent-skill-activation-source.ts new file mode 100644 index 000000000..334638e4c --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/agent-skill-activation-source.ts @@ -0,0 +1,26 @@ +/* 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. + */ + + + +/** + * Source that activated an agent skill. + */ + +export const AgentSkillActivationSource = { + SLASH: 'slash', + TOOL: 'tool' +} as const; + +export type AgentSkillActivationSource = typeof AgentSkillActivationSource[keyof typeof AgentSkillActivationSource]; diff --git a/lib/packages/fabro-api-client/src/models/agent-skill-summary.ts b/lib/packages/fabro-api-client/src/models/agent-skill-summary.ts new file mode 100644 index 000000000..c9a3f101c --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/agent-skill-summary.ts @@ -0,0 +1,23 @@ +/* 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. + */ + + + +/** + * Summary of an available agent skill. + */ +export interface AgentSkillSummary { + 'name': string; + 'description': string; +} diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index ae17c6ae8..8aeb3aa6e 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -1,4 +1,8 @@ +export * from './activated-skill'; +export * from './agent-mcp-tool-summary'; export * from './agent-permissions'; +export * from './agent-skill-activation-source'; +export * from './agent-skill-summary'; export * from './aggregate-billing'; export * from './aggregate-billing-totals'; export * from './api-question'; @@ -138,7 +142,11 @@ export * from './manifest-goal'; export * from './manifest-target'; export * from './manifest-workflow'; export * from './manifest-workflow-config'; +export * from './mcp-server-projection'; export * from './mcp-server-settings'; +export * from './mcp-server-status'; +export * from './mcp-server-status-failed'; +export * from './mcp-server-status-ready'; export * from './mcp-transport'; export * from './mcp-transport-http'; export * from './mcp-transport-sandbox'; @@ -354,6 +362,7 @@ export * from './session-record'; export * from './session-status'; export * from './session-summary'; export * from './session-turn'; +export * from './skills-projection'; export * from './slack-integration-settings'; export * from './ssh-access-request'; export * from './ssh-access-response'; @@ -368,6 +377,12 @@ export * from './stage-timing'; export * from './start-record'; export * from './start-run-request'; export * from './steer-run-request'; +export * from './sub-agent-projection'; +export * from './sub-agent-status'; +export * from './sub-agent-status-closed'; +export * from './sub-agent-status-completed'; +export * from './sub-agent-status-failed'; +export * from './sub-agent-status-running'; export * from './submit-answer-multi-selected-request'; export * from './submit-answer-no-request'; export * from './submit-answer-request'; diff --git a/lib/packages/fabro-api-client/src/models/mcp-server-projection.ts b/lib/packages/fabro-api-client/src/models/mcp-server-projection.ts new file mode 100644 index 000000000..d603dadbe --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/mcp-server-projection.ts @@ -0,0 +1,27 @@ +/* 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 { McpServerStatus } from './mcp-server-status'; + +/** + * Projected state for one MCP server observed by an agent stage. + */ +export interface McpServerProjection { + 'server_name': string; + 'tool_count': number; + 'status': McpServerStatus; +} diff --git a/lib/packages/fabro-api-client/src/models/mcp-server-status-failed.ts b/lib/packages/fabro-api-client/src/models/mcp-server-status-failed.ts new file mode 100644 index 000000000..acee4291f --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/mcp-server-status-failed.ts @@ -0,0 +1,26 @@ +/* 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. + */ + + + +export interface McpServerStatusFailed { + 'kind': McpServerStatusFailedKindEnum; + 'error': string; +} + +export const McpServerStatusFailedKindEnum = { + FAILED: 'failed' +} as const; + +export type McpServerStatusFailedKindEnum = typeof McpServerStatusFailedKindEnum[keyof typeof McpServerStatusFailedKindEnum]; diff --git a/lib/packages/fabro-api-client/src/models/mcp-server-status-ready.ts b/lib/packages/fabro-api-client/src/models/mcp-server-status-ready.ts new file mode 100644 index 000000000..ab82276ed --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/mcp-server-status-ready.ts @@ -0,0 +1,29 @@ +/* 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 { AgentMcpToolSummary } from './agent-mcp-tool-summary'; + +export interface McpServerStatusReady { + 'kind': McpServerStatusReadyKindEnum; + 'tools': Array; +} + +export const McpServerStatusReadyKindEnum = { + READY: 'ready' +} as const; + +export type McpServerStatusReadyKindEnum = typeof McpServerStatusReadyKindEnum[keyof typeof McpServerStatusReadyKindEnum]; diff --git a/lib/packages/fabro-api-client/src/models/mcp-server-status.ts b/lib/packages/fabro-api-client/src/models/mcp-server-status.ts new file mode 100644 index 000000000..50af943f0 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/mcp-server-status.ts @@ -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. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { AgentMcpToolSummary } from './agent-mcp-tool-summary'; +// May contain unused imports in some cases +// @ts-ignore +import type { McpServerStatusFailed } from './mcp-server-status-failed'; +// May contain unused imports in some cases +// @ts-ignore +import type { McpServerStatusReady } from './mcp-server-status-ready'; + +/** + * @type McpServerStatus + * Projected MCP server readiness status. + */ +export type McpServerStatus = { kind: 'failed' } & McpServerStatusFailed | { kind: 'ready' } & McpServerStatusReady; diff --git a/lib/packages/fabro-api-client/src/models/run-projection.ts b/lib/packages/fabro-api-client/src/models/run-projection.ts index 0ab7b799f..31a026a51 100644 --- a/lib/packages/fabro-api-client/src/models/run-projection.ts +++ b/lib/packages/fabro-api-client/src/models/run-projection.ts @@ -43,9 +43,6 @@ import type { StageProjection } from './stage-projection'; // May contain unused imports in some cases // @ts-ignore import type { StartRecord } from './start-record'; -// May contain unused imports in some cases -// @ts-ignore -import type { TodoListProjection } from './todo-list-projection'; /** * Raw internal run projection derived from the event log. @@ -79,10 +76,6 @@ export interface RunProjection { 'pull_request'?: PullRequestLink | null; 'superseded_by'?: string | null; 'pending_interviews': { [key: string]: PendingInterviewRecord; }; - /** - * Projected todo / task lists, keyed by `list_id` (`openai_plan:` or `anthropic_tasks:`). Built by replaying `todo.created`, `todo.updated`, and `todo.deleted` events. - */ - 'todos_by_list'?: { [key: string]: TodoListProjection; }; /** * Map from StageId (`node_id@visit`) to stage projection data. */ diff --git a/lib/packages/fabro-api-client/src/models/run-stage.ts b/lib/packages/fabro-api-client/src/models/run-stage.ts index 4fa5dce2e..2e25d4c55 100644 --- a/lib/packages/fabro-api-client/src/models/run-stage.ts +++ b/lib/packages/fabro-api-client/src/models/run-stage.ts @@ -49,9 +49,6 @@ export interface RunStage { * 1-based visit count; bumped each time the workflow re-enters this node. */ 'visit': number; - /** - * Provider, model, and request controls recorded for the latest stage attempt. - */ 'provider_used'?: StageModelUsage | null; /** * Wall-clock time the latest attempt of this stage started, if known. diff --git a/lib/packages/fabro-api-client/src/models/skills-projection.ts b/lib/packages/fabro-api-client/src/models/skills-projection.ts new file mode 100644 index 000000000..897c4b4af --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/skills-projection.ts @@ -0,0 +1,29 @@ +/* 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 { ActivatedSkill } from './activated-skill'; +// May contain unused imports in some cases +// @ts-ignore +import type { AgentSkillSummary } from './agent-skill-summary'; + +/** + * Agent skills discovered and activated during a stage. + */ +export interface SkillsProjection { + 'available': Array; + 'activated': Array; +} diff --git a/lib/packages/fabro-api-client/src/models/stage-model-usage.ts b/lib/packages/fabro-api-client/src/models/stage-model-usage.ts index 56461f455..8ba389a8a 100644 --- a/lib/packages/fabro-api-client/src/models/stage-model-usage.ts +++ b/lib/packages/fabro-api-client/src/models/stage-model-usage.ts @@ -24,6 +24,9 @@ import type { ReasoningEffort } from './reasoning-effort'; * Provider, model, and request-control metadata recorded for a stage attempt. */ export interface StageModelUsage { + /** + * Source of the stage\'s model usage metadata. + */ 'mode': string; 'provider'?: string | null; 'model'?: string | null; diff --git a/lib/packages/fabro-api-client/src/models/stage-projection.ts b/lib/packages/fabro-api-client/src/models/stage-projection.ts index b915b2861..cdec8e984 100644 --- a/lib/packages/fabro-api-client/src/models/stage-projection.ts +++ b/lib/packages/fabro-api-client/src/models/stage-projection.ts @@ -24,6 +24,12 @@ import type { BillingModelRef } from './billing-model-ref'; import type { CommandTermination } from './command-termination'; // May contain unused imports in some cases // @ts-ignore +import type { McpServerProjection } from './mcp-server-projection'; +// May contain unused imports in some cases +// @ts-ignore +import type { SkillsProjection } from './skills-projection'; +// May contain unused imports in some cases +// @ts-ignore import type { StageCompletion } from './stage-completion'; // May contain unused imports in some cases // @ts-ignore @@ -34,6 +40,12 @@ import type { StageState } from './stage-state'; // May contain unused imports in some cases // @ts-ignore import type { StageTiming } from './stage-timing'; +// May contain unused imports in some cases +// @ts-ignore +import type { SubAgentProjection } from './sub-agent-projection'; +// May contain unused imports in some cases +// @ts-ignore +import type { TodoListProjection } from './todo-list-projection'; /** * Observable projection data for one workflow stage execution. @@ -43,9 +55,6 @@ export interface StageProjection { 'prompt'?: string | null; 'response'?: string | null; 'completion'?: StageCompletion | null; - /** - * Provider and model metadata recorded for the stage attempt. - */ 'provider_used'?: StageModelUsage | null; 'diff'?: string | null; /** @@ -71,6 +80,19 @@ export interface StageProjection { 'timing'?: StageTiming | null; 'usage': BilledTokenCounts; 'model'?: BillingModelRef | null; + 'todos'?: TodoListProjection | null; + /** + * Subagents spawned by this stage, in replay/insertion order. + */ + 'subagents'?: Array; + /** + * Agent skills discovered and activated during this stage. + */ + 'skills'?: SkillsProjection; + /** + * MCP servers observed by this stage. + */ + 'mcp_servers'?: Array; /** * Lifecycle state of the stage projection. */ diff --git a/lib/packages/fabro-api-client/src/models/sub-agent-projection.ts b/lib/packages/fabro-api-client/src/models/sub-agent-projection.ts new file mode 100644 index 000000000..c2576d069 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/sub-agent-projection.ts @@ -0,0 +1,28 @@ +/* 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 { SubAgentStatus } from './sub-agent-status'; + +/** + * Current projected state for one subagent spawned by an agent stage. + */ +export interface SubAgentProjection { + 'agent_id': string; + 'depth': number; + 'task': string; + 'status': SubAgentStatus; +} diff --git a/lib/packages/fabro-api-client/src/models/sub-agent-status-closed.ts b/lib/packages/fabro-api-client/src/models/sub-agent-status-closed.ts new file mode 100644 index 000000000..d4c7bb5a6 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/sub-agent-status-closed.ts @@ -0,0 +1,25 @@ +/* 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. + */ + + + +export interface SubAgentStatusClosed { + 'kind': SubAgentStatusClosedKindEnum; +} + +export const SubAgentStatusClosedKindEnum = { + CLOSED: 'closed' +} as const; + +export type SubAgentStatusClosedKindEnum = typeof SubAgentStatusClosedKindEnum[keyof typeof SubAgentStatusClosedKindEnum]; diff --git a/lib/packages/fabro-api-client/src/models/sub-agent-status-completed.ts b/lib/packages/fabro-api-client/src/models/sub-agent-status-completed.ts new file mode 100644 index 000000000..2250ee02a --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/sub-agent-status-completed.ts @@ -0,0 +1,27 @@ +/* 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. + */ + + + +export interface SubAgentStatusCompleted { + 'kind': SubAgentStatusCompletedKindEnum; + 'success': boolean; + 'turns_used': number; +} + +export const SubAgentStatusCompletedKindEnum = { + COMPLETED: 'completed' +} as const; + +export type SubAgentStatusCompletedKindEnum = typeof SubAgentStatusCompletedKindEnum[keyof typeof SubAgentStatusCompletedKindEnum]; diff --git a/lib/packages/fabro-api-client/src/models/sub-agent-status-failed.ts b/lib/packages/fabro-api-client/src/models/sub-agent-status-failed.ts new file mode 100644 index 000000000..a9eed6831 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/sub-agent-status-failed.ts @@ -0,0 +1,26 @@ +/* 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. + */ + + + +export interface SubAgentStatusFailed { + 'kind': SubAgentStatusFailedKindEnum; + 'error': any; +} + +export const SubAgentStatusFailedKindEnum = { + FAILED: 'failed' +} as const; + +export type SubAgentStatusFailedKindEnum = typeof SubAgentStatusFailedKindEnum[keyof typeof SubAgentStatusFailedKindEnum]; diff --git a/lib/packages/fabro-api-client/src/models/sub-agent-status-running.ts b/lib/packages/fabro-api-client/src/models/sub-agent-status-running.ts new file mode 100644 index 000000000..79063cbe0 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/sub-agent-status-running.ts @@ -0,0 +1,25 @@ +/* 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. + */ + + + +export interface SubAgentStatusRunning { + 'kind': SubAgentStatusRunningKindEnum; +} + +export const SubAgentStatusRunningKindEnum = { + RUNNING: 'running' +} as const; + +export type SubAgentStatusRunningKindEnum = typeof SubAgentStatusRunningKindEnum[keyof typeof SubAgentStatusRunningKindEnum]; diff --git a/lib/packages/fabro-api-client/src/models/sub-agent-status.ts b/lib/packages/fabro-api-client/src/models/sub-agent-status.ts new file mode 100644 index 000000000..630717c08 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/sub-agent-status.ts @@ -0,0 +1,33 @@ +/* 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 { SubAgentStatusClosed } from './sub-agent-status-closed'; +// May contain unused imports in some cases +// @ts-ignore +import type { SubAgentStatusCompleted } from './sub-agent-status-completed'; +// May contain unused imports in some cases +// @ts-ignore +import type { SubAgentStatusFailed } from './sub-agent-status-failed'; +// May contain unused imports in some cases +// @ts-ignore +import type { SubAgentStatusRunning } from './sub-agent-status-running'; + +/** + * @type SubAgentStatus + * Projected lifecycle status for a subagent. + */ +export type SubAgentStatus = { kind: 'closed' } & SubAgentStatusClosed | { kind: 'completed' } & SubAgentStatusCompleted | { kind: 'failed' } & SubAgentStatusFailed | { kind: 'running' } & SubAgentStatusRunning;