diff --git a/docs/internal/events.md b/docs/internal/events.md index add8b9411..833c66fc0 100644 --- a/docs/internal/events.md +++ b/docs/internal/events.md @@ -1660,9 +1660,9 @@ Pebble's `RouteFailover`, `McpServerReady`, `McpServerFailed`, and `McpServerDisconnected` events, stored verbatim with pebble's envelope in `properties` like every other pebble event. Fabro also mirrors each onto its own `agent.failover`, `agent.mcp.ready`, `agent.mcp.failed`, and -`agent.mcp.disconnected`, which the store folds into `StageProjection`'s -`mcp_servers`; the pebble events feed `StageProjection.agent`. The mirrors -go once every reader is on `agent`. +`agent.mcp.disconnected`. The stage view reads MCP state from +`StageProjection.agent`, which the pebble events feed; the mirrors change +nothing on the stage any more and go next. ### `agent.route.failover.stopped` diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index 3757ae924..12616a832 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -11029,8 +11029,11 @@ components: example: "2026-05-23T12:34:56Z" event_seq: type: ["integer", "null"] - format: uint32 - minimum: 1 + format: uint64 + minimum: 0 + description: >- + The coding agent's own event sequence for the snapshot, when it + carried one; not the run event sequence. example: 42 breakdown: type: array @@ -11170,23 +11173,6 @@ components: oneOf: - $ref: "#/components/schemas/BillingModelRef" - type: "null" - todos: - oneOf: - - $ref: "#/components/schemas/TodoListProjection" - - type: "null" - description: | - Todo / task list owned by this stage's root agent session. OpenAI - child sessions have separate per-session plans that do not appear - here. Anthropic task lists are root-scoped and shared with child - sessions, so child mutations of that shared list do appear here. - 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. permission_level: oneOf: - $ref: "#/components/schemas/PermissionLevel" @@ -11199,16 +11185,6 @@ components: Tool parameter schemas are intentionally omitted from this projection. items: $ref: "#/components/schemas/ToolSummary" - mcp_servers: - type: array - description: MCP servers observed by this stage. - items: - $ref: "#/components/schemas/McpServerProjection" - context_window: - oneOf: - - $ref: "#/components/schemas/ContextWindowSnapshot" - - type: "null" - description: Latest content-free context-window snapshot for this agent stage. inference: oneOf: - $ref: "#/components/schemas/StageInferenceProjection" @@ -11340,102 +11316,6 @@ components: - text - tool_call - 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/SkillSummary" - activated: - type: array - items: - $ref: "#/components/schemas/ActivatedSkill" - SkillSummary: description: Summary of an available agent skill. type: object @@ -11448,18 +11328,6 @@ components: description: type: string - ActivatedSkill: - description: One observed agent skill activation. - type: object - required: - - name - - source - properties: - name: - type: string - source: - $ref: "#/components/schemas/SkillActivationSource" - SkillActivationSource: description: Source that activated an agent skill. type: string @@ -11556,91 +11424,6 @@ components: type: string enum: [read, write, shell, subagent, other] - McpServerProjection: - description: Projected state for one MCP server observed by an agent stage. - type: object - required: - - server_name - - tool_count - - status - - invoked - properties: - server_name: - type: string - tool_count: - type: integer - minimum: 0 - status: - $ref: "#/components/schemas/McpServerStatus" - invoked: - type: boolean - description: True once the agent has invoked at least one tool from this server during the stage. - - McpServerStatus: - description: Projected MCP server readiness status. - oneOf: - - $ref: "#/components/schemas/McpServerStatusReady" - - $ref: "#/components/schemas/McpServerStatusFailed" - - $ref: "#/components/schemas/McpServerStatusDisconnected" - discriminator: - propertyName: kind - mapping: - ready: "#/components/schemas/McpServerStatusReady" - failed: "#/components/schemas/McpServerStatusFailed" - disconnected: "#/components/schemas/McpServerStatusDisconnected" - - 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 - - McpServerStatusDisconnected: - description: The server was ready and then its connection closed during the stage; its tools fail until the session ends. - type: object - required: - - kind - - error - properties: - kind: - type: string - enum: [disconnected] - error: - type: string - description: What closed the connection, as the client observed it. - - 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 - AgentSessionProjection: description: >- The coding agent's fold of one stage's event stream: token counts and diff --git a/lib/apps/fabro-server/src/server/handler/runs.rs b/lib/apps/fabro-server/src/server/handler/runs.rs index b2de8d3c6..4020d29eb 100644 --- a/lib/apps/fabro-server/src/server/handler/runs.rs +++ b/lib/apps/fabro-server/src/server/handler/runs.rs @@ -1772,7 +1772,11 @@ async fn get_run_stage_context_window( .into_response(); } - let Some(snapshot) = stage.context_window.as_ref() else { + let Some(snapshot) = stage + .agent + .as_ref() + .and_then(|agent| agent.context_window.as_ref()) + else { return Json(StageContextWindow::unavailable( stage_id, StageContextWindowUnavailableReason::NotObserved, @@ -1789,7 +1793,11 @@ async fn get_run_stage_context_window( } fn is_agent_context_window_stage(stage: &StageProjection) -> bool { - if stage.context_window.is_some() { + if stage + .agent + .as_ref() + .is_some_and(|agent| agent.context_window.is_some()) + { return true; } if stage.handler == Some(StageHandler::Agent) { diff --git a/lib/components/fabro-store/src/run_state.rs b/lib/components/fabro-store/src/run_state.rs index df6d0184b..e9cbf1390 100644 --- a/lib/components/fabro-store/src/run_state.rs +++ b/lib/components/fabro-store/src/run_state.rs @@ -9,18 +9,16 @@ use fabro_types::run_event::{ }; use fabro_types::settings::run::RunEnvironmentSettings; use fabro_types::{ - ActivatedSkill, AgentControlState, AskFabro, BilledModelUsage, BilledTokenCounts, Checkpoint, - CheckpointRecord, CommandTermination, Conclusion, EventBody, FailureCategory, FailureSignature, - InterviewQuestionRecord, McpServerProjection, McpServerStatus, ModelRef, Outcome, - PendingInterviewRecord, PendingReason, PullRequestCreation, PullRequestCreationStatus, - PullRequestLink, RepositoryRef, Run, RunApproval, RunApprovalState, RunBillingSummary, - RunControlAction, RunDiff, RunEvent, RunId, RunLifecycle, RunLinks, RunModel, RunOrigin, - RunProjection, RunSandbox, RunSandboxFailure, RunSandboxInstance, RunSandboxPlan, - RunSandboxRuntime, RunSize, RunSpec, RunStatus, RunTimestamps, SandboxProviderKind, - StageCompletion, StageHandler, StageId, StageInferenceProjection, StageModelUsage, - StageOutcome, StageProjection, StageState, StartRecord, SubAgentProjection, SubAgentStatus, - TodoCreatedProps, TodoDeletedProps, TodoListKind, TodoListProjection, TodoProjection, - TodoUpdatedProps, WorkflowRef, billing_rollup, first_event_seq, timing, + AgentControlState, AskFabro, BilledModelUsage, BilledTokenCounts, Checkpoint, CheckpointRecord, + CommandTermination, Conclusion, EventBody, FailureCategory, FailureSignature, + InterviewQuestionRecord, ModelRef, Outcome, PendingInterviewRecord, PendingReason, + PullRequestCreation, PullRequestCreationStatus, PullRequestLink, RepositoryRef, Run, + RunApproval, RunApprovalState, RunBillingSummary, RunControlAction, RunDiff, RunEvent, RunId, + RunLifecycle, RunLinks, RunModel, RunOrigin, RunProjection, RunSandbox, RunSandboxFailure, + RunSandboxInstance, RunSandboxPlan, RunSandboxRuntime, RunSize, RunSpec, RunStatus, + RunTimestamps, SandboxProviderKind, StageCompletion, StageHandler, StageId, + StageInferenceProjection, StageModelUsage, StageOutcome, StageProjection, StageState, + StartRecord, WorkflowRef, billing_rollup, first_event_seq, timing, }; use fabro_util::error::render_compact_with_causes; use lithos_llm::catalog::{ModelId, ProviderId}; @@ -707,41 +705,6 @@ impl RunProjectionReducer for RunProjection { stage.state = StageState::from(props.status); stage.agent_control = AgentControlState::Running; } - 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(), - }, - invoked: false, - }); - } - 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(), - }, - invoked: false, - }); - } - EventBody::AgentMcpDisconnected(props) => { - let Some(stage) = stage_at_stored_or_visit(self, stored, props.visit, event.seq) - else { - return Ok(()); - }; - mark_mcp_server_disconnected(stage, &props.server_name, &props.error); - } _ => {} } @@ -779,22 +742,13 @@ fn apply_agent_event( reason = "pebble's event vocabulary is non-exhaustive and only some events project" )] match props.coding_event() { - CodingEvent::AssistantMessage { - model, - context_window, - .. - } => { + CodingEvent::AssistantMessage { model, .. } => { let Some(stage) = stage_at_stored_or_visit(state, stored, visit, seq) else { return; }; if let Some(model) = stage_model_ref(stage, model) { stage.model = Some(model); } - if let Some(context_window) = context_window { - let mut context_window = context_window.clone(); - context_window.event_seq = Some(u64::from(seq)); - stage.context_window = Some(context_window); - } close_inference_bracket(state, stored, visit, seq, ts); } CodingEvent::LlmRequestStarted { requested_model } => { @@ -838,105 +792,6 @@ fn apply_agent_event( }; stage.agent_control = AgentControlState::Running; } - CodingEvent::TodoCreated(todo) => { - if !should_project_root_agent_todo_event(stored, todo.list_kind) { - return; - } - let Some(stage) = stage_at_stored_or_visit(state, stored, visit, seq) else { - return; - }; - apply_todo_created(stage, todo); - } - CodingEvent::TodoUpdated(todo) => { - if !should_project_root_agent_todo_event(stored, todo.list_kind) { - return; - } - let Some(stage) = stage_at_stored_or_visit(state, stored, visit, seq) else { - return; - }; - apply_todo_updated(stage, todo); - } - CodingEvent::TodoDeleted(todo) => { - if !should_project_root_agent_todo_event(stored, todo.list_kind) { - return; - } - let Some(stage) = stage_at_stored_or_visit(state, stored, visit, seq) else { - return; - }; - apply_todo_deleted(stage, todo); - } - CodingEvent::SubAgentSpawned { - agent_id, - depth, - task, - .. - } => { - let Some(stage) = stage_at_stored_or_visit(state, stored, visit, seq) else { - return; - }; - stage.subagents.push(SubAgentProjection { - agent_id: agent_id.clone(), - depth: *depth, - task: task.clone(), - status: SubAgentStatus::Running, - }); - } - // A reused subagent stays one projected row: the spawn task and - // generation 1 identify it, and every later generation only moves - // its status. The per-turn task and generation stay in the event - // log for consumers that need each turn. - CodingEvent::SubAgentTurnStarted { agent_id, .. } => { - set_subagent_status(state, stored, visit, seq, agent_id, SubAgentStatus::Running); - } - CodingEvent::SubAgentCompleted { - agent_id, - success, - turns_used, - .. - } => { - set_subagent_status( - state, - stored, - visit, - seq, - agent_id, - SubAgentStatus::Completed { - success: *success, - turns_used: *turns_used, - }, - ); - } - CodingEvent::SubAgentFailed { - agent_id, error, .. - } => { - let error = serde_json::to_value(error).unwrap_or_default(); - set_subagent_status( - state, - stored, - visit, - seq, - agent_id, - SubAgentStatus::Failed { error }, - ); - } - CodingEvent::SubAgentClosed { agent_id, .. } => { - set_subagent_status(state, stored, visit, seq, agent_id, SubAgentStatus::Closed); - } - CodingEvent::SkillsDiscovered { skills, .. } => { - let Some(stage) = stage_at_stored_or_visit(state, stored, visit, seq) else { - return; - }; - stage.skills.available.clone_from(skills); - } - CodingEvent::SkillActivated { skill_name, source } => { - let Some(stage) = stage_at_stored_or_visit(state, stored, visit, seq) else { - return; - }; - stage.skills.activated.push(ActivatedSkill { - name: skill_name.clone(), - source: *source, - }); - } CodingEvent::ToolCallStarted { tool_name, tool_call_id, @@ -957,15 +812,6 @@ fn apply_agent_event( { tool.invoked = true; } - if let Some(server) = mcp_server_from_tool_name(tool_name) { - if let Some(projection) = stage - .mcp_servers - .iter_mut() - .find(|p| mcp_name_eq(&p.server_name, server)) - { - projection.invoked = true; - } - } // A subagent's tools run inside the root session's tool call, // so the root batch already covers them. Timing them again // would double-count that span. @@ -1022,172 +868,6 @@ fn stage_provider(stage: &StageProjection) -> Option { .or_else(|| stage.model.as_ref().map(|model| model.provider.clone())) } -/// Decide whether a TODO event should mutate -/// `StageProjection.root_agent_todos`. -/// -/// OpenAI plan lists are scoped per agent session (`openai_plan:`), -/// so a child/subagent session emits its own list events on the same stage. -/// The root-agent projection excludes those child plans, while the underlying -/// events remain in the run event log. Kimi todo lists -/// (`kimi_todos:`) are scoped the same way. Anthropic task lists -/// are root-scoped (`anthropic_tasks:`) and intentionally -/// shared with subagents, so they always project. -fn should_project_root_agent_todo_event(stored: &RunEvent, list_kind: TodoListKind) -> bool { - // `TodoListKind` is non-exhaustive: a list kind this build does not know - // is treated as session-scoped, the conservative reading. - matches!(list_kind, TodoListKind::AnthropicTasks) || stored.parent_session_id.is_none() -} - -fn apply_todo_created(stage: &mut StageProjection, props: &TodoCreatedProps) { - if stage - .root_agent_todos - .as_ref() - .is_none_or(|list| list.list_id != props.list_id || list.kind != props.list_kind) - { - stage.root_agent_todos = Some(TodoListProjection::new( - props.list_kind, - props.list_id.clone(), - )); - } - let list = stage - .root_agent_todos - .as_mut() - .expect("todo list was just inserted"); - list.upsert(TodoProjection { - id: props.todo_id.clone(), - status: props.status, - order: props.order, - subject: props.subject.clone(), - description: props.description.clone(), - active_form: props.active_form.clone(), - owner: props.owner.clone(), - blocks: props.blocks.clone(), - blocked_by: props.blocked_by.clone(), - metadata: props.metadata.clone(), - }); -} - -fn apply_todo_updated(stage: &mut StageProjection, props: &TodoUpdatedProps) { - if let Some(list) = stage - .root_agent_todos - .as_mut() - .filter(|list| list.list_id == props.list_id) - { - list.apply_patch(&props.todo_id, props); - } -} - -fn apply_todo_deleted(stage: &mut StageProjection, props: &TodoDeletedProps) { - let Some(list) = stage - .root_agent_todos - .as_mut() - .filter(|list| list.list_id == props.list_id) - else { - return; - }; - list.remove(&props.todo_id); - if list.items.is_empty() { - stage.root_agent_todos = None; - } -} - -/// Move an already-projected subagent to a new lifecycle status. Every -/// subagent event after the spawn updates the same row, so reuse shows one -/// agent returning to running rather than a second agent appearing. -fn set_subagent_status( - state: &mut RunProjection, - stored: &RunEvent, - visit: u32, - seq: u32, - agent_id: &str, - status: SubAgentStatus, -) { - let Some(stage) = stage_at_stored_or_visit(state, stored, visit, seq) else { - return; - }; - if let Some(subagent) = subagent_mut(stage, agent_id) { - subagent.status = status; - } -} - -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, mut server: McpServerProjection) { - if let Some(existing) = stage - .mcp_servers - .iter_mut() - .find(|existing| existing.server_name == server.server_name) - { - // Status/tool-count may flip (Ready → Failed across reconnects); keep - // the sticky `invoked` flag so a server still reads as "used" after - // its ready/failed state changes. - server.invoked = server.invoked || existing.invoked; - *existing = server; - } else { - stage.mcp_servers.push(server); - } -} - -/// Move a server the stage saw come up to `Disconnected`. Its tool count and -/// sticky `invoked` flag stay: the tools existed and may have been used, they -/// only fail from here on. A disconnect for a server the stage never saw come -/// up is still recorded, without tools. -fn mark_mcp_server_disconnected(stage: &mut StageProjection, server_name: &str, error: &str) { - let status = McpServerStatus::Disconnected { - error: error.to_string(), - }; - if let Some(existing) = stage - .mcp_servers - .iter_mut() - .find(|existing| existing.server_name == server_name) - { - existing.status = status; - } else { - stage.mcp_servers.push(McpServerProjection { - server_name: server_name.to_string(), - tool_count: 0, - status, - invoked: false, - }); - } -} - -/// Extract the `` segment from an `mcp____` qualified -/// tool name. Returns `None` for non-MCP tools or malformed names. -fn mcp_server_from_tool_name(tool_name: &str) -> Option<&str> { - let rest = tool_name.strip_prefix("mcp__")?; - let idx = rest.find("__")?; - let server = &rest[..idx]; - (!server.is_empty()).then_some(server) -} - -/// Match an MCP server projection name against a server segment parsed from a -/// qualified tool name. Tool names use `fabro_mcp::qualified_tool_name`, which -/// sanitizes non-alphanumeric characters in the server name; normalize the -/// stored projection name the same way before comparing. -fn mcp_name_eq(projection_name: &str, parsed_from_tool: &str) -> bool { - fn normalize(s: &str) -> String { - s.chars() - .map(|c| { - if c.is_alphanumeric() || c == '_' { - c - } else { - '_' - } - }) - .collect() - } - normalize(projection_name) == parsed_from_tool -} - fn projection_from_created(event: &EventEnvelope) -> Result { let stored = &event.event; let EventBody::RunCreated(props) = &stored.body else { @@ -1849,29 +1529,25 @@ mod tests { AgentAcpCancelledProps, AgentAcpCompletedProps, AgentAcpStartedProps, AgentAcpTimedOutProps, AgentEventProps, AgentMcpDisconnectedProps, AgentMcpFailedProps, AgentMcpReadyProps, AgentMcpToolSummary, AgentSessionActivatedProps, - AgentSessionDeactivatedProps, AgentToolsAvailableProps, CheckpointCompletedProps, - InterviewCompletedProps, InterviewOption, InterviewStartedProps, - ParallelBranchCompletedProps, ParallelBranchStartedProps, RunCompletedProps, - RunControlEffectProps, StageCompletedProps, StageFailedProps, StagePromptProps, - StageRetryingProps, StageStartedProps, + AgentSessionDeactivatedProps, CheckpointCompletedProps, InterviewCompletedProps, + InterviewOption, InterviewStartedProps, ParallelBranchCompletedProps, + ParallelBranchStartedProps, RunCompletedProps, RunControlEffectProps, StageCompletedProps, + StageFailedProps, StagePromptProps, StageRetryingProps, StageStartedProps, }; use fabro_types::settings::run::DockerfileSource; use fabro_types::{ AgentBackend, AgentControlState, AttrValue, AutomationRef, BilledModelUsage, BilledTokenCounts, BlobHash, BlockedReason, Checkpoint, CheckpointRecord, - CommandTermination, EventBody, FailureCategory, FailureDetail, FailureReason, Graph, - McpServerStatus, Node, Outcome, ParallelBranchId, PendingReason, PermissionLevel, - PullRequestCreationStatus, PullRequestLink, QuestionType, RunApprovalState, - RunBillingSummary, RunControlAction, RunDiff, RunEvent, RunSize, RunSpec, RunStatus, - SandboxProviderKind, StageHandler, StageModelUsage, StageOutcome, StageState, StageTiming, - SubAgentStatus, SuccessReason, WorkflowSettings, first_event_seq, fixtures, test_support, + CommandTermination, EventBody, FailureCategory, FailureDetail, FailureReason, Graph, Node, + Outcome, ParallelBranchId, PendingReason, PullRequestCreationStatus, PullRequestLink, + QuestionType, RunApprovalState, RunBillingSummary, RunControlAction, RunDiff, RunEvent, + RunSize, RunSpec, RunStatus, SandboxProviderKind, StageHandler, StageModelUsage, + StageOutcome, StageState, StageTiming, SuccessReason, WorkflowSettings, first_event_seq, + fixtures, test_support, }; use lithos_llm::types::{ReasoningEffort, Speed, TokenCounts}; use pebble_coding_agent::events::{ - CodingAgentEvent, CodingEvent, CompactionReason, ContextWindowBreakdownItem, - ContextWindowCategory, ContextWindowCountMethod, ContextWindowSnapshot, - ContextWindowStaleness, ContextWindowWarning, ErrorData, ErrorKind, SkillActivationSource, - SkillSummary, TokenUsage, ToolCategory, ToolSource, ToolSummary, + CodingAgentEvent, CodingEvent, CompactionReason, ErrorData, ErrorKind, TokenUsage, }; use pebble_coding_agent::tools::ToolOutputMetadata; use serde_json::json; @@ -6694,1283 +6370,6 @@ mod tests { assert_eq!(stage.timing.map(|t| t.wall_time_ms), None); } - mod todo_reducer { - use fabro_types::{ - TodoCreatedProps, TodoDeletedProps, TodoListKind, TodoListProjection, TodoStatus, - TodoUpdatedProps, - }; - - use super::*; - - fn stage_id() -> StageId { - StageId::new("code", 1) - } - - fn root_agent_todos<'a>( - state: &'a RunProjection, - stage_id: &StageId, - ) -> &'a TodoListProjection { - state - .stage(stage_id) - .and_then(|stage| stage.root_agent_todos.as_ref()) - .expect("root agent todos present") - } - - fn child_stage_event(seq: u32, body: EventBody, stage_id: StageId) -> EventEnvelope { - let mut event = test_stage_event(seq, body, stage_id); - event.event.session_id = Some(format!("child-session-{seq}")); - event.event.parent_session_id = Some("root-session".to_string()); - event - } - - fn created( - list: &str, - list_kind: TodoListKind, - id: &str, - order: u32, - subject: &str, - ) -> EventBody { - agent_body(CodingEvent::TodoCreated(TodoCreatedProps { - list_id: list.to_string(), - list_kind, - todo_id: id.to_string(), - status: TodoStatus::Pending, - order, - subject: subject.to_string(), - description: String::new(), - active_form: None, - owner: None, - blocks: Vec::new(), - blocked_by: Vec::new(), - metadata: BTreeMap::new(), - })) - } - - fn updated_status( - list: &str, - list_kind: TodoListKind, - id: &str, - status: TodoStatus, - ) -> EventBody { - agent_body(CodingEvent::TodoUpdated(TodoUpdatedProps { - list_id: list.to_string(), - list_kind, - todo_id: id.to_string(), - status: Some(status), - order: None, - subject: None, - description: None, - active_form: None, - owner: None, - add_blocks: None, - add_blocked_by: None, - metadata_patch: BTreeMap::new(), - })) - } - - fn deleted(list: &str, list_kind: TodoListKind, id: &str) -> EventBody { - agent_body(CodingEvent::TodoDeleted(TodoDeletedProps { - list_id: list.to_string(), - list_kind, - todo_id: id.to_string(), - })) - } - - #[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_stage_event( - 1, - created(list, TodoListKind::OpenAiPlan, "a", 0, "first"), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&test_stage_event( - 2, - created(list, TodoListKind::OpenAiPlan, "b", 1, "second"), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&test_stage_event( - 3, - updated_status(list, TodoListKind::OpenAiPlan, "a", TodoStatus::InProgress), - stage_id.clone(), - )) - .unwrap(); - - let projection = root_agent_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); - assert_eq!(projection.items[1].id, "b"); - } - - #[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_stage_event( - 1, - created(list, TodoListKind::OpenAiPlan, "a", 0, "first"), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&test_stage_event( - 2, - created(list, TodoListKind::OpenAiPlan, "b", 1, "second"), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&test_stage_event( - 3, - deleted(list, TodoListKind::OpenAiPlan, "a"), - stage_id.clone(), - )) - .unwrap(); - - let projection = root_agent_todos(&state, &stage_id); - assert_eq!(projection.items.len(), 1); - assert_eq!(projection.items[0].id, "b"); - } - - #[test] - 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_stage_event( - 1, - created("openai_plan:s1", TodoListKind::OpenAiPlan, "a", 0, "p1"), - plan_one.clone(), - )) - .unwrap(); - state - .apply_event(&test_stage_event( - 2, - created("openai_plan:s2", TodoListKind::OpenAiPlan, "a", 0, "p2"), - plan_two.clone(), - )) - .unwrap(); - state - .apply_event(&test_stage_event( - 3, - created( - "anthropic_tasks:s_root", - TodoListKind::AnthropicTasks, - "1", - 0, - "claude task", - ), - claude.clone(), - )) - .unwrap(); - - assert_eq!(root_agent_todos(&state, &plan_one).items[0].subject, "p1"); - assert_eq!(root_agent_todos(&state, &plan_two).items[0].subject, "p2"); - assert_eq!( - root_agent_todos(&state, &claude).items[0].subject, - "claude task" - ); - } - - #[test] - fn root_openai_plan_remains_projected_after_child_plan_events() { - let mut state = initialized_projection(); - let stage_id = stage_id(); - let root_list = "openai_plan:root_session"; - let child_list = "openai_plan:child_session"; - state - .apply_event(&test_stage_event( - 1, - created( - root_list, - TodoListKind::OpenAiPlan, - "root-a", - 0, - "root first", - ), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&test_stage_event( - 2, - created( - root_list, - TodoListKind::OpenAiPlan, - "root-b", - 1, - "root second", - ), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&child_stage_event( - 3, - created( - child_list, - TodoListKind::OpenAiPlan, - "child-a", - 0, - "child first", - ), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&child_stage_event( - 4, - created( - child_list, - TodoListKind::OpenAiPlan, - "child-b", - 1, - "child second", - ), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&test_stage_event( - 5, - updated_status( - root_list, - TodoListKind::OpenAiPlan, - "root-a", - TodoStatus::Completed, - ), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&test_stage_event( - 6, - updated_status( - root_list, - TodoListKind::OpenAiPlan, - "root-b", - TodoStatus::Completed, - ), - stage_id.clone(), - )) - .unwrap(); - - let projection = root_agent_todos(&state, &stage_id); - assert_eq!(projection.list_id, root_list); - assert_eq!(projection.kind, TodoListKind::OpenAiPlan); - assert_eq!(projection.items.len(), 2); - assert_eq!(projection.items[0].id, "root-a"); - assert_eq!(projection.items[0].status, TodoStatus::Completed); - assert_eq!(projection.items[1].id, "root-b"); - assert_eq!(projection.items[1].status, TodoStatus::Completed); - } - - #[test] - fn child_session_whole_lists_do_not_project_when_root_has_no_list() { - for (kind, child_list) in [ - (TodoListKind::OpenAiPlan, "openai_plan:child_session"), - (TodoListKind::KimiTodos, "kimi_todos:child_session"), - ] { - let mut state = initialized_projection(); - let stage_id = stage_id(); - state - .apply_event(&test_stage_event( - 1, - EventBody::StageStarted(started_props()), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&child_stage_event( - 2, - created(child_list, kind, "c-a", 0, "child work"), - stage_id.clone(), - )) - .unwrap(); - - let stage = state.stage(&stage_id).expect("stage projection present"); - assert!( - stage.root_agent_todos.is_none(), - "a child session's {kind} list must not become the stage's root list" - ); - } - } - - #[test] - fn root_openai_plan_projects_after_earlier_child_plan_event() { - let mut state = initialized_projection(); - let stage_id = stage_id(); - let root_list = "openai_plan:root_session"; - state - .apply_event(&test_stage_event( - 1, - EventBody::StageStarted(started_props()), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&child_stage_event( - 2, - created( - "openai_plan:child_session", - TodoListKind::OpenAiPlan, - "child-a", - 0, - "child work", - ), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&test_stage_event( - 3, - created( - root_list, - TodoListKind::OpenAiPlan, - "root-a", - 0, - "root work", - ), - stage_id.clone(), - )) - .unwrap(); - - let projection = root_agent_todos(&state, &stage_id); - assert_eq!(projection.list_id, root_list); - assert_eq!(projection.items.len(), 1); - assert_eq!(projection.items[0].subject, "root work"); - } - - #[test] - fn anthropic_child_session_task_events_still_project() { - let mut state = initialized_projection(); - let stage_id = stage_id(); - let list = "anthropic_tasks:root_session"; - state - .apply_event(&child_stage_event( - 1, - created( - list, - TodoListKind::AnthropicTasks, - "task-a", - 0, - "task first", - ), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&child_stage_event( - 2, - updated_status( - list, - TodoListKind::AnthropicTasks, - "task-a", - TodoStatus::Completed, - ), - stage_id.clone(), - )) - .unwrap(); - - let projection = root_agent_todos(&state, &stage_id); - assert_eq!(projection.list_id, list); - assert_eq!(projection.kind, TodoListKind::AnthropicTasks); - assert_eq!(projection.items.len(), 1); - assert_eq!(projection.items[0].id, "task-a"); - assert_eq!(projection.items[0].status, TodoStatus::Completed); - } - - #[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_stage_event( - 1, - created(list, TodoListKind::AnthropicTasks, "1", 0, "t"), - 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_stage_event( - 2, - agent_body(CodingEvent::TodoUpdated(TodoUpdatedProps { - list_id: list.to_string(), - list_kind: TodoListKind::AnthropicTasks, - todo_id: "1".to_string(), - status: None, - order: None, - subject: None, - description: None, - active_form: None, - owner: None, - add_blocks: None, - add_blocked_by: None, - metadata_patch: meta, - })), - stage_id.clone(), - )) - .unwrap(); - let mut delete = BTreeMap::new(); - delete.insert("k1".to_string(), serde_json::Value::Null); - state - .apply_event(&test_stage_event( - 3, - agent_body(CodingEvent::TodoUpdated(TodoUpdatedProps { - list_id: list.to_string(), - list_kind: TodoListKind::AnthropicTasks, - todo_id: "1".to_string(), - status: None, - order: None, - subject: None, - description: None, - active_form: None, - owner: None, - add_blocks: None, - add_blocked_by: None, - metadata_patch: delete, - })), - stage_id.clone(), - )) - .unwrap(); - - let todo = &root_agent_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 interrupt_settlement_and_steering_update_agent_control_projection() { - let mut state = initialized_projection(); - let stage_id = stage_id(); - - state - .apply_event(&test_stage_event( - 1, - agent_body(CodingEvent::RoundInterrupted { generation: 1 }), - stage_id.clone(), - )) - .unwrap(); - assert_eq!( - state.stage(&stage_id).unwrap().agent_control, - AgentControlState::WaitingForSteer - ); - - state - .apply_event(&test_stage_event( - 2, - agent_body(CodingEvent::SteeringInjected { - text: "continue".to_string(), - content: None, - actor: None, - }), - stage_id.clone(), - )) - .unwrap(); - assert_eq!( - state.stage(&stage_id).unwrap().agent_control, - AgentControlState::Running - ); - - state - .apply_event(&test_stage_event( - 3, - agent_body(CodingEvent::RoundInterrupted { generation: 2 }), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&test_stage_event( - 4, - EventBody::AgentSessionDeactivated(AgentSessionDeactivatedProps { visit: 1 }), - stage_id.clone(), - )) - .unwrap(); - assert_eq!( - state.stage(&stage_id).unwrap().agent_control, - AgentControlState::Running - ); - - state - .apply_event(&test_stage_event( - 5, - agent_body(CodingEvent::RoundInterrupted { generation: 3 }), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&test_stage_event( - 6, - EventBody::StageFailed(failed_props(10, false)), - stage_id.clone(), - )) - .unwrap(); - assert_eq!( - state.stage(&stage_id).unwrap().agent_control, - AgentControlState::Running - ); - } - - #[test] - fn subagent_events_update_stage_projection() { - let mut state = initialized_projection(); - let stage_id = stage_id(); - - state - .apply_event(&test_stage_event( - 1, - agent_body(CodingEvent::SubAgentSpawned { - agent_id: "sub-1".to_string(), - depth: 1, - task: "write tests".to_string(), - generation: 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, - agent_body(CodingEvent::SubAgentCompleted { - agent_id: "sub-1".to_string(), - depth: 1, - generation: 1, - success: true, - turns_used: 3, - }), - 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, - agent_body(CodingEvent::SubAgentTurnStarted { - agent_id: "sub-1".to_string(), - depth: 1, - task: "fix the review findings".to_string(), - generation: 2, - }), - stage_id.clone(), - )) - .unwrap(); - let stage = state.stage(&stage_id).unwrap(); - assert_eq!(stage.subagents.len(), 1); - assert_eq!(stage.subagents[0].task, "write tests"); - assert_eq!(stage.subagents[0].status, SubAgentStatus::Running); - - state - .apply_event(&test_stage_event( - 4, - agent_body(CodingEvent::SubAgentCompleted { - agent_id: "sub-1".to_string(), - depth: 1, - generation: 2, - success: true, - turns_used: 5, - }), - stage_id.clone(), - )) - .unwrap(); - let stage = state.stage(&stage_id).unwrap(); - assert_eq!(stage.subagents.len(), 1); - assert_eq!(stage.subagents[0].status, SubAgentStatus::Completed { - success: true, - turns_used: 5, - }); - - state - .apply_event(&test_stage_event( - 5, - agent_body(CodingEvent::SubAgentSpawned { - agent_id: "sub-2".to_string(), - depth: 2, - task: "debug failure".to_string(), - generation: 1, - }), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&test_stage_event( - 6, - agent_body(CodingEvent::SubAgentFailed { - agent_id: "sub-2".to_string(), - depth: 2, - generation: 1, - error: ErrorData::new(ErrorKind::Agent, "boom"), - }), - stage_id.clone(), - )) - .unwrap(); - let stage = state.stage(&stage_id).unwrap(); - assert_eq!(stage.subagents[1].status, SubAgentStatus::Failed { - error: json!({ "kind": "agent", "message": "boom" }), - }); - - state - .apply_event(&test_stage_event( - 7, - agent_body(CodingEvent::SubAgentClosed { - agent_id: "sub-2".to_string(), - depth: 2, - generation: 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, - agent_body(CodingEvent::SkillsDiscovered { - profile: "claude".to_string(), - source_dirs: vec![".claude/skills".to_string()], - skills: vec![ - SkillSummary { - name: "rust".to_string(), - description: "Rust help".to_string(), - }, - SkillSummary { - name: "docs".to_string(), - description: "Docs help".to_string(), - }, - ], - skipped: Vec::new(), - }), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&test_stage_event( - 2, - agent_body(CodingEvent::SkillActivated { - skill_name: "rust".to_string(), - source: SkillActivationSource::Slash, - }), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&test_stage_event( - 3, - agent_body(CodingEvent::SkillActivated { - skill_name: "rust".to_string(), - source: SkillActivationSource::Tool, - }), - 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, - SkillActivationSource::Slash - ); - assert_eq!( - stage.skills.activated[1].source, - SkillActivationSource::Tool - ); - } - - #[test] - fn agent_session_activation_updates_stage_permission_level_projection() { - fn activated_props( - permission_level: Option, - ) -> AgentSessionActivatedProps { - AgentSessionActivatedProps { - thread_id: None, - provider: Some("openai".to_string()), - model: Some("gpt-5.4".to_string()), - reasoning_effort: None, - speed: None, - permission_level, - capabilities: vec![], - visit: 1, - } - } - - let mut state = initialized_projection(); - let stage_id = stage_id(); - - state - .apply_event(&test_stage_event( - 1, - EventBody::AgentSessionActivated(activated_props(Some( - PermissionLevel::ReadOnly, - ))), - stage_id.clone(), - )) - .unwrap(); - - let stage = state.stage(&stage_id).unwrap(); - assert_eq!(stage.permission_level, Some(PermissionLevel::ReadOnly)); - - let mut legacy_state = initialized_projection(); - legacy_state - .apply_event(&test_stage_event( - 1, - EventBody::AgentSessionActivated(activated_props(None)), - stage_id.clone(), - )) - .unwrap(); - - let legacy_stage = legacy_state.stage(&stage_id).unwrap(); - assert_eq!(legacy_stage.permission_level, None); - } - - fn agent_tool(name: &str, category: ToolCategory, invoked: bool) -> ToolSummary { - ToolSummary { - name: name.to_string(), - description: format!("{name} description"), - source: ToolSource::Native, - category, - invoked, - } - } - - #[test] - fn agent_tools_available_replaces_stage_agent_tools() { - let mut state = initialized_projection(); - let stage_id = stage_id(); - - state - .apply_event(&test_stage_event( - 1, - EventBody::AgentToolsAvailable(AgentToolsAvailableProps { - tools: vec![ - agent_tool("read_file", ToolCategory::Read, false), - agent_tool("apply_patch", ToolCategory::Write, false), - ], - visit: 1, - }), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&test_stage_event( - 2, - EventBody::AgentToolsAvailable(AgentToolsAvailableProps { - tools: vec![agent_tool("grep", ToolCategory::Read, false)], - visit: 1, - }), - stage_id.clone(), - )) - .unwrap(); - - let stage = state.stage(&stage_id).unwrap(); - assert_eq!(stage.agent_tools, vec![agent_tool( - "grep", - ToolCategory::Read, - false - )]); - } - - #[test] - fn agent_tool_started_marks_only_matching_available_tool_invoked() { - let mut state = initialized_projection(); - let stage_id = stage_id(); - - state - .apply_event(&test_stage_event( - 1, - EventBody::AgentToolsAvailable(AgentToolsAvailableProps { - tools: vec![ - agent_tool("read_file", ToolCategory::Read, false), - agent_tool("apply_patch", ToolCategory::Write, false), - ], - visit: 1, - }), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&test_stage_event( - 2, - agent_body(CodingEvent::ToolCallStarted { - tool_name: "apply_patch".to_string(), - tool_call_id: "call_patch".to_string(), - arguments: serde_json::json!({}), - }), - stage_id.clone(), - )) - .unwrap(); - - let stage = state.stage(&stage_id).unwrap(); - assert!(!stage.agent_tools[0].invoked); - assert!(stage.agent_tools[1].invoked); - } - - #[test] - fn legacy_tool_started_without_available_tools_does_not_synthesize_tool_list() { - let mut state = initialized_projection(); - let stage_id = stage_id(); - - state - .apply_event(&test_stage_event( - 1, - agent_body(CodingEvent::ToolCallStarted { - tool_name: "apply_patch".to_string(), - tool_call_id: "call_patch".to_string(), - arguments: serde_json::json!({}), - }), - stage_id.clone(), - )) - .unwrap(); - - let stage = state.stage(&stage_id).unwrap(); - assert!(stage.agent_tools.is_empty()); - } - - #[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(), - }, - ], - startup_ms: 0, - 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(), - startup_ms: 0, - 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(), - }], - startup_ms: 0, - 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!(!stage.mcp_servers[0].invoked); - 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(), - }); - assert!(!stage.mcp_servers[1].invoked); - } - - #[test] - fn mcp_server_disconnect_keeps_tool_count_and_invoked() { - let mut state = initialized_projection(); - let stage_id = stage_id(); - - state - .apply_event(&test_stage_event( - 1, - EventBody::AgentMcpReady(AgentMcpReadyProps { - server_name: "github".to_string(), - tool_count: 1, - tools: vec![AgentMcpToolSummary { - name: "mcp__github__list_issues".to_string(), - original_name: "list_issues".to_string(), - }], - startup_ms: 842, - visit: 1, - }), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&test_stage_event( - 2, - agent_body(CodingEvent::ToolCallStarted { - tool_name: "mcp__github__list_issues".to_string(), - tool_call_id: "call_gh".to_string(), - arguments: serde_json::json!({}), - }), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&test_stage_event( - 3, - EventBody::AgentMcpDisconnected(AgentMcpDisconnectedProps { - server_name: "github".to_string(), - error: "transport closed".to_string(), - visit: 1, - }), - stage_id.clone(), - )) - .unwrap(); - - let stage = state.stage(&stage_id).unwrap(); - assert_eq!(stage.mcp_servers.len(), 1); - let github = &stage.mcp_servers[0]; - assert_eq!(github.server_name, "github"); - assert_eq!(github.status, McpServerStatus::Disconnected { - error: "transport closed".to_string(), - }); - assert_eq!(github.tool_count, 1, "the tools existed; they now fail"); - assert!(github.invoked, "the server was used before it dropped"); - } - - #[test] - fn mcp_server_disconnect_without_a_ready_is_recorded_without_tools() { - let mut state = initialized_projection(); - let stage_id = stage_id(); - - state - .apply_event(&test_stage_event( - 1, - EventBody::AgentMcpDisconnected(AgentMcpDisconnectedProps { - server_name: "github".to_string(), - error: "transport closed".to_string(), - visit: 1, - }), - stage_id.clone(), - )) - .unwrap(); - - let stage = state.stage(&stage_id).unwrap(); - assert_eq!(stage.mcp_servers.len(), 1); - assert_eq!(stage.mcp_servers[0].tool_count, 0); - assert!(!stage.mcp_servers[0].invoked); - assert_eq!(stage.mcp_servers[0].status, McpServerStatus::Disconnected { - error: "transport closed".to_string(), - }); - } - - #[test] - fn agent_tool_started_marks_matching_mcp_server_as_invoked() { - 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: 1, - tools: vec![AgentMcpToolSummary { - name: "read_file".to_string(), - original_name: "read_file".to_string(), - }], - startup_ms: 0, - visit: 1, - }), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&test_stage_event( - 2, - EventBody::AgentMcpReady(AgentMcpReadyProps { - server_name: "other".to_string(), - tool_count: 0, - tools: vec![], - startup_ms: 0, - visit: 1, - }), - stage_id.clone(), - )) - .unwrap(); - // Native (non-MCP) tool call: should not touch any MCP server. - state - .apply_event(&test_stage_event( - 3, - agent_body(CodingEvent::ToolCallStarted { - tool_name: "Bash".to_string(), - tool_call_id: "call_bash".to_string(), - arguments: serde_json::json!({}), - }), - stage_id.clone(), - )) - .unwrap(); - // Qualified MCP tool call: flips matching server's `invoked`. - state - .apply_event(&test_stage_event( - 4, - agent_body(CodingEvent::ToolCallStarted { - tool_name: "mcp__filesystem__read_file".to_string(), - tool_call_id: "call_fs".to_string(), - arguments: serde_json::json!({}), - }), - stage_id.clone(), - )) - .unwrap(); - - let stage = state.stage(&stage_id).unwrap(); - let filesystem = stage - .mcp_servers - .iter() - .find(|s| s.server_name == "filesystem") - .unwrap(); - assert!(filesystem.invoked, "filesystem should be marked invoked"); - let other = stage - .mcp_servers - .iter() - .find(|s| s.server_name == "other") - .unwrap(); - assert!(!other.invoked, "unused MCP server should stay un-invoked"); - } - - #[test] - fn mcp_invoked_flag_survives_status_reread() { - 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: 1, - tools: vec![AgentMcpToolSummary { - name: "read_file".to_string(), - original_name: "read_file".to_string(), - }], - startup_ms: 0, - visit: 1, - }), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&test_stage_event( - 2, - agent_body(CodingEvent::ToolCallStarted { - tool_name: "mcp__filesystem__read_file".to_string(), - tool_call_id: "call_fs".to_string(), - arguments: serde_json::json!({}), - }), - stage_id.clone(), - )) - .unwrap(); - // Server re-reports Ready (e.g. tool registry refresh): invoked - // must remain true, not get clobbered back to false. - state - .apply_event(&test_stage_event( - 3, - 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: "stat".to_string(), - original_name: "stat".to_string(), - }, - ], - startup_ms: 0, - visit: 1, - }), - stage_id.clone(), - )) - .unwrap(); - - let stage = state.stage(&stage_id).unwrap(); - assert!(stage.mcp_servers[0].invoked); - assert_eq!(stage.mcp_servers[0].tool_count, 2); - } - - #[test] - fn agent_messages_replace_latest_context_window_for_matching_stage() { - let mut state = initialized_projection(); - let stage_id = stage_id(); - let first = context_window_snapshot(10); - let second = context_window_snapshot(20); - - state - .apply_event(&test_stage_event( - 7, - agent_message_with_context_window(first), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&test_stage_event( - 8, - agent_message_with_context_window(second), - stage_id.clone(), - )) - .unwrap(); - - let stage = state.stage(&stage_id).unwrap(); - let snapshot = stage.context_window.as_ref().unwrap(); - assert_eq!(snapshot.input_tokens, 20); - assert_eq!(snapshot.event_seq, Some(8)); - } - - #[test] - fn agent_message_without_context_window_preserves_existing_context_window() { - let mut state = initialized_projection(); - let stage_id = stage_id(); - - state - .apply_event(&test_stage_event( - 7, - agent_message_with_context_window(context_window_snapshot(10)), - stage_id.clone(), - )) - .unwrap(); - state - .apply_event(&test_stage_event( - 8, - agent_message_body(1, 1), - stage_id.clone(), - )) - .unwrap(); - - let snapshot = state - .stage(&stage_id) - .unwrap() - .context_window - .as_ref() - .unwrap(); - assert_eq!(snapshot.input_tokens, 10); - assert_eq!(snapshot.event_seq, Some(7)); - } - - fn agent_message_with_context_window(context_window: ContextWindowSnapshot) -> EventBody { - let CodingEvent::AssistantMessage { - text, - model, - usage, - cost_usd_micros, - cost_source, - tool_call_count, - reasoning, - .. - } = assistant_message(1, 1) - else { - unreachable!("assistant_message builds an assistant message"); - }; - agent_body(CodingEvent::AssistantMessage { - text, - model, - usage, - cost_usd_micros, - cost_source, - tool_call_count, - context_window: Some(context_window), - reasoning, - }) - } - - fn context_window_snapshot(input_tokens: u64) -> ContextWindowSnapshot { - ContextWindowSnapshot { - provider: "openai".to_string(), - model: "gpt-5.4".to_string(), - context_window_tokens: 400_000, - input_tokens, - usage_percent: input_tokens as f64 * 100.0 / 400_000.0, - count_method: ContextWindowCountMethod::LocalEstimate, - staleness: ContextWindowStaleness::Live, - generated_at: SystemTime::now(), - event_seq: None, - breakdown: vec![ContextWindowBreakdownItem { - category: ContextWindowCategory::Conversation, - tokens: input_tokens, - usage_percent: input_tokens as f64 * 100.0 / 400_000.0, - }], - warnings: vec![ContextWindowWarning { - code: "local_token_estimate".to_string(), - message: "input token count is a local estimate".to_string(), - }], - } - } - } - mod inference_bracket_reducer { use fabro_types::{LlmOutputKind, LlmRetryPhase, StageInferenceProjection}; @@ -8201,12 +6600,12 @@ mod tests { } /// Fabro's stage fold and pebble's `SessionProjection` read the same - /// stored events, and every stage now carries pebble's fold of its own + /// stored events, and every stage carries pebble's fold of its own /// events as `StageProjection.agent`. These tests pin the two folds to /// each other: a stage's live account is the prompt delta pebble - /// reports, and every field the stage projection still keeps its own - /// arms for is derivable from `agent` under a stated rule. They are the - /// safety net for reading `agent.*` instead and deleting the old fields. + /// reports, the stage's own `usage` and `model` follow from `agent` + /// under a stated rule, and the facts the stage view reads from `agent` + /// are the whole-session fold's for that stage's events. mod session_projection_parity { use fabro_types::{ModelRef, TodoListKind}; use lithos_llm::catalog::{ModelId, ProviderId}; @@ -8390,13 +6789,8 @@ mod tests { projection.apply(coding_event(event)); } - let code_stage = run.stage(&code).unwrap(); - assert_eq!(code_stage.subagents.len(), 1); - assert_eq!(code_stage.subagents[0].agent_id, "sub-1"); - assert_eq!(code_stage.subagents[0].status, SubAgentStatus::Completed { - success: true, - turns_used: 1, - }); + let code_agent = run.stage(&code).unwrap().agent.as_ref().unwrap(); + assert_eq!(code_agent.subagents, projection.subagents); assert_eq!(projection.subagents.len(), 1); assert_eq!(projection.subagents[0].agent_id, "sub-1"); assert_eq!( @@ -8407,7 +6801,13 @@ mod tests { } ); assert!( - run.stage(&review).unwrap().subagents.is_empty(), + run.stage(&review) + .unwrap() + .agent + .as_ref() + .unwrap() + .subagents + .is_empty(), "the child was the code stage's" ); assert_eq!(projection.subagent_counts.spawned, 1); @@ -8458,13 +6858,10 @@ mod tests { assert_eq!(resumed, replayed); } - /// Pebble folds its own `McpServer*` events; fabro's `mcp_servers` - /// arms fold the `agent.mcp.*` events the workflow sink mirrors them - /// onto. The mirrored events are built here the way the sink builds - /// them. The sink stores the pebble event as well, which is what - /// feeds `StageProjection.agent`; - /// `the_old_stage_fields_are_derived_from_the_embedded_fold` - /// drives both from one stream. + /// The stage's embedded fold sees the pebble `McpServer*` events the + /// sink stores, so its MCP view is the whole-session fold's; the + /// `agent.mcp.*` mirrors the sink still emits change nothing on the + /// stage. #[test] fn mcp_servers_agree_across_the_two_folds() { let code = StageId::new("code", 1); @@ -8500,27 +6897,23 @@ mod tests { assert_eq!(resumed, projection); let mut run = initialized_projection(); + run.apply_event(&stored(1, &code, ready)).unwrap(); run.apply_event(&test_stage_event( - 1, + 2, EventBody::AgentMcpReady(AgentMcpReadyProps { server_name: "github".to_string(), tool_count: tools.len(), - tools: tools - .iter() - .map(|tool| AgentMcpToolSummary { - name: tool.name.clone(), - original_name: tool.original_name.clone(), - }) - .collect(), + tools: mirrored_tools(&tools), startup_ms: 842, visit: 1, }), code.clone(), )) .unwrap(); - run.apply_event(&stored(2, &code, call)).unwrap(); + run.apply_event(&stored(3, &code, call)).unwrap(); + run.apply_event(&stored(4, &code, disconnected)).unwrap(); run.apply_event(&test_stage_event( - 3, + 5, EventBody::AgentMcpDisconnected(AgentMcpDisconnectedProps { server_name: "github".to_string(), error: "transport closed".to_string(), @@ -8530,21 +6923,13 @@ mod tests { )) .unwrap(); - let stage = run.stage(&code).unwrap(); - assert_eq!(stage.mcp_servers.len(), projection.mcp_servers.len()); - let server = &stage.mcp_servers[0]; - let pebble = &projection.mcp_servers["github"]; - assert_eq!(server.server_name, "github"); - assert_eq!(server.tool_count, pebble.tools.len()); - assert_eq!(server.invoked, pebble.invoked); - assert!(server.invoked); - assert_eq!(pebble.error, None, "a disconnect is not a failed start"); - assert_eq!(server.status, McpServerStatus::Disconnected { - error: pebble - .disconnected - .clone() - .expect("pebble recorded the disconnect"), - }); + let agent = run.stage(&code).unwrap().agent.as_ref().unwrap(); + assert_eq!(agent.mcp_servers, projection.mcp_servers); + let github = &agent.mcp_servers["github"]; + assert!(github.invoked); + assert_eq!(github.tools.len(), 1); + assert_eq!(github.disconnected.as_deref(), Some("transport closed")); + assert_eq!(github.error, None, "a disconnect is not a failed start"); } fn assistant_message_with_window( @@ -8595,13 +6980,14 @@ mod tests { .collect() } - /// Every field the stage projection keeps its own fold for is - /// derivable from `stage.agent`, under the rule each assertion - /// states. The stream is what the sink stores for one agent stage: - /// fabro's own `agent.session.activated` and the `agent.mcp.*` - /// mirrors next to pebble's events. + /// The stage keeps `usage` and `model` as its own, derived from + /// `stage.agent` under the rule each assertion states; everything + /// else the stage view shows is read from `agent` directly. The + /// stream is what the sink stores for one agent stage: fabro's own + /// `agent.session.activated` and the `agent.mcp.*` mirrors next to + /// pebble's events. #[test] - fn the_old_stage_fields_are_derived_from_the_embedded_fold() { + fn the_stage_view_reads_the_embedded_fold() { let code = StageId::new("code", 1); let model = billed_usage().model().clone(); let provider = model.provider.to_string(); @@ -8837,80 +7223,45 @@ mod tests { )) ); - // Context window: the same snapshot, except that fabro stamps the - // run event seq into `event_seq` and pebble keeps the event's own. - let mut fabro_window = stage - .context_window - .clone() - .expect("fabro kept the latest window"); - assert_eq!(fabro_window.event_seq, Some(9)); - fabro_window.event_seq = None; - assert_eq!(Some(fabro_window), agent.context_window); - - // Todos: fabro keeps the root agent's list; pebble keeps every - // list in the tree, and the root's is the one keyed by its id. + // Everything else the stage view shows is the fold's. + assert_eq!( + agent + .context_window + .as_ref() + .map(|window| window.input_tokens), + Some(123_456) + ); let root_todos = agent .todos .values() - .find(|list| list.list_id == list.kind.list_id(ROOT)); - assert_eq!(stage.root_agent_todos.as_ref(), root_todos); - assert!(root_todos.is_some()); - assert_eq!(agent.todos.len(), 2, "the child's plan is only pebble's"); + .find(|list| list.list_id == list.kind.list_id(ROOT)) + .expect("the root's list is keyed by its session id"); + assert_eq!(root_todos.items.len(), 1); + assert_eq!(agent.todos.len(), 2, "the child's plan is kept apart"); assert!(agent.todos.contains_key(&child_list)); - - // Subagents: the same rows; the status tag is `status`, not - // `kind`, and a failure carries pebble's `ErrorData`. - assert_eq!(stage.subagents.len(), agent.subagents.len()); assert_eq!(agent.subagents.len(), 2); - for (fabro, pebble) in stage.subagents.iter().zip(&agent.subagents) { - assert_eq!(fabro.agent_id, pebble.agent_id); - assert_eq!(fabro.depth, pebble.depth); - assert_eq!(fabro.task, pebble.task); - let mut pebble_status = serde_json::to_value(&pebble.status).unwrap(); - let tag = pebble_status - .as_object_mut() - .unwrap() - .remove("status") - .expect("pebble tags the status"); - pebble_status["kind"] = tag; - assert_eq!(serde_json::to_value(&fabro.status).unwrap(), pebble_status); - } + assert_eq!( + serde_json::to_value(&agent.subagents[0].status).unwrap()["status"], + "completed" + ); assert_eq!( serde_json::to_value(&agent.subagents[1].status).unwrap()["status"], "failed" ); - - // Skills: the same shape. - assert_eq!(stage.skills.available, agent.skills.available); - assert_eq!(stage.skills.activated.len(), agent.skills.activated.len()); - for (fabro, pebble) in stage.skills.activated.iter().zip(&agent.skills.activated) { - assert_eq!(fabro.name, pebble.name); - assert_eq!(fabro.source, pebble.source); - } - - // MCP servers: `disconnected` set is Disconnected, else `error` - // set is Failed, else Ready; the tool count is `tools.len()`. - assert_eq!(stage.mcp_servers.len(), agent.mcp_servers.len()); + assert_eq!(agent.skills.available.len(), 1); + assert_eq!(agent.skills.activated[0].name, "rust"); + assert_eq!( + agent.skills.activated[0].source, + SkillActivationSource::Tool + ); assert_eq!(agent.mcp_servers.len(), 2); - for server in &stage.mcp_servers { - let pebble = &agent.mcp_servers[&server.server_name]; - assert_eq!(server.invoked, pebble.invoked); - assert_eq!(server.tool_count, pebble.tools.len()); - let expected = if let Some(error) = &pebble.disconnected { - McpServerStatus::Disconnected { - error: error.clone(), - } - } else if let Some(error) = &pebble.error { - McpServerStatus::Failed { - error: error.clone(), - } - } else { - McpServerStatus::Ready { - tools: mirrored_tools(&pebble.tools), - } - }; - assert_eq!(server.status, expected, "{}", server.server_name); - } + let github = &agent.mcp_servers["github"]; + assert_eq!(github.tools.len(), 1); + assert_eq!(github.disconnected.as_deref(), Some("transport closed")); + assert_eq!(github.error, None); + let broken = &agent.mcp_servers["broken"]; + assert_eq!(broken.error.as_deref(), Some("could not launch")); + assert!(broken.tools.is_empty()); assert!(agent.mcp_servers["github"].invoked); assert_eq!(agent.mcp_servers["github"].startup_ms, Some(842)); assert_eq!(agent.mcp_servers["broken"].startup_ms, Some(3)); diff --git a/lib/foundation/fabro-api/build.rs b/lib/foundation/fabro-api/build.rs index 72c597aaa..1e9a4389a 100644 --- a/lib/foundation/fabro-api/build.rs +++ b/lib/foundation/fabro-api/build.rs @@ -404,10 +404,6 @@ fn main() { &[], ), ("TodoListProjection", "fabro_types::TodoListProjection", &[]), - ("SubAgentProjection", "fabro_types::SubAgentProjection", &[]), - ("SubAgentStatus", "fabro_types::SubAgentStatus", &[]), - ("SkillsProjection", "fabro_types::SkillsProjection", &[]), - ("ActivatedSkill", "fabro_types::ActivatedSkill", &[]), ("SkillSummary", "fabro_types::SkillSummary", &[]), ( "SkillActivationSource", @@ -423,17 +419,6 @@ fn main() { "fabro_types::AgentToolsAvailableProps", &[], ), - ( - "McpServerProjection", - "fabro_types::McpServerProjection", - &[], - ), - ("McpServerStatus", "fabro_types::McpServerStatus", &[]), - ( - "AgentMcpToolSummary", - "fabro_types::AgentMcpToolSummary", - &[], - ), // Pebble's own fold of a stage's agent events, embedded in // `StageProjection.agent`. Every nested type is pebble's; the schema // names carry an `AgentSession` prefix where fabro already has a diff --git a/lib/foundation/fabro-api/src/lib.rs b/lib/foundation/fabro-api/src/lib.rs index c93eff100..8ded5c107 100644 --- a/lib/foundation/fabro-api/src/lib.rs +++ b/lib/foundation/fabro-api/src/lib.rs @@ -38,17 +38,16 @@ pub mod types { BlockedReason, FailureReason, PendingReason, RunControlAction, RunStatus, SuccessReason, }; pub use fabro_types::{ - ActivatedSkill, AgentControlState, AgentEventProps, AgentMcpToolSummary, - AgentToolsAvailableProps, AskFabro, AuthMethod, AutomationRef, BilledModelUsage, - BilledTokenCounts, BlobHash, CommandTermination, Conclusion, ContextWindowBreakdownItem, - ContextWindowCategory, ContextWindowCountMethod, ContextWindowSnapshot, - ContextWindowStaleness, ContextWindowWarning, CreateVariableRequest, DiffStats, - DiffSummary, DirtyStatus, EventEnvelope, ExecOutputTail, FailureCategory, FailureDetail, - FailureSignature, GitContext, GitRunTarget, GitRunTarget as AutomationGitWorkflowSource, - IdpIdentity, IntegrationConnectionKind, IntegrationConnectionState, - IntegrationConnectionStatus, IntegrationProvider, IntegrationStatus, InterviewOption, - InterviewQuestionRecord, LlmOutputKind, McpServerDraft as CreateMcpServerRequest, - McpServerProjection, McpServerReplace as ReplaceMcpServerRequest, McpServerStatus, + AgentControlState, AgentEventProps, AgentToolsAvailableProps, AskFabro, AuthMethod, + AutomationRef, BilledModelUsage, BilledTokenCounts, BlobHash, CommandTermination, + Conclusion, ContextWindowBreakdownItem, ContextWindowCategory, ContextWindowCountMethod, + ContextWindowSnapshot, ContextWindowStaleness, ContextWindowWarning, CreateVariableRequest, + DiffStats, DiffSummary, DirtyStatus, EventEnvelope, ExecOutputTail, FailureCategory, + FailureDetail, FailureSignature, GitContext, GitRunTarget, + GitRunTarget as AutomationGitWorkflowSource, IdpIdentity, IntegrationConnectionKind, + IntegrationConnectionState, IntegrationConnectionStatus, IntegrationProvider, + IntegrationStatus, InterviewOption, InterviewQuestionRecord, LlmOutputKind, + McpServerDraft as CreateMcpServerRequest, McpServerReplace as ReplaceMcpServerRequest, McpServerView as McpServer, McpTransportView, Model, ModelControls, ModelCosts, ModelFeatures, ModelLimits, ModelRef as BillingModelRef, ModelTestMode, PairId, PairMessageId, PairMessageRecord, PairMessageRequest, PairRecord, PairStartRequest, @@ -65,13 +64,13 @@ pub mod types { SandboxDetails, SandboxInfo, SandboxListMeta, SandboxListResponse, SandboxProviderKind, SandboxProviderLookupError, SandboxService, SandboxServiceListResponse, SecretMetadata, SecretType, ServerSettings, SessionDetail, SessionId, SessionStatus, SessionSummary, - SessionTurn, SkillActivationSource, SkillSummary, SkillsProjection, StageCompletion, - StageContextWindow, StageContextWindowUnavailableReason, StageHandler, StageId, - StageInferenceProjection, StageModelUsage, StageOutcome, StageProjection, StageState, - StageToolBatchProjection, SubAgentProjection, SubAgentStatus, SystemActorKind, - SystemIntegrationStatus, SystemIntegrationsResponse, TodoListProjection, ToolCategory, - ToolSource, ToolSummary, TurnId, UpdateVariableRequest, UserPrincipal, Variable, - VariableListResponse, WorkflowPath, WorkflowSettings, WorkflowVersion, WorkflowVersionId, + SessionTurn, SkillActivationSource, SkillSummary, StageCompletion, StageContextWindow, + StageContextWindowUnavailableReason, StageHandler, StageId, StageInferenceProjection, + StageModelUsage, StageOutcome, StageProjection, StageState, StageToolBatchProjection, + SystemActorKind, SystemIntegrationStatus, SystemIntegrationsResponse, TodoListProjection, + ToolCategory, ToolSource, ToolSummary, TurnId, UpdateVariableRequest, UserPrincipal, + Variable, VariableListResponse, WorkflowPath, WorkflowSettings, WorkflowVersion, + WorkflowVersionId, }; pub use lithos_llm::catalog::{ModelHandle, ProviderId}; pub use lithos_llm::types::{ diff --git a/lib/foundation/fabro-api/tests/stage_projection_round_trip.rs b/lib/foundation/fabro-api/tests/stage_projection_round_trip.rs index de27a4811..0b7ed2e74 100644 --- a/lib/foundation/fabro-api/tests/stage_projection_round_trip.rs +++ b/lib/foundation/fabro-api/tests/stage_projection_round_trip.rs @@ -1,8 +1,7 @@ use std::any::{TypeId, type_name}; use fabro_api::types::{ - ActivatedSkill as ApiActivatedSkill, AgentControlState as ApiAgentControlState, - AgentMcpToolSummary as ApiAgentMcpToolSummary, + AgentControlState as ApiAgentControlState, AgentToolsAvailableProps as ApiAgentToolsAvailableProps, BilledModelUsage as ApiBilledModelUsage, ContextWindowBreakdownItem as ApiContextWindowBreakdownItem, @@ -11,26 +10,23 @@ use fabro_api::types::{ ContextWindowSnapshot as ApiContextWindowSnapshot, ContextWindowStaleness as ApiContextWindowStaleness, ContextWindowWarning as ApiContextWindowWarning, LlmOutputKind as ApiLlmOutputKind, - McpServerProjection as ApiMcpServerProjection, McpServerStatus as ApiMcpServerStatus, ParallelBranchResult as ApiParallelBranchResult, PermissionLevel as ApiPermissionLevel, SkillActivationSource as ApiSkillActivationSource, SkillSummary as ApiSkillSummary, - SkillsProjection as ApiSkillsProjection, StageContextWindow as ApiStageContextWindow, + StageContextWindow as ApiStageContextWindow, StageContextWindowUnavailableReason as ApiStageContextWindowUnavailableReason, StageInferenceProjection as ApiStageInferenceProjection, StageProjection as ApiStageProjection, StageToolBatchProjection as ApiStageToolBatchProjection, - SubAgentProjection as ApiSubAgentProjection, SubAgentStatus as ApiSubAgentStatus, TodoListProjection as ApiTodoListProjection, ToolCategory as ApiToolCategory, ToolSource as ApiToolSource, ToolSummary as ApiToolSummary, }; use fabro_types::{ - ActivatedSkill, AgentControlState, AgentMcpToolSummary, AgentToolsAvailableProps, - BilledModelUsage, ContextWindowBreakdownItem, ContextWindowCategory, ContextWindowCountMethod, - ContextWindowSnapshot, ContextWindowStaleness, ContextWindowWarning, LlmOutputKind, - McpServerProjection, McpServerStatus, ModelRef, ParallelBranchId, ParallelBranchResult, - PermissionLevel, SkillActivationSource, SkillSummary, SkillsProjection, StageContextWindow, + AgentControlState, AgentToolsAvailableProps, BilledModelUsage, ContextWindowBreakdownItem, + ContextWindowCategory, ContextWindowCountMethod, ContextWindowSnapshot, ContextWindowStaleness, + ContextWindowWarning, LlmOutputKind, ModelRef, ParallelBranchId, ParallelBranchResult, + PermissionLevel, SkillActivationSource, SkillSummary, StageContextWindow, StageContextWindowUnavailableReason, StageId, StageInferenceProjection, StageProjection, - StageToolBatchProjection, SubAgentProjection, SubAgentStatus, TodoListKind, TodoListProjection, - ToolCategory, ToolSource, ToolSummary, + StageToolBatchProjection, TodoListKind, TodoListProjection, ToolCategory, ToolSource, + ToolSummary, }; use lithos_llm::catalog::{ModelId, ProviderId}; use lithos_llm::types::TokenCounts; @@ -116,19 +112,12 @@ 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::(); 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::(); @@ -300,45 +289,6 @@ 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" - } - ] - }, "permission_level": "read-only", "agent_tools": [ { @@ -360,41 +310,6 @@ fn stage_projection_round_trips_representative_json() { "invoked": false } ], - "mcp_servers": [ - { - "server_name": "filesystem", - "tool_count": 1, - "status": { - "kind": "ready", - "tools": [ - { - "name": "read_file", - "original_name": "read_file" - } - ] - }, - "invoked": true - } - ], - "context_window": { - "provider": "openai", - "model": "gpt-5.4", - "context_window_tokens": 400000, - "input_tokens": 123456, - "usage_percent": 30.864, - "count_method": "provider_api_scaled_breakdown", - "staleness": "live", - "generated_at": "2026-05-23T12:34:56.000Z", - "event_seq": 42, - "breakdown": [ - { - "category": "system_prompt", - "tokens": 30000, - "usage_percent": 7.5 - } - ], - "warnings": [] - }, "inference": { "session_id": "ses_root", "started_at": "2026-04-29T12:34:00Z", @@ -461,7 +376,7 @@ fn permission_level_matches_openapi_json_shape() { } #[test] -fn nested_agent_state_types_match_openapi_json_shape() { +fn todo_list_and_skill_types_match_openapi_json_shape() { for (kind, list_id, wire_kind) in [ ( TodoListKind::OpenAiPlan, @@ -484,32 +399,6 @@ fn nested_agent_state_types_match_openapi_json_shape() { 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 = SkillSummary { name: "rust".to_string(), description: "Rust workflow help".to_string(), @@ -529,103 +418,6 @@ fn nested_agent_state_types_match_openapi_json_shape() { assert_eq!(source_json, json!("slash")); let api_source: ApiSkillActivationSource = serde_json::from_value(source_json).unwrap(); assert_eq!(api_source, SkillActivationSource::Slash); - - let activated = ActivatedSkill { - name: "rust".to_string(), - source: SkillActivationSource::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] }, - invoked: true, - }; - 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" - } - ] - }, - "invoked": true, - }) - ); - let api_mcp: ApiMcpServerProjection = serde_json::from_value(mcp_json).unwrap(); - assert_eq!(api_mcp, mcp_server); - assert_eq!(mcp_server.tool_count, 1); - - let disconnected = McpServerProjection { - server_name: "filesystem".to_string(), - tool_count: 1, - status: McpServerStatus::Disconnected { - error: "transport closed".to_string(), - }, - invoked: true, - }; - let disconnected_json = serde_json::to_value(&disconnected).unwrap(); - assert_eq!( - disconnected_json, - json!({ - "server_name": "filesystem", - "tool_count": 1, - "status": { - "kind": "disconnected", - "error": "transport closed" - }, - "invoked": true, - }) - ); - let api_disconnected: ApiMcpServerProjection = - serde_json::from_value(disconnected_json).unwrap(); - assert_eq!(api_disconnected, disconnected); } #[test] diff --git a/lib/foundation/fabro-types/src/lib.rs b/lib/foundation/fabro-types/src/lib.rs index 9c0005449..1feb98b78 100644 --- a/lib/foundation/fabro-types/src/lib.rs +++ b/lib/foundation/fabro-types/src/lib.rs @@ -143,10 +143,9 @@ pub use run_intent::{ TargetValidationError, ValidatedGitRunTarget, ValidatedRunTarget, }; pub use run_projection::{ - ActivatedSkill, AgentControlState, CheckpointRecord, McpServerProjection, McpServerStatus, - PendingInterviewRecord, RunProjection, SkillsProjection, StageContextWindow, + AgentControlState, CheckpointRecord, PendingInterviewRecord, RunProjection, StageContextWindow, StageContextWindowUnavailableReason, StageInferenceProjection, StageModelUsage, - StageProjection, StageToolBatchProjection, SubAgentProjection, SubAgentStatus, first_event_seq, + StageProjection, StageToolBatchProjection, first_event_seq, }; pub use run_sandbox::{ RunSandbox, RunSandboxFailure, RunSandboxInstance, RunSandboxKind, RunSandboxPlan, diff --git a/lib/foundation/fabro-types/src/run_projection.rs b/lib/foundation/fabro-types/src/run_projection.rs index ed7ccb9ad..24afa8142 100644 --- a/lib/foundation/fabro-types/src/run_projection.rs +++ b/lib/foundation/fabro-types/src/run_projection.rs @@ -6,19 +6,18 @@ use chrono::{DateTime, Utc}; use lithos_llm::types::{ReasoningEffort, Speed}; use pebble_coding_agent::events::{ ContextWindowBreakdownItem, ContextWindowCountMethod, ContextWindowSnapshot, - ContextWindowStaleness, ContextWindowWarning, LlmOutputKind, PermissionLevel, - SkillActivationSource, SkillSummary, TodoListProjection, ToolSummary, + ContextWindowStaleness, ContextWindowWarning, LlmOutputKind, PermissionLevel, ToolSummary, }; use pebble_coding_agent::projection::SessionProjection; use strum::{Display, EnumString, IntoStaticStr}; use crate::run_event::{AgentSessionActivatedProps, StagePromptProps}; use crate::{ - AgentBackend, AgentMcpToolSummary, BilledModelUsage, BilledTokenCounts, Checkpoint, Conclusion, - GitIdentity, InterviewQuestionRecord, InvalidTransition, ModelRef, ParallelBranchId, - PullRequestCreation, PullRequestLink, RunApproval, RunControlAction, RunDiff, RunId, - RunSandbox, RunSpec, RunStatus, RunTiming, StageCompletion, StageHandler, StageId, StageState, - StageTiming, StartRecord, timing, + AgentBackend, BilledModelUsage, BilledTokenCounts, Checkpoint, Conclusion, GitIdentity, + InterviewQuestionRecord, InvalidTransition, ModelRef, ParallelBranchId, PullRequestCreation, + PullRequestLink, RunApproval, RunControlAction, RunDiff, RunId, RunSandbox, RunSpec, RunStatus, + RunTiming, StageCompletion, StageHandler, StageId, StageState, StageTiming, StartRecord, + timing, }; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -304,25 +303,10 @@ pub struct StageProjection { /// `usage` to `model`. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub billing_by_model: Vec, - /// Todo/task list owned by the stage's root agent session. - /// - /// OpenAI child sessions own separate per-session plans and do not appear - /// here. Anthropic task lists are root-scoped and shared with child - /// sessions, so child mutations of that shared list do appear here. - #[serde(default, rename = "todos", skip_serializing_if = "Option::is_none")] - pub root_agent_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 = "Option::is_none")] pub permission_level: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub agent_tools: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub mcp_servers: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub context_window: Option, /// Open inference bracket for this stage, if the event log contains one. /// /// `Some` means exactly *"an `agent.llm.started` was recorded and no @@ -341,10 +325,12 @@ pub struct StageProjection { #[serde(default)] pub agent_control: AgentControlState, /// Pebble's fold of this stage's agent events: the one agent projection, - /// fed every `agent.*` and `todo.*` event stored on the stage. Present - /// for pebble-backed agent stages once their first agent event is - /// stored; `None` for prompt, command, ACP, human, parallel, and - /// conditional stages. + /// fed every `agent.*` and `todo.*` event stored on the stage, and what + /// the stage view reads for todos, subagents, skills, MCP servers, files, + /// failovers, compactions, and the context window. Present for + /// pebble-backed agent stages once their first agent event is stored; + /// `None` for prompt, command, ACP, human, parallel, and conditional + /// stages. /// /// Its lifetime fields are the stage's totals across every prompt the /// stage ran, because each stage gets its own fold over its own events. @@ -427,67 +413,6 @@ pub enum AgentControlState { WaitingForSteer, } -#[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: SkillActivationSource, -} - -#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct McpServerProjection { - pub server_name: String, - pub tool_count: usize, - pub status: McpServerStatus, - /// True once any tool from this server has been invoked during the stage. - pub invoked: bool, -} - -#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum McpServerStatus { - Ready { - tools: Vec, - }, - Failed { - error: String, - }, - /// The server was ready and then its connection closed during the - /// stage; its tools fail until the session ends. - Disconnected { - 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] @@ -510,13 +435,8 @@ impl StageProjection { tool_batch: None, usage: BilledTokenCounts::default(), model: None, - root_agent_todos: None, - subagents: Vec::new(), - skills: SkillsProjection::default(), permission_level: None, agent_tools: Vec::new(), - mcp_servers: Vec::new(), - context_window: None, inference: None, acp_started_at: None, agent_control: AgentControlState::default(), diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES index a98114497..8a724e582 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES +++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES @@ -27,12 +27,10 @@ base.ts common.ts configuration.ts index.ts -models/activated-skill.ts models/agent-control-state.ts models/agent-error-data.ts models/agent-error-kind.ts models/agent-event-props.ts -models/agent-mcp-tool-summary.ts models/agent-session-activated-props.ts models/agent-session-activated-skill.ts models/agent-session-activity.ts @@ -244,12 +242,7 @@ models/manifest-workflow.ts models/mcp-http-protocol.ts models/mcp-server-list-meta.ts models/mcp-server-list-response.ts -models/mcp-server-projection.ts models/mcp-server-settings.ts -models/mcp-server-status-disconnected.ts -models/mcp-server-status-failed.ts -models/mcp-server-status-ready.ts -models/mcp-server-status.ts models/mcp-server.ts models/mcp-tool-summary.ts models/mcp-transport-http.ts @@ -508,7 +501,6 @@ models/session-summary.ts models/session-turn.ts models/skill-activation-source.ts models/skill-summary.ts -models/skills-projection.ts models/slack-integration-settings.ts models/ssh-access-request.ts models/ssh-access-response.ts @@ -527,12 +519,6 @@ models/stage-tool-batch-projection.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 deleted file mode 100644 index 2ba86c9f4..000000000 --- a/lib/packages/fabro-api-client/src/models/activated-skill.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Fabro Run API - * HTTP API for managing Fabro workflow run executions. - * - * The version of the OpenAPI document: 0.2.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 { SkillActivationSource } from './skill-activation-source'; - -/** - * One observed agent skill activation. - */ -export interface ActivatedSkill { - 'name': string; - 'source': SkillActivationSource; -} 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 deleted file mode 100644 index e5510ca18..000000000 --- a/lib/packages/fabro-api-client/src/models/agent-mcp-tool-summary.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Fabro Run API - * HTTP API for managing Fabro workflow run executions. - * - * The version of the OpenAPI document: 0.2.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/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index d707783e3..c099cd2d3 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -1,9 +1,7 @@ -export * from './activated-skill'; export * from './agent-control-state'; export * from './agent-error-data'; export * from './agent-error-kind'; export * from './agent-event-props'; -export * from './agent-mcp-tool-summary'; export * from './agent-session-activated-props'; export * from './agent-session-activated-skill'; export * from './agent-session-activity'; @@ -215,12 +213,7 @@ export * from './mcp-http-protocol'; export * from './mcp-server'; export * from './mcp-server-list-meta'; export * from './mcp-server-list-response'; -export * from './mcp-server-projection'; export * from './mcp-server-settings'; -export * from './mcp-server-status'; -export * from './mcp-server-status-disconnected'; -export * from './mcp-server-status-failed'; -export * from './mcp-server-status-ready'; export * from './mcp-tool-summary'; export * from './mcp-transport'; export * from './mcp-transport-http'; @@ -478,7 +471,6 @@ export * from './session-summary'; export * from './session-turn'; export * from './skill-activation-source'; export * from './skill-summary'; -export * from './skills-projection'; export * from './slack-integration-settings'; export * from './ssh-access-request'; export * from './ssh-access-response'; @@ -497,12 +489,6 @@ export * from './stage-tool-batch-projection'; 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 deleted file mode 100644 index 6d6f01393..000000000 --- a/lib/packages/fabro-api-client/src/models/mcp-server-projection.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Fabro Run API - * HTTP API for managing Fabro workflow run executions. - * - * The version of the OpenAPI document: 0.2.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; - /** - * True once the agent has invoked at least one tool from this server during the stage. - */ - 'invoked': boolean; -} diff --git a/lib/packages/fabro-api-client/src/models/mcp-server-status-disconnected.ts b/lib/packages/fabro-api-client/src/models/mcp-server-status-disconnected.ts deleted file mode 100644 index e684c9d01..000000000 --- a/lib/packages/fabro-api-client/src/models/mcp-server-status-disconnected.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Fabro Run API - * HTTP API for managing Fabro workflow run executions. - * - * The version of the OpenAPI document: 0.2.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - - - -/** - * The server was ready and then its connection closed during the stage; its tools fail until the session ends. - */ -export interface McpServerStatusDisconnected { - 'kind': McpServerStatusDisconnectedKindEnum; - /** - * What closed the connection, as the client observed it. - */ - 'error': string; -} - -export const McpServerStatusDisconnectedKindEnum = { - DISCONNECTED: 'disconnected' -} as const; - -export type McpServerStatusDisconnectedKindEnum = typeof McpServerStatusDisconnectedKindEnum[keyof typeof McpServerStatusDisconnectedKindEnum]; 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 deleted file mode 100644 index c644eba1d..000000000 --- a/lib/packages/fabro-api-client/src/models/mcp-server-status-failed.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Fabro Run API - * HTTP API for managing Fabro workflow run executions. - * - * The version of the OpenAPI document: 0.2.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 deleted file mode 100644 index 58dbbd421..000000000 --- a/lib/packages/fabro-api-client/src/models/mcp-server-status-ready.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Fabro Run API - * HTTP API for managing Fabro workflow run executions. - * - * The version of the OpenAPI document: 0.2.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 deleted file mode 100644 index 45e63f255..000000000 --- a/lib/packages/fabro-api-client/src/models/mcp-server-status.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Fabro Run API - * HTTP API for managing Fabro workflow run executions. - * - * The version of the OpenAPI document: 0.2.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 { McpServerStatusDisconnected } from './mcp-server-status-disconnected'; -// 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: 'disconnected' } & McpServerStatusDisconnected | { kind: 'failed' } & McpServerStatusFailed | { kind: 'ready' } & McpServerStatusReady; diff --git a/lib/packages/fabro-api-client/src/models/skills-projection.ts b/lib/packages/fabro-api-client/src/models/skills-projection.ts deleted file mode 100644 index 470a90f59..000000000 --- a/lib/packages/fabro-api-client/src/models/skills-projection.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Fabro Run API - * HTTP API for managing Fabro workflow run executions. - * - * The version of the OpenAPI document: 0.2.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 { SkillSummary } from './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-context-window.ts b/lib/packages/fabro-api-client/src/models/stage-context-window.ts index 6770f4ed8..e2e82f321 100644 --- a/lib/packages/fabro-api-client/src/models/stage-context-window.ts +++ b/lib/packages/fabro-api-client/src/models/stage-context-window.ts @@ -50,6 +50,9 @@ export interface StageContextWindow { 'count_method': ContextWindowCountMethod | null; 'staleness': ContextWindowStaleness; 'generated_at': string | null; + /** + * The coding agent\'s own event sequence for the snapshot, when it carried one; not the run event sequence. + */ 'event_seq': number | null; 'breakdown': Array; 'warnings': Array; 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 9834299d1..8e141edd2 100644 --- a/lib/packages/fabro-api-client/src/models/stage-projection.ts +++ b/lib/packages/fabro-api-client/src/models/stage-projection.ts @@ -33,21 +33,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 { ContextWindowSnapshot } from './context-window-snapshot'; -// 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 { ParallelBranchResult } from './parallel-branch-result'; // May contain unused imports in some cases // @ts-ignore import type { PermissionLevel } from './permission-level'; // 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 @@ -66,12 +57,6 @@ import type { StageTiming } from './stage-timing'; import type { StageToolBatchProjection } from './stage-tool-batch-projection'; // 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'; -// May contain unused imports in some cases -// @ts-ignore import type { ToolSummary } from './tool-summary'; /** @@ -120,25 +105,11 @@ export interface StageProjection { 'tool_batch'?: StageToolBatchProjection | 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; 'permission_level'?: PermissionLevel | null; /** * Effective model-callable tools exposed to this agent stage session. Tool parameter schemas are intentionally omitted from this projection. */ 'agent_tools'?: Array; - /** - * MCP servers observed by this stage. - */ - 'mcp_servers'?: Array; - 'context_window'?: ContextWindowSnapshot | null; 'inference'?: StageInferenceProjection | null; /** * Start of an external ACP agent process, if one is running. ACP agents do not expose Fabro\'s internal LLM brackets, so the process lifetime supplies their live inference estimate. 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 deleted file mode 100644 index 4d1b22a0f..000000000 --- a/lib/packages/fabro-api-client/src/models/sub-agent-projection.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Fabro Run API - * HTTP API for managing Fabro workflow run executions. - * - * The version of the OpenAPI document: 0.2.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 deleted file mode 100644 index 114cb14ca..000000000 --- a/lib/packages/fabro-api-client/src/models/sub-agent-status-closed.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Fabro Run API - * HTTP API for managing Fabro workflow run executions. - * - * The version of the OpenAPI document: 0.2.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 deleted file mode 100644 index b5c186fb3..000000000 --- a/lib/packages/fabro-api-client/src/models/sub-agent-status-completed.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Fabro Run API - * HTTP API for managing Fabro workflow run executions. - * - * The version of the OpenAPI document: 0.2.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 deleted file mode 100644 index cc08b4a3a..000000000 --- a/lib/packages/fabro-api-client/src/models/sub-agent-status-failed.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Fabro Run API - * HTTP API for managing Fabro workflow run executions. - * - * The version of the OpenAPI document: 0.2.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 deleted file mode 100644 index 9f47c69d1..000000000 --- a/lib/packages/fabro-api-client/src/models/sub-agent-status-running.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Fabro Run API - * HTTP API for managing Fabro workflow run executions. - * - * The version of the OpenAPI document: 0.2.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 deleted file mode 100644 index 1d88f804b..000000000 --- a/lib/packages/fabro-api-client/src/models/sub-agent-status.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Fabro Run API - * HTTP API for managing Fabro workflow run executions. - * - * The version of the OpenAPI document: 0.2.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;