From 318fdf720683dd6921ef4cd875e36ffd1323d945 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sun, 13 Sep 2026 08:13:10 -0600 Subject: [PATCH] Read the stage view from the agent's fold StageProjection loses todos, subagents, skills, mcp_servers, and context_window, the types behind them, their fold arms and helpers, and their OpenAPI schemas: every one of those facts is pebble's fold in StageProjection.agent now. The context-window endpoint reads the fold's snapshot, whose event_seq is the agent's own sequence. The parity module keeps its assertions on the surviving own fields, usage and model, and checks that what the stage view reads from agent is the whole-session fold's for the stage's events. The TypeScript client is regenerated and its stale models removed. Co-Authored-By: Claude Fable 5.1 --- docs/internal/events.md | 6 +- docs/public/api-reference/fabro-api.yaml | 227 +- .../fabro-server/src/server/handler/runs.rs | 12 +- lib/components/fabro-store/src/run_state.rs | 1825 +---------------- lib/foundation/fabro-api/build.rs | 15 - lib/foundation/fabro-api/src/lib.rs | 35 +- .../tests/stage_projection_round_trip.rs | 226 +- lib/foundation/fabro-types/src/lib.rs | 5 +- .../fabro-types/src/run_projection.rs | 104 +- .../src/.openapi-generator/FILES | 14 - .../src/models/activated-skill.ts | 26 - .../src/models/agent-mcp-tool-summary.ts | 23 - .../fabro-api-client/src/models/index.ts | 14 - .../src/models/mcp-server-projection.ts | 31 - .../models/mcp-server-status-disconnected.ts | 32 - .../src/models/mcp-server-status-failed.ts | 26 - .../src/models/mcp-server-status-ready.ts | 29 - .../src/models/mcp-server-status.ts | 33 - .../src/models/skills-projection.ts | 29 - .../src/models/stage-context-window.ts | 3 + .../src/models/stage-projection.ts | 29 - .../src/models/sub-agent-projection.ts | 28 - .../src/models/sub-agent-status-closed.ts | 25 - .../src/models/sub-agent-status-completed.ts | 27 - .../src/models/sub-agent-status-failed.ts | 26 - .../src/models/sub-agent-status-running.ts | 25 - .../src/models/sub-agent-status.ts | 33 - 27 files changed, 149 insertions(+), 2759 deletions(-) delete mode 100644 lib/packages/fabro-api-client/src/models/activated-skill.ts delete mode 100644 lib/packages/fabro-api-client/src/models/agent-mcp-tool-summary.ts delete mode 100644 lib/packages/fabro-api-client/src/models/mcp-server-projection.ts delete mode 100644 lib/packages/fabro-api-client/src/models/mcp-server-status-disconnected.ts delete mode 100644 lib/packages/fabro-api-client/src/models/mcp-server-status-failed.ts delete mode 100644 lib/packages/fabro-api-client/src/models/mcp-server-status-ready.ts delete mode 100644 lib/packages/fabro-api-client/src/models/mcp-server-status.ts delete mode 100644 lib/packages/fabro-api-client/src/models/skills-projection.ts delete mode 100644 lib/packages/fabro-api-client/src/models/sub-agent-projection.ts delete mode 100644 lib/packages/fabro-api-client/src/models/sub-agent-status-closed.ts delete mode 100644 lib/packages/fabro-api-client/src/models/sub-agent-status-completed.ts delete mode 100644 lib/packages/fabro-api-client/src/models/sub-agent-status-failed.ts delete mode 100644 lib/packages/fabro-api-client/src/models/sub-agent-status-running.ts delete mode 100644 lib/packages/fabro-api-client/src/models/sub-agent-status.ts 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;