diff --git a/Cargo.lock b/Cargo.lock index dfc9f6234..b7d2b6f3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2273,8 +2273,10 @@ dependencies = [ "fabro-config", "fabro-environment", "fabro-types", + "jsonschema", "lithos-llm", "openapiv3", + "pebble-coding-agent", "prettyplease", "progenitor", "progenitor-client", diff --git a/docs/internal/events.md b/docs/internal/events.md index 1f0747002..bcab1a858 100644 --- a/docs/internal/events.md +++ b/docs/internal/events.md @@ -951,7 +951,12 @@ Object-lifecycle event. `session_id` and `parent_session_id` are envelope fields } ``` -No properties. +No properties. One per prompt, when the agent has nothing more to do for +it. Pebble's `SessionProjection`, embedded in `StageProjection.agent`, reads +it to mark the prompt complete and the session idle, so the projection +rebuilt from the run's log needs it. Runs recorded before fabro stored it +never have it; their `agent.activity` stays `running`, and +`StageProjection.state` is the authority on whether the stage is done. ### `agent.input` @@ -1402,6 +1407,10 @@ Emitted when a sub-agent is spawned. ### `agent.mcp.ready` +Fabro's mirror of pebble's `McpServerReady`. The pebble event itself is +also stored, as `agent.mcp.server.ready`; see the section on stored pebble +events below. + ```json { "id": "...", "ts": "...", "run_id": "...", @@ -1605,7 +1614,10 @@ Emitted whenever a skill is activated in the running session. Sources: ### `agent.failover` -Emitted when the agent fails over to a different LLM provider/model. +Emitted when the agent fails over to a different LLM provider/model. On an +agent stage this is fabro's mirror of pebble's `RouteFailover`, which is +also stored as `agent.route.failover`; a one-shot prompt stage, which walks +the fallback plan without pebble, emits only this event. ```json { @@ -1633,10 +1645,20 @@ Emitted when the agent fails over to a different LLM provider/model. | `error` | string | Error that triggered failover | | `continuation` | string? | How the new route carried the prompt on, as pebble reported it: `replay_prompt` (nothing the prompt committed was in the conversation, so the new route was asked the prompt again) or `continue_turn` (the conversation held assistant output or tool results, so the new route continued from there). Absent on events written before pebble reported it and on one-shot prompt stages, which re-send their request themselves | +### `agent.route.failover`, `agent.mcp.server.ready`, `agent.mcp.server.failed`, `agent.mcp.server.disconnected` + +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.route.failover.stopped` Pebble's `RouteFailoverStopped` event, stored verbatim like every other -pebble event fabro does not mirror. An agent stage with fallback routes +pebble event. An agent stage with fallback routes publishes it when a model failure ends the prompt on its current route anyway: the failure does not qualify for failover (`reason: "ineligible"`) or every route has been taken (`reason: "exhausted"`). It follows the diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index 5241666de..aa06923ed 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -11227,6 +11227,18 @@ components: agent_control: $ref: "#/components/schemas/AgentControlState" description: Whether the agent is executing normally or waiting for steering after an interrupt. + agent: + oneOf: + - $ref: "#/components/schemas/AgentSessionProjection" + - type: "null" + description: >- + The coding agent's own fold of this stage's `agent.*` and `todo.*` + events, present for agent stages once their first agent event is + stored and absent for every other stage kind. Each stage owns one + fold over its own events, so the lifetime fields are the stage's + totals across every prompt it ran. `activity` reads `running` on + stages recorded before `agent.processing.end` was kept; `state` is + the authority on whether the stage is done. state: $ref: "#/components/schemas/StageState" description: Lifecycle state of the stage projection. @@ -11617,6 +11629,779 @@ components: original_name: type: string + AgentSessionProjection: + description: >- + The coding agent's fold of one stage's event stream: token counts and + provider-reported cost for the root session and each descendant, the + route and where it moved, the context window, tools, MCP servers, + skills, todo lists, subagents, compactions, files touched, and the + prompt in progress. Counts only; pricing a count from the catalog is + fabro's, and lives in `StageProjection.usage`. + type: object + required: + - root_session_id + - route + - activity + - usage + - cost_usd_micros + - messages + - descendants + - context_window + - tools + - mcp_servers + - skills + - subagent_counts + - todos + - subagents + - compactions + - files_touched + - last_file_touched + - prompts + - prompt + properties: + root_session_id: + type: ["string", "null"] + description: The root session, once an event named it. + route: + $ref: "#/components/schemas/AgentSessionRoute" + activity: + $ref: "#/components/schemas/AgentSessionActivity" + usage: + $ref: "#/components/schemas/TokenUsage" + description: The root session's usage over the stage. + cost_usd_micros: + type: ["integer", "null"] + format: uint64 + minimum: 0 + description: The root session's provider-reported cost, when a provider reported one. + messages: + type: integer + format: uint64 + minimum: 0 + description: Committed assistant messages from the root session. + descendants: + type: object + additionalProperties: + $ref: "#/components/schemas/AgentSessionDescendantAccount" + description: Every descendant session's account, by session id. + context_window: + oneOf: + - $ref: "#/components/schemas/ContextWindowSnapshot" + - type: "null" + description: >- + The root session's latest context window. `event_seq` is the + agent's own sequence when it carried one, not the run event seq. + tools: + type: object + additionalProperties: + $ref: "#/components/schemas/AgentSessionToolActivity" + description: Every tool called anywhere in the tree, by the name the model used. + retries: + type: integer + format: uint64 + minimum: 0 + default: 0 + description: Model calls retried after a failed attempt, across the tree. + mcp_servers: + type: object + additionalProperties: + $ref: "#/components/schemas/AgentSessionMcpServer" + description: Every MCP server the root configured, by name. + skills: + $ref: "#/components/schemas/AgentSessionSkills" + subagent_counts: + $ref: "#/components/schemas/AgentSessionSubagentCounts" + todos: + type: object + additionalProperties: + $ref: "#/components/schemas/TodoListProjection" + description: >- + Every todo list in the tree, by list id. The root agent's own list + is the one whose id ends with `root_session_id`. + subagents: + type: array + items: + $ref: "#/components/schemas/AgentSessionSubagent" + compactions: + type: array + items: + $ref: "#/components/schemas/AgentSessionCompaction" + description: The root session's compactions, in order. + failovers: + type: array + items: + $ref: "#/components/schemas/AgentSessionRouteFailover" + default: [] + description: Every move the root made to a fallback route, in order. + failover_stopped: + oneOf: + - $ref: "#/components/schemas/AgentSessionFailoverStop" + - type: "null" + description: >- + Why the prompt in progress, or the last one, stayed on its route + and ended there although fallback routes were named. Cleared when + a prompt starts. + files_touched: + type: array + items: + type: string + description: Files written or edited across the tree, sorted. + last_file_touched: + type: ["string", "null"] + prompts: + type: integer + format: uint64 + minimum: 0 + description: How many prompts have started. + prompt: + $ref: "#/components/schemas/AgentSessionPromptDelta" + pending_writes: + type: object + additionalProperties: + type: array + items: + type: string + description: >- + In-flight bookkeeping, not a fact about the session: the paths a + write or edit tool call named, by tool call id, between its start + and its completion. Present only while such a call is open; a + settled projection has no such member. + + AgentSessionActivity: + description: Where a session stands, as its events tell it. + type: string + enum: [idle, running, waiting_for_steer, ended] + + AgentSessionRoute: + description: The route a session runs on, as it reported it. + type: object + required: + - provider + - model + properties: + provider: + type: ["string", "null"] + model: + type: ["string", "null"] + + AgentSessionDescendantAccount: + description: What one descendant session spent, as its own events reported it. + type: object + required: + - parent + - usage + - cost_usd_micros + - messages + - compactions + properties: + parent: + type: string + description: The session that spawned it. + provider: + type: string + description: The provider it runs on, as its `SessionStarted` reported it. + model: + type: string + description: >- + The model it runs on, from its `SessionStarted`; when the start + was not seen, the model of its first answer. + usage: + $ref: "#/components/schemas/TokenUsage" + cost_usd_micros: + type: ["integer", "null"] + format: uint64 + minimum: 0 + messages: + type: integer + format: uint64 + minimum: 0 + description: Committed assistant messages. + compactions: + type: integer + format: uint64 + minimum: 0 + description: Compactions it completed. + + AgentSessionToolActivity: + description: How one tool has been used across the tree. + type: object + required: + - calls + - errors + - open + properties: + calls: + type: integer + format: uint64 + minimum: 0 + description: Calls started. + errors: + type: integer + format: uint64 + minimum: 0 + description: Calls that completed as errors. + open: + type: integer + format: uint64 + minimum: 0 + description: Calls started and not yet completed. + + AgentSessionSubagentCounts: + description: How many child lifecycle events the tree recorded. + type: object + required: + - spawned + - turns_started + - completed + - failed + - closed + properties: + spawned: + type: integer + format: uint64 + minimum: 0 + turns_started: + type: integer + format: uint64 + minimum: 0 + completed: + type: integer + format: uint64 + minimum: 0 + failed: + type: integer + format: uint64 + minimum: 0 + closed: + type: integer + format: uint64 + minimum: 0 + + AgentSessionMcpServer: + description: >- + One MCP server the session configured, and whether it has been called. + `disconnected` set means the server came up and its connection then + closed; otherwise `error` set means it did not start; otherwise it is + ready with `tools`. + type: object + required: + - tools + - error + - invoked + properties: + tools: + type: array + items: + $ref: "#/components/schemas/McpToolSummary" + error: + type: ["string", "null"] + description: Why it did not start, when it did not. + invoked: + type: boolean + description: Whether any of its tools has been called. + disconnected: + type: string + description: What closed its connection during the session, when it closed. + startup_ms: + type: integer + format: uint64 + minimum: 0 + description: >- + Milliseconds from launch to its outcome: to its tools being + listed, or to the failure. Absent until either has been seen. + + AgentSessionSkills: + description: The skills the root session found and the ones activated anywhere in the tree. + type: object + required: + - available + - activated + properties: + available: + type: array + items: + $ref: "#/components/schemas/SkillSummary" + activated: + type: array + items: + $ref: "#/components/schemas/AgentSessionActivatedSkill" + + AgentSessionActivatedSkill: + description: A skill the session activated. + type: object + required: + - name + - source + properties: + name: + type: string + source: + $ref: "#/components/schemas/SkillActivationSource" + + AgentSessionSubagent: + description: One child the root spawned. A reused child stays one row; every event after the spawn moves its status. + 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/AgentSessionSubagentStatus" + + AgentSessionSubagentStatus: + description: Where a child stands. + oneOf: + - $ref: "#/components/schemas/AgentSessionSubagentStatusRunning" + - $ref: "#/components/schemas/AgentSessionSubagentStatusCompleted" + - $ref: "#/components/schemas/AgentSessionSubagentStatusFailed" + - $ref: "#/components/schemas/AgentSessionSubagentStatusClosed" + discriminator: + propertyName: status + mapping: + running: "#/components/schemas/AgentSessionSubagentStatusRunning" + completed: "#/components/schemas/AgentSessionSubagentStatusCompleted" + failed: "#/components/schemas/AgentSessionSubagentStatusFailed" + closed: "#/components/schemas/AgentSessionSubagentStatusClosed" + + AgentSessionSubagentStatusRunning: + type: object + required: + - status + properties: + status: + type: string + enum: [running] + + AgentSessionSubagentStatusCompleted: + type: object + required: + - status + - success + - turns_used + properties: + status: + type: string + enum: [completed] + success: + type: boolean + turns_used: + type: integer + minimum: 0 + + AgentSessionSubagentStatusFailed: + type: object + required: + - status + - error + properties: + status: + type: string + enum: [failed] + error: + $ref: "#/components/schemas/AgentErrorData" + + AgentSessionSubagentStatusClosed: + type: object + required: + - status + properties: + status: + type: string + enum: [closed] + + AgentSessionCompaction: + description: One compaction the root session completed. + type: object + required: + - reason + - original_turn_count + - preserved_turn_count + - summary_token_estimate + - tracked_file_count + properties: + reason: + $ref: "#/components/schemas/CompactionReason" + original_turn_count: + type: integer + minimum: 0 + preserved_turn_count: + type: integer + minimum: 0 + summary_token_estimate: + type: integer + minimum: 0 + tracked_file_count: + type: integer + minimum: 0 + usage: + $ref: "#/components/schemas/TokenUsage" + description: >- + The summary call's tokens: a breakdown of the session's and the + prompt's usage, which already include them. Zero on compactions + recorded before it was kept. + cost_usd_micros: + type: integer + format: uint64 + minimum: 0 + description: The summary call's provider-reported cost, included in the totals the same way. + + AgentSessionRouteFailover: + description: One move the root session made to a fallback route, as the stream reported it from the route it moved to. + type: object + required: + - from + - to + - attempt + - error + - usage + - inference_ms + - tool_ms + - continuation + properties: + from: + type: string + description: The `provider/model` that failed. + to: + type: string + description: The `provider/model` the prompt continued on. + attempt: + type: integer + format: uint32 + minimum: 0 + description: How many routes the prompt had moved through, this one included. + error: + $ref: "#/components/schemas/AgentErrorData" + description: The failure that ended the previous route. + usage: + $ref: "#/components/schemas/TokenUsage" + description: >- + What the prompt spent on the failed route. Already in the + session's and the prompt's totals through that route's committed + answers: a breakdown, not an addition. + cost_usd_micros: + type: integer + format: uint64 + minimum: 0 + inference_ms: + type: integer + format: uint64 + minimum: 0 + description: Milliseconds the prompt spent waiting on the failed route's model. + tool_ms: + type: integer + format: uint64 + minimum: 0 + description: Milliseconds the prompt spent running tools on the failed route. + continuation: + $ref: "#/components/schemas/FailoverContinuation" + + AgentSessionFailoverStop: + description: Why a prompt stayed on its route and ended there although fallback routes were named. + type: object + required: + - route + - attempt + - reason + - error + properties: + route: + type: string + description: The `provider/model` the prompt ended on. + attempt: + type: integer + format: uint32 + minimum: 0 + description: How many fallback routes the prompt had moved through; `0` on the route it started on. + reason: + $ref: "#/components/schemas/FailoverStop" + error: + $ref: "#/components/schemas/AgentErrorData" + description: The failure that ended the prompt. + + AgentSessionPromptDelta: + description: >- + What the prompt in progress, or the last one, did: reset when a prompt + starts, complete once `completed` is set. + type: object + required: + - completed + - usage + - cost_usd_micros + - messages + - context_window + - tool_calls + - descendants + - subagents + - compactions + - files_touched + - last_file_touched + properties: + completed: + type: boolean + description: Whether the prompt reached its end. + usage: + $ref: "#/components/schemas/TokenUsage" + description: The root session's usage over the prompt. + cost_usd_micros: + type: ["integer", "null"] + format: uint64 + minimum: 0 + messages: + type: integer + format: uint64 + minimum: 0 + description: Committed assistant messages. + context_window: + oneOf: + - $ref: "#/components/schemas/ContextWindowSnapshot" + - type: "null" + description: The latest context window the prompt reported. + tool_calls: + type: integer + format: uint64 + minimum: 0 + description: Tool calls started, across the tree. + retries: + type: integer + format: uint64 + minimum: 0 + default: 0 + description: Model calls retried after a failed attempt, across the tree. + failovers: + type: integer + format: uint32 + minimum: 0 + default: 0 + description: Moves the root made to a fallback route during the prompt. + descendants: + type: object + additionalProperties: + $ref: "#/components/schemas/AgentSessionDescendantAccount" + description: What each descendant spent during the prompt, by session id. + subagents: + $ref: "#/components/schemas/AgentSessionSubagentCounts" + description: Child lifecycle events during the prompt. + compactions: + type: array + items: + $ref: "#/components/schemas/AgentSessionCompaction" + description: Compactions the root completed during the prompt. + files_touched: + type: array + items: + type: string + description: Files written or edited during the prompt, across the tree, sorted. + last_file_touched: + type: ["string", "null"] + + TokenUsage: + description: >- + Token accounting as the coding agent counts it. The five buckets are + disjoint: every token is counted in exactly one, so their plain sum + is the total. A bucket that is absent reads as zero. + type: object + properties: + input: + type: integer + format: uint64 + minimum: 0 + default: 0 + description: Prompt tokens that were neither read from nor written to a cache. + output: + type: integer + format: uint64 + minimum: 0 + default: 0 + description: Completion tokens that are not reasoning tokens. + reasoning: + type: integer + format: uint64 + minimum: 0 + default: 0 + description: Completion tokens spent on reasoning, billed at the output rate. + cache_read: + type: integer + format: uint64 + minimum: 0 + default: 0 + description: Prompt tokens served from a provider cache. + cache_write: + type: integer + format: uint64 + minimum: 0 + default: 0 + description: Prompt tokens written into a provider cache. + + McpToolSummary: + description: One tool an MCP server advertised, as the coding agent's registry named it. + type: object + required: + - name + - original_name + properties: + name: + type: string + description: "The name the model calls: `mcp__{server}__{tool}`." + original_name: + type: string + description: The server's own name for the tool. + + CompactionReason: + description: Why a conversation compaction ran. + type: string + enum: [threshold, manual, overflow] + + FailoverContinuation: + description: >- + How a prompt carries on after a failover. `replay_prompt` when nothing + the prompt committed is in the conversation, so the new route is asked + the prompt again; `continue_turn` when the conversation holds output + or tool results this prompt committed, so the new route continues the + turn from where it stood. + type: string + enum: [replay_prompt, continue_turn] + + FailoverStop: + description: >- + Why a model failure ends a prompt on its route when fallback routes + were named. `ineligible` when the failure follows the request, so + another route would fail the same way; `exhausted` when every named + route has been taken. + type: string + enum: [ineligible, exhausted] + + AgentErrorData: + description: >- + A failure as the coding agent's event stream carries it: category, + safe message, retry advice, provider and model context, and the + rendered source chain. Never a raw provider response body. + type: object + required: + - kind + - message + properties: + kind: + $ref: "#/components/schemas/AgentErrorKind" + message: + type: string + description: The whole failure rendered for a person, cause included. + llm_kind: + $ref: "#/components/schemas/LlmErrorKind" + description: The model-layer category, when a model call failed. + retry: + $ref: "#/components/schemas/LlmRetryClassification" + description: Whether repeating the same model call is safe, when a model call failed. + provider: + type: string + description: The provider that produced the failure, when one was selected. + model: + type: string + description: The model that produced the failure, when one was selected. + status: + type: integer + format: uint16 + minimum: 0 + description: The HTTP status, when the failure came from an HTTP response. + provider_code: + type: string + description: The provider's own error code, as reported on the wire. + provider_retry_after_millis: + type: integer + format: uint64 + minimum: 0 + description: The provider's advised wait in milliseconds. + source_chain: + type: array + items: + type: string + description: The text of each cause below `message`, outermost first. + + AgentErrorKind: + description: The stable category of a coding agent failure. + type: string + enum: + - llm + - compaction + - agent + - invalid_input + - session_closed + - invalid_state + - tool_execution + - interrupted + - tool_rounds_exhausted + - task + - event_stream + + LlmErrorKind: + description: >- + The model layer's stable failure category. Known values are + `configuration`, `model_selection`, `authentication`, + `access_denied`, `not_found`, `invalid_request`, `context_length`, + `rate_limit`, `quota_exceeded`, `content_filter`, `server`, + `provider`, `network`, `timeout`, `stream_decode`, `response_decode`, + `resource_limit`, `middleware`, and `cancelled`. A category written + by a newer model layer is carried as its own spelling. + type: string + example: rate_limit + + LlmRetryClassification: + description: Whether repeating the same resolved model call is safe. + oneOf: + - $ref: "#/components/schemas/LlmRetryClassificationNever" + - $ref: "#/components/schemas/LlmRetryClassificationSafe" + - $ref: "#/components/schemas/LlmRetryClassificationAfter" + discriminator: + propertyName: type + mapping: + never: "#/components/schemas/LlmRetryClassificationNever" + safe: "#/components/schemas/LlmRetryClassificationSafe" + after: "#/components/schemas/LlmRetryClassificationAfter" + + LlmRetryClassificationNever: + description: Repeating the call cannot succeed. + type: object + required: + - type + properties: + type: + type: string + enum: [never] + + LlmRetryClassificationSafe: + description: Repeating the call is safe on the caller's own schedule. + type: object + required: + - type + properties: + type: + type: string + enum: [safe] + + LlmRetryClassificationAfter: + description: Repeating the call is safe after the given delay. + type: object + required: + - type + - after_millis + properties: + type: + type: string + enum: [after] + after_millis: + type: integer + format: uint64 + minimum: 0 + description: The delay in milliseconds. + StageModelUsage: description: Provider, model, and request-control metadata recorded for a stage attempt. type: object diff --git a/lib/components/fabro-store/src/run_state.rs b/lib/components/fabro-store/src/run_state.rs index 7f3aef83e..e0cedb00f 100644 --- a/lib/components/fabro-store/src/run_state.rs +++ b/lib/components/fabro-store/src/run_state.rs @@ -759,6 +759,11 @@ fn apply_agent_event( ts: DateTime, ) { let visit = props.visit; + // Pebble's own fold sees every agent event the stage stored, before the + // fabro-only arms below read the same event. + if let Some(stage) = stage_at_stored_or_visit(state, stored, visit, seq) { + stage.agent.get_or_insert_default().apply(&props.event); + } #[expect( clippy::wildcard_enum_match_arm, reason = "pebble's event vocabulary is non-exhaustive and only some events project" @@ -8037,13 +8042,20 @@ mod tests { } /// Fabro's stage fold and pebble's `SessionProjection` read the same - /// stored events. The stage projection stays fabro's: it is the wire - /// contract the API serves and is applied to incrementally, so pebble's - /// value cannot stand in for it. These tests pin the two folds to each - /// other for a retained session that spans two stages, so a stage's live - /// account is the prompt delta pebble reports and the two never drift. + /// stored events, and every stage now 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. mod session_projection_parity { - use pebble_coding_agent::events::{InputSource, McpToolSummary}; + use fabro_types::{ModelRef, TodoListKind}; + use lithos_llm::catalog::{ModelId, ProviderId}; + use pebble_coding_agent::events::{ + ContextWindowCountMethod, ContextWindowSnapshot, ContextWindowStaleness, + ErrorData as AgentErrorData, ErrorKind as AgentErrorKind, InputSource, McpToolSummary, + SkillActivationSource, SkillSummary, TodoCreatedProps, TodoStatus, + }; use pebble_coding_agent::projection::{ SessionActivity, SessionProjection, SubagentStatus as PebbleSubagentStatus, }; @@ -8185,6 +8197,25 @@ mod tests { assert_eq!(projection.usage.input, 170); assert_eq!(projection.descendant_usage().0.input, 7); assert_eq!(projection.prompts, 2); + + // Each stage's embedded fold is fed that stage's events only, so + // its lifetime totals are the stage's own prompts: the delta the + // whole-session fold reports for them. + let code_agent = code_stage + .agent + .as_ref() + .expect("the code stage carries a fold"); + assert_eq!(code_agent.usage, code_delta.usage); + assert_eq!(code_agent.descendant_usage(), code_delta.descendant_usage()); + assert_eq!(code_agent.prompts, 1); + assert!(code_agent.prompt.completed); + let review_agent = review_stage + .agent + .as_ref() + .expect("the review stage carries a fold"); + assert_eq!(review_agent.usage, review_delta.usage); + assert!(review_agent.descendants.is_empty()); + assert_eq!(review_agent.prompts, 1); } #[test] @@ -8233,6 +8264,15 @@ mod tests { AgentControlState::Running, "fabro moves control to idle on its own stage events, not pebble's" ); + for stage in [&code, &review] { + let agent = run.stage(stage).unwrap().agent.as_ref().unwrap(); + assert_eq!( + agent.activity, + SessionActivity::Idle, + "the stored agent.processing.end completes the stage's own fold" + ); + assert_eq!(agent.root_session_id.as_deref(), Some(ROOT)); + } } #[test] @@ -8259,10 +8299,13 @@ mod tests { assert_eq!(resumed, replayed); } - /// Pebble folds its own `McpServer*` events; fabro folds the - /// `agent.mcp.*` events the workflow sink mirrors them onto, since - /// the raw pebble event is not stored. The mirrored events are built - /// here the way the sink builds them. + /// 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. #[test] fn mcp_servers_agree_across_the_two_folds() { let code = StageId::new("code", 1); @@ -8344,5 +8387,374 @@ mod tests { .expect("pebble recorded the disconnect"), }); } + + fn assistant_message_with_window( + input: u64, + output: u64, + window: ContextWindowSnapshot, + ) -> CodingEvent { + CodingEvent::AssistantMessage { + text: "assistant text".to_string(), + model: billed_usage().model().model_id.to_string(), + usage: TokenUsage { + input, + output, + ..TokenUsage::default() + }, + cost_usd_micros: None, + cost_source: None, + tool_call_count: 0, + context_window: Some(window), + reasoning: None, + } + } + + fn todo(list_id: &str, list_kind: TodoListKind, todo_id: &str) -> TodoCreatedProps { + TodoCreatedProps { + list_id: list_id.to_string(), + list_kind, + todo_id: todo_id.to_string(), + status: TodoStatus::Pending, + order: 0, + subject: "write tests".to_string(), + description: String::new(), + active_form: None, + owner: None, + blocks: Vec::new(), + blocked_by: Vec::new(), + metadata: std::collections::BTreeMap::new(), + } + } + + fn mirrored_tools(tools: &[McpToolSummary]) -> Vec { + tools + .iter() + .map(|tool| AgentMcpToolSummary { + name: tool.name.clone(), + original_name: tool.original_name.clone(), + }) + .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. + #[test] + fn the_old_stage_fields_are_derived_from_the_embedded_fold() { + let code = StageId::new("code", 1); + let model = billed_usage().model().clone(); + let provider = model.provider.to_string(); + let model_id = model.model_id.to_string(); + let tools = vec![McpToolSummary { + name: "mcp__github__list_issues".to_string(), + original_name: "list_issues".to_string(), + }]; + let root_list = TodoListKind::AnthropicTasks.list_id(ROOT); + let child_list = TodoListKind::OpenAiPlan.list_id(CHILD); + let window = ContextWindowSnapshot { + provider: provider.clone(), + model: model_id.clone(), + context_window_tokens: 400_000, + input_tokens: 123_456, + usage_percent: 30.864, + count_method: ContextWindowCountMethod::LocalEstimate, + staleness: ContextWindowStaleness::Live, + generated_at: SystemTime::UNIX_EPOCH, + event_seq: None, + breakdown: Vec::new(), + warnings: Vec::new(), + }; + let events = vec![ + test_stage_event(1, activated(&provider, &model_id), code.clone()), + stored( + 2, + &code, + root(CodingEvent::SessionStarted { + provider: Some(provider.clone()), + model: Some(model_id.clone()), + }), + ), + stored( + 3, + &code, + root(CodingEvent::McpServerReady { + server: "github".to_string(), + tools: tools.clone(), + startup_ms: 842, + }), + ), + test_stage_event( + 4, + EventBody::AgentMcpReady(AgentMcpReadyProps { + server_name: "github".to_string(), + tool_count: tools.len(), + tools: mirrored_tools(&tools), + startup_ms: 842, + visit: 1, + }), + code.clone(), + ), + stored( + 5, + &code, + root(CodingEvent::McpServerFailed { + server: "broken".to_string(), + error: "could not launch".to_string(), + startup_ms: 3, + }), + ), + test_stage_event( + 6, + EventBody::AgentMcpFailed(AgentMcpFailedProps { + server_name: "broken".to_string(), + error: "could not launch".to_string(), + startup_ms: 3, + visit: 1, + }), + code.clone(), + ), + stored( + 7, + &code, + root(CodingEvent::SkillsDiscovered { + profile: "anthropic".to_string(), + source_dirs: Vec::new(), + skills: vec![SkillSummary { + name: "rust".to_string(), + description: "Rust workflow help".to_string(), + }], + skipped: Vec::new(), + }), + ), + stored(8, &code, root(prompt())), + stored( + 9, + &code, + root(assistant_message_with_window(100, 10, window)), + ), + stored( + 10, + &code, + root(CodingEvent::ToolCallStarted { + tool_name: "mcp__github__list_issues".to_string(), + tool_call_id: "call_1".to_string(), + arguments: json!({}), + }), + ), + stored( + 11, + &code, + root(CodingEvent::SkillActivated { + skill_name: "rust".to_string(), + source: SkillActivationSource::Tool, + }), + ), + stored( + 12, + &code, + root(CodingEvent::TodoCreated(todo( + &root_list, + TodoListKind::AnthropicTasks, + "t1", + ))), + ), + stored( + 13, + &code, + root(CodingEvent::SubAgentSpawned { + agent_id: "sub-1".to_string(), + depth: 1, + task: "look around".to_string(), + generation: 1, + }), + ), + stored( + 14, + &code, + child(CodingEvent::TodoCreated(todo( + &child_list, + TodoListKind::OpenAiPlan, + "p1", + ))), + ), + stored(15, &code, child(assistant_message(7, 1))), + stored( + 16, + &code, + root(CodingEvent::SubAgentCompleted { + agent_id: "sub-1".to_string(), + depth: 1, + generation: 1, + success: true, + turns_used: 1, + }), + ), + stored( + 17, + &code, + root(CodingEvent::SubAgentSpawned { + agent_id: "sub-2".to_string(), + depth: 1, + task: "check the tests".to_string(), + generation: 1, + }), + ), + stored( + 18, + &code, + root(CodingEvent::SubAgentFailed { + agent_id: "sub-2".to_string(), + depth: 1, + generation: 1, + error: AgentErrorData::new(AgentErrorKind::Agent, "boom"), + }), + ), + stored( + 19, + &code, + child(CodingEvent::McpServerDisconnected { + server: "github".to_string(), + error: "transport closed".to_string(), + }), + ), + test_stage_event( + 20, + EventBody::AgentMcpDisconnected(AgentMcpDisconnectedProps { + server_name: "github".to_string(), + error: "transport closed".to_string(), + visit: 1, + }), + code.clone(), + ), + stored(21, &code, root(assistant_message(50, 5))), + stored(22, &code, root(CodingEvent::ProcessingEnd)), + ]; + + let mut run = initialized_projection(); + for event in &events { + run.apply_event(event).unwrap(); + } + let stage = run.stage(&code).unwrap(); + let agent = stage + .agent + .as_ref() + .expect("an agent stage carries pebble's fold"); + assert_eq!(agent.root_session_id.as_deref(), Some(ROOT)); + assert!(agent.prompt.completed); + assert_eq!(agent.activity, SessionActivity::Idle); + + // Usage: the stage's live account is the tree's spend, the root's + // own plus every descendant's. (At completion fabro's billing + // replaces it with the root-only report; that rule goes next.) + let (descendants, _) = agent.descendant_usage(); + assert_eq!( + stage.usage.input_tokens, + tokens(agent.usage.input + descendants.input) + ); + assert_eq!( + stage.usage.output_tokens, + tokens(agent.usage.output + descendants.output) + ); + assert_eq!( + stage.usage.total_tokens, + tokens(agent.usage.total() + descendants.total()) + ); + assert_eq!(stage.usage.input_tokens, 157, "100 + 7 + 50"); + + // Model: the route the session reported. + let route_provider = agent + .route + .provider + .as_deref() + .expect("route names a provider"); + let route_model = agent.route.model.as_deref().expect("route names a model"); + assert_eq!( + stage.model, + Some(ModelRef::new( + ProviderId::new(route_provider), + ModelId::new(route_model) + )) + ); + + // 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. + 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"); + 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[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.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); + } + 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/components/fabro-workflow/src/handler/llm/pebble.rs b/lib/components/fabro-workflow/src/handler/llm/pebble.rs index b21e3e60b..07f4231f0 100644 --- a/lib/components/fabro-workflow/src/handler/llm/pebble.rs +++ b/lib/components/fabro-workflow/src/handler/llm/pebble.rs @@ -163,13 +163,12 @@ fn classify_agent_error(error: pebble_coding_agent::Error) -> AgentErrorDisposit // --- Event sink ----------------------------------------------------------- /// Pebble's durable event sink for one stage: every agent event becomes a -/// run event in the run's log before the agent goes on. A route failover and -/// an MCP server's outcome or disconnect are facts the run already has -/// events for, so those are mirrored onto the run's own `agent.failover`, -/// `agent.mcp.ready`, `agent.mcp.failed`, and `agent.mcp.disconnected` -/// events instead of being stored twice. A failover that stops short, with -/// the chain exhausted or the error ineligible, has no event of fabro's own -/// and is stored as pebble's `agent.route.failover.stopped`. +/// run event in the run's log before the agent goes on, so the stage's +/// `SessionProjection` rebuilt from the log sees what the live one saw. A +/// route failover and an MCP server's outcome or disconnect are also +/// mirrored onto the run's own `agent.failover`, `agent.mcp.ready`, +/// `agent.mcp.failed`, and `agent.mcp.disconnected` events, which the store +/// still folds; those mirrors go once every reader is on the projection. struct WorkflowEventSink { emitter: Arc, node_id: String, @@ -214,7 +213,6 @@ impl EventSink for WorkflowEventSink { }, &self.scope, ); - return Ok(()); } CodingEvent::McpServerReady { server, @@ -238,7 +236,6 @@ impl EventSink for WorkflowEventSink { }, &self.scope, ); - return Ok(()); } CodingEvent::McpServerFailed { server, @@ -255,7 +252,6 @@ impl EventSink for WorkflowEventSink { }, &self.scope, ); - return Ok(()); } CodingEvent::McpServerDisconnected { server, error } => { self.emitter.emit_scoped( @@ -267,12 +263,13 @@ impl EventSink for WorkflowEventSink { }, &self.scope, ); - return Ok(()); } _ => {} } - // Deltas and the prompt's own durability barrier are not run history. - if event.event.is_streaming_noise() || matches!(event.event, CodingEvent::ProcessingEnd) { + // Streaming deltas are not run history. `ProcessingEnd` is: pebble's + // `SessionProjection` reads it to complete the prompt and mark the + // session idle, so a projection rebuilt from the run's log needs it. + if event.event.is_streaming_noise() { return Ok(()); } self.emitter diff --git a/lib/foundation/fabro-api/Cargo.toml b/lib/foundation/fabro-api/Cargo.toml index ec0ef3535..874532f9d 100644 --- a/lib/foundation/fabro-api/Cargo.toml +++ b/lib/foundation/fabro-api/Cargo.toml @@ -20,6 +20,7 @@ fabro-config = { path = "../fabro-config" } fabro-environment.workspace = true fabro-types = { path = "../fabro-types" } lithos-llm = { workspace = true, features = ["runtime"] } +pebble-coding-agent.workspace = true progenitor-client = "0.13" regress = "0.10" reqwest.workspace = true @@ -38,3 +39,5 @@ syn = "2" [dev-dependencies] fabro-types = { path = "../fabro-types", features = ["test-support"] } +jsonschema.workspace = true +serde_yaml = "0.9" diff --git a/lib/foundation/fabro-api/build.rs b/lib/foundation/fabro-api/build.rs index 6630080f7..d5c207a1c 100644 --- a/lib/foundation/fabro-api/build.rs +++ b/lib/foundation/fabro-api/build.rs @@ -433,6 +433,122 @@ fn main() { "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 + // schema of the same name for its own projection. + ( + "AgentSessionProjection", + "pebble_coding_agent::projection::SessionProjection", + &[], + ), + ( + "AgentSessionActivity", + "pebble_coding_agent::projection::SessionActivity", + &[], + ), + ( + "AgentSessionRoute", + "pebble_coding_agent::projection::RouteProjection", + &[], + ), + ( + "AgentSessionDescendantAccount", + "pebble_coding_agent::projection::DescendantAccount", + &[], + ), + ( + "AgentSessionToolActivity", + "pebble_coding_agent::projection::ToolActivity", + &[], + ), + ( + "AgentSessionSubagentCounts", + "pebble_coding_agent::projection::SubagentCounts", + &[], + ), + ( + "AgentSessionMcpServer", + "pebble_coding_agent::projection::McpServerProjection", + &[], + ), + ( + "AgentSessionSkills", + "pebble_coding_agent::projection::SkillsProjection", + &[], + ), + ( + "AgentSessionActivatedSkill", + "pebble_coding_agent::projection::ActivatedSkill", + &[], + ), + ( + "AgentSessionSubagent", + "pebble_coding_agent::projection::SubagentProjection", + &[], + ), + ( + "AgentSessionSubagentStatus", + "pebble_coding_agent::projection::SubagentStatus", + &[], + ), + ( + "AgentSessionCompaction", + "pebble_coding_agent::projection::CompactionProjection", + &[], + ), + ( + "AgentSessionRouteFailover", + "pebble_coding_agent::projection::RouteFailoverProjection", + &[], + ), + ( + "AgentSessionFailoverStop", + "pebble_coding_agent::projection::FailoverStopProjection", + &[], + ), + ( + "AgentSessionPromptDelta", + "pebble_coding_agent::projection::PromptDelta", + &[], + ), + ("TokenUsage", "pebble_coding_agent::events::TokenUsage", &[]), + ( + "McpToolSummary", + "pebble_coding_agent::events::McpToolSummary", + &[], + ), + ( + "CompactionReason", + "pebble_coding_agent::events::CompactionReason", + &[], + ), + ( + "FailoverContinuation", + "pebble_coding_agent::events::FailoverContinuation", + &[], + ), + ( + "FailoverStop", + "pebble_coding_agent::events::FailoverStop", + &[], + ), + ( + "AgentErrorData", + "pebble_coding_agent::events::ErrorData", + &[], + ), + ( + "AgentErrorKind", + "pebble_coding_agent::events::ErrorKind", + &[], + ), + ("LlmErrorKind", "lithos_llm::types::ErrorKind", &[]), + ( + "LlmRetryClassification", + "lithos_llm::types::RetryClassification", + &[], + ), ( "McpHttpProtocol", "fabro_types::settings::run::McpHttpProtocol", diff --git a/lib/foundation/fabro-api/src/lib.rs b/lib/foundation/fabro-api/src/lib.rs index 3ac034c73..485c72e20 100644 --- a/lib/foundation/fabro-api/src/lib.rs +++ b/lib/foundation/fabro-api/src/lib.rs @@ -75,12 +75,31 @@ pub mod types { }; pub use lithos_llm::catalog::{ModelHandle, ProviderId}; pub use lithos_llm::types::{ - ContentPart, Cost as CompletionCost, CostSource, Message, ReasoningEffort, ReasoningOutput, - ResponseFormat as CompletionResponseFormat, Role, Speed as BillingSpeed, + ContentPart, Cost as CompletionCost, CostSource, ErrorKind as LlmErrorKind, Message, + ReasoningEffort, ReasoningOutput, ResponseFormat as CompletionResponseFormat, + RetryClassification as LlmRetryClassification, Role, Speed as BillingSpeed, TokenCounts as CompletionUsage, ToolChoice as CompletionToolChoice, ToolDefinition as CompletionToolDefinition, ToolDefinitionKind as CompletionToolDefinitionKind, }; + /// `StageProjection.agent` is the coding agent's own fold of the stage's + /// events; the API reuses pebble's types under the schema names. + pub use pebble_coding_agent::events::{ + CompactionReason, ErrorData as AgentErrorData, ErrorKind as AgentErrorKind, + FailoverContinuation, FailoverStop, McpToolSummary, TokenUsage, + }; + pub use pebble_coding_agent::projection::{ + ActivatedSkill as AgentSessionActivatedSkill, + CompactionProjection as AgentSessionCompaction, + DescendantAccount as AgentSessionDescendantAccount, + FailoverStopProjection as AgentSessionFailoverStop, + McpServerProjection as AgentSessionMcpServer, PromptDelta as AgentSessionPromptDelta, + RouteFailoverProjection as AgentSessionRouteFailover, RouteProjection as AgentSessionRoute, + SessionActivity as AgentSessionActivity, SessionProjection as AgentSessionProjection, + SkillsProjection as AgentSessionSkills, SubagentCounts as AgentSessionSubagentCounts, + SubagentProjection as AgentSessionSubagent, SubagentStatus as AgentSessionSubagentStatus, + ToolActivity as AgentSessionToolActivity, + }; /// A sandbox's status on the API is the sandbox driver's own type. pub use sandbox_driver::{ NetworkPolicy as SandboxNetworkPolicy, Resources as SandboxResources, SandboxId, diff --git a/lib/foundation/fabro-api/tests/agent_session_projection_round_trip.rs b/lib/foundation/fabro-api/tests/agent_session_projection_round_trip.rs new file mode 100644 index 000000000..1c6a5ba87 --- /dev/null +++ b/lib/foundation/fabro-api/tests/agent_session_projection_round_trip.rs @@ -0,0 +1,629 @@ +//! `StageProjection.agent` is pebble's `SessionProjection`, reused verbatim +//! along with every type nested in it. These tests prove the API types are +//! pebble's own and that the OpenAPI schemas describe pebble's serde shape: +//! a populated projection validates against `AgentSessionProjection`, every +//! key it serializes is declared, and every enum variant this build knows is +//! in the spec. A pebble re-pin that adds a field or a variant fails here +//! until the spec is updated. + +use std::any::{TypeId, type_name}; +use std::collections::BTreeMap; +use std::num::NonZeroU32; +use std::time::{Duration, SystemTime}; + +use fabro_api::types::{ + AgentErrorData as ApiAgentErrorData, AgentErrorKind as ApiAgentErrorKind, + AgentSessionActivatedSkill as ApiAgentSessionActivatedSkill, + AgentSessionActivity as ApiAgentSessionActivity, + AgentSessionCompaction as ApiAgentSessionCompaction, + AgentSessionDescendantAccount as ApiAgentSessionDescendantAccount, + AgentSessionFailoverStop as ApiAgentSessionFailoverStop, + AgentSessionMcpServer as ApiAgentSessionMcpServer, + AgentSessionProjection as ApiAgentSessionProjection, + AgentSessionPromptDelta as ApiAgentSessionPromptDelta, + AgentSessionRoute as ApiAgentSessionRoute, + AgentSessionRouteFailover as ApiAgentSessionRouteFailover, + AgentSessionSkills as ApiAgentSessionSkills, AgentSessionSubagent as ApiAgentSessionSubagent, + AgentSessionSubagentCounts as ApiAgentSessionSubagentCounts, + AgentSessionSubagentStatus as ApiAgentSessionSubagentStatus, + AgentSessionToolActivity as ApiAgentSessionToolActivity, + CompactionReason as ApiCompactionReason, FailoverContinuation as ApiFailoverContinuation, + FailoverStop as ApiFailoverStop, LlmErrorKind as ApiLlmErrorKind, + LlmRetryClassification as ApiLlmRetryClassification, McpToolSummary as ApiMcpToolSummary, + StageProjection as ApiStageProjection, TokenUsage as ApiTokenUsage, +}; +use fabro_types::StageProjection; +use lithos_llm::types::{ErrorKind as LlmErrorKind, RetryClassification}; +use pebble_coding_agent::events::{ + CodingAgentEvent, CodingEvent, CompactionReason, ContextWindowCountMethod, + ContextWindowSnapshot, ContextWindowStaleness, ErrorData, ErrorKind, FailoverContinuation, + FailoverStop, InputSource, LlmRetryPhase, McpToolSummary, SkillActivationSource, SkillSummary, + TodoCreatedProps, TodoListKind, TodoStatus, TokenUsage, +}; +use pebble_coding_agent::projection::{ + ActivatedSkill, CompactionProjection, DescendantAccount, FailoverStopProjection, + McpServerProjection, PromptDelta, RouteFailoverProjection, RouteProjection, SessionActivity, + SessionProjection, SkillsProjection, SubagentCounts, SubagentProjection, SubagentStatus, + ToolActivity, +}; +use pebble_coding_agent::tools::ToolOutputMetadata; +use serde_json::{Value, json}; + +#[test] +fn agent_session_projection_reuses_pebbles_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::(); + assert_same_type::(); + assert_same_type::(); + assert_same_type::(); + assert_same_type::(); + assert_same_type::(); +} + +#[test] +fn a_populated_projection_matches_its_openapi_schema() { + let projection = scripted_projection(); + // The script reached every section of the projection. + assert_eq!(projection.root_session_id.as_deref(), Some("ses_root")); + assert_eq!(projection.activity, SessionActivity::Idle); + assert_eq!(projection.route.model.as_deref(), Some("claude-fable-5")); + assert_eq!(projection.prompts, 1); + assert!(projection.prompt.completed); + assert_eq!(projection.messages, 2); + assert_eq!(projection.retries, 1); + assert_eq!(projection.descendants.len(), 1); + assert_eq!(projection.tools.len(), 2); + assert_eq!(projection.mcp_servers.len(), 2); + assert_eq!(projection.skills.activated.len(), 1); + assert_eq!(projection.todos.len(), 1); + assert_eq!(projection.subagents.len(), 2); + assert_eq!(projection.compactions.len(), 1); + assert_eq!(projection.failovers.len(), 1); + assert!(projection.failover_stopped.is_some()); + assert_eq!(projection.files_touched, ["/workspace/src/lib.rs"]); + assert!(projection.context_window.is_some()); + + let value = serde_json::to_value(&projection).unwrap(); + assert!( + value.get("pending_writes").is_none(), + "a settled projection keeps its bookkeeping off the wire: {value}" + ); + assert_valid("AgentSessionProjection", &value); + assert_declared( + &spec(), + &json!({ "$ref": "#/components/schemas/AgentSessionProjection" }), + &value, + "agent", + ); + + let api: ApiAgentSessionProjection = serde_json::from_value(value.clone()).unwrap(); + assert_eq!(api, projection); + assert_eq!(serde_json::to_value(&api).unwrap(), value); +} + +#[test] +fn a_projection_taken_mid_write_declares_its_open_write() { + let mut projection = scripted_projection(); + projection.apply(&root(CodingEvent::ToolCallStarted { + tool_name: "write_file".to_string(), + tool_call_id: "call_open".to_string(), + arguments: json!({"file_path": "/workspace/README.md", "content": "x"}), + })); + let value = serde_json::to_value(&projection).unwrap(); + assert_eq!( + value["pending_writes"]["call_open"], + json!(["/workspace/README.md"]) + ); + assert_valid("AgentSessionProjection", &value); + assert_declared( + &spec(), + &json!({ "$ref": "#/components/schemas/AgentSessionProjection" }), + &value, + "agent", + ); +} + +#[test] +fn a_stage_projection_carrying_the_fold_matches_its_openapi_schema() { + let mut stage = StageProjection::new(NonZeroU32::new(1).unwrap()); + stage.agent = Some(scripted_projection()); + let value = serde_json::to_value(&stage).unwrap(); + assert!(value["agent"].is_object()); + assert_valid("StageProjection", &value); + assert_declared( + &spec(), + &json!({ "$ref": "#/components/schemas/StageProjection" }), + &value, + "stage", + ); + let api: ApiStageProjection = serde_json::from_value(value.clone()).unwrap(); + assert_eq!(serde_json::to_value(&api).unwrap(), value); + + let without: StageProjection = serde_json::from_value(json!({ + "first_event_seq": 1, + "prompt": null, + "response": null, + "completion": null, + "provider_used": null, + "diff": null, + "script_invocation": null, + "script_timing": null, + "parallel_results": null, + "output": null, + "usage": { + "input_tokens": 0, + "output_tokens": 0, + "total_tokens": 0, + "reasoning_tokens": 0, + "cache_read_tokens": 0, + "cache_write_tokens": 0 + }, + "agent_control": "running", + "state": "running" + })) + .unwrap(); + assert!( + without.agent.is_none(), + "a stage written before the fold existed has none" + ); + assert!( + serde_json::to_value(&without) + .unwrap() + .get("agent") + .is_none(), + "a stage without a fold serializes none" + ); +} + +#[test] +fn every_enum_variant_this_build_knows_is_in_the_spec() { + for activity in [ + SessionActivity::Idle, + SessionActivity::Running, + SessionActivity::WaitingForSteer, + SessionActivity::Ended, + ] { + assert_valid( + "AgentSessionActivity", + &serde_json::to_value(activity).unwrap(), + ); + } + for reason in [ + CompactionReason::Threshold, + CompactionReason::Manual, + CompactionReason::Overflow, + ] { + assert_valid("CompactionReason", &serde_json::to_value(reason).unwrap()); + } + for continuation in [ + FailoverContinuation::ReplayPrompt, + FailoverContinuation::ContinueTurn, + ] { + assert_valid( + "FailoverContinuation", + &serde_json::to_value(continuation).unwrap(), + ); + } + for stop in [FailoverStop::Ineligible, FailoverStop::Exhausted] { + assert_valid("FailoverStop", &serde_json::to_value(stop).unwrap()); + } + for kind in [ + ErrorKind::Llm, + ErrorKind::Compaction, + ErrorKind::Agent, + ErrorKind::InvalidInput, + ErrorKind::SessionClosed, + ErrorKind::InvalidState, + ErrorKind::ToolExecution, + ErrorKind::Interrupted, + ErrorKind::ToolRoundsExhausted, + ErrorKind::Task, + ErrorKind::EventStream, + ] { + assert_valid("AgentErrorKind", &serde_json::to_value(kind).unwrap()); + } + for retry in [ + RetryClassification::Never, + RetryClassification::Safe, + RetryClassification::after(Duration::from_millis(1500)), + ] { + let value = serde_json::to_value(retry).unwrap(); + assert_valid("LlmRetryClassification", &value); + assert_declared( + &spec(), + &json!({ "$ref": "#/components/schemas/LlmRetryClassification" }), + &value, + "retry", + ); + } + for kind in [ + LlmErrorKind::RateLimit, + LlmErrorKind::ContextLength, + LlmErrorKind::Unknown("later".to_string()), + ] { + assert_valid("LlmErrorKind", &serde_json::to_value(kind).unwrap()); + } + for status in [ + SubagentStatus::Running, + SubagentStatus::Completed { + success: true, + turns_used: 3, + }, + SubagentStatus::Failed { error: llm_error() }, + SubagentStatus::Closed, + ] { + let value = serde_json::to_value(&status).unwrap(); + assert_valid("AgentSessionSubagentStatus", &value); + assert_declared( + &spec(), + &json!({ "$ref": "#/components/schemas/AgentSessionSubagentStatus" }), + &value, + "status", + ); + } +} + +// --- The scripted session ------------------------------------------------- + +fn scripted_projection() -> SessionProjection { + let mut projection = SessionProjection::new(); + projection.apply_all(&scripted_events()); + projection +} + +/// One prompt that touches every section: MCP servers up and down, skills, +/// a todo, a completed and a failed child, a retry, a failover with a +/// compaction on the new route, and a stop. +fn scripted_events() -> Vec { + vec![ + root(CodingEvent::SessionStarted { + provider: Some("openai".to_string()), + model: Some("gpt-5.2".to_string()), + }), + root(CodingEvent::McpServerReady { + server: "github".to_string(), + tools: vec![McpToolSummary { + name: "mcp__github__list_issues".to_string(), + original_name: "list_issues".to_string(), + }], + startup_ms: 842, + }), + root(CodingEvent::McpServerFailed { + server: "broken".to_string(), + error: "could not launch".to_string(), + startup_ms: 3, + }), + root(CodingEvent::SkillsDiscovered { + profile: "anthropic".to_string(), + source_dirs: Vec::new(), + skills: vec![SkillSummary { + name: "rust".to_string(), + description: "Rust workflow help".to_string(), + }], + skipped: Vec::new(), + }), + root(CodingEvent::UserInput { + text: "build it".to_string(), + content: None, + source: InputSource::Prompt, + }), + root(message("openai", "gpt-5.2", 100, 10, Some(500))), + root(CodingEvent::ToolCallStarted { + tool_name: "mcp__github__list_issues".to_string(), + tool_call_id: "call_1".to_string(), + arguments: json!({}), + }), + root(completed("mcp__github__list_issues", "call_1", false)), + root(CodingEvent::ToolCallStarted { + tool_name: "write_file".to_string(), + tool_call_id: "call_2".to_string(), + arguments: json!({"file_path": "/workspace/src/lib.rs", "content": "pub fn x() {}"}), + }), + root(completed("write_file", "call_2", false)), + root(CodingEvent::SkillActivated { + skill_name: "rust".to_string(), + source: SkillActivationSource::Tool, + }), + root(CodingEvent::TodoCreated(TodoCreatedProps { + list_id: TodoListKind::AnthropicTasks.list_id("ses_root"), + list_kind: TodoListKind::AnthropicTasks, + todo_id: "t1".to_string(), + status: TodoStatus::InProgress, + order: 0, + subject: "write tests".to_string(), + description: String::new(), + active_form: Some("Writing tests".to_string()), + owner: None, + blocks: Vec::new(), + blocked_by: Vec::new(), + metadata: BTreeMap::new(), + })), + root(CodingEvent::SubAgentSpawned { + agent_id: "sub-1".to_string(), + depth: 1, + task: "review".to_string(), + generation: 1, + }), + child(CodingEvent::SessionStarted { + provider: Some("openai".to_string()), + model: Some("gpt-5.2-mini".to_string()), + }), + child(message("openai", "gpt-5.2-mini", 7, 1, None)), + root(CodingEvent::SubAgentCompleted { + agent_id: "sub-1".to_string(), + depth: 1, + generation: 1, + success: true, + turns_used: 1, + }), + root(CodingEvent::SubAgentSpawned { + agent_id: "sub-2".to_string(), + depth: 1, + task: "check the tests".to_string(), + generation: 1, + }), + root(CodingEvent::SubAgentFailed { + agent_id: "sub-2".to_string(), + depth: 1, + generation: 1, + error: ErrorData::new(ErrorKind::Agent, "boom"), + }), + root(CodingEvent::LlmRetry { + provider: "openai".to_string(), + model: "gpt-5.2".to_string(), + attempt: 0, + delay_secs: 0.1, + error: ErrorData::new(ErrorKind::Llm, "slow down"), + phase: LlmRetryPhase::Open, + }), + root(CodingEvent::RouteFailover { + from: "openai/gpt-5.2".to_string(), + to: "anthropic/claude-fable-5".to_string(), + attempt: 1, + error: llm_error(), + usage: TokenUsage { + input: 100, + output: 10, + ..TokenUsage::default() + }, + cost_usd_micros: Some(500), + inference_ms: 120, + tool_ms: 30, + continuation: FailoverContinuation::ContinueTurn, + }), + root(CodingEvent::CompactionCompleted { + original_turn_count: 20, + preserved_turn_count: 6, + summary_token_estimate: 500, + tracked_file_count: 1, + reason: CompactionReason::Threshold, + usage: TokenUsage { + input: 30, + ..TokenUsage::default() + }, + cost_usd_micros: Some(2), + }), + root(message("anthropic", "claude-fable-5", 50, 5, Some(300))), + root(CodingEvent::RouteFailoverStopped { + route: "anthropic/claude-fable-5".to_string(), + attempt: 1, + reason: FailoverStop::Exhausted, + error: llm_error(), + }), + root(CodingEvent::ProcessingEnd), + ] +} + +fn root(event: CodingEvent) -> CodingAgentEvent { + CodingAgentEvent::new("ses_root".to_string(), event, SystemTime::UNIX_EPOCH) +} + +fn child(event: CodingEvent) -> CodingAgentEvent { + CodingAgentEvent::new("ses_child".to_string(), event, SystemTime::UNIX_EPOCH) + .with_parent_session_id("ses_root".to_string()) +} + +fn message(provider: &str, model: &str, input: u64, output: u64, cost: Option) -> CodingEvent { + CodingEvent::AssistantMessage { + text: "ok".to_string(), + model: model.to_string(), + usage: TokenUsage { + input, + output, + ..TokenUsage::default() + }, + cost_usd_micros: cost, + cost_source: None, + tool_call_count: 0, + context_window: Some(ContextWindowSnapshot { + provider: provider.to_string(), + model: model.to_string(), + context_window_tokens: 400_000, + input_tokens: 123_456, + usage_percent: 30.864, + count_method: ContextWindowCountMethod::LocalEstimate, + staleness: ContextWindowStaleness::Live, + generated_at: SystemTime::UNIX_EPOCH, + event_seq: Some(9), + breakdown: Vec::new(), + warnings: Vec::new(), + }), + reasoning: None, + } +} + +fn completed(tool_name: &str, tool_call_id: &str, is_error: bool) -> CodingEvent { + CodingEvent::ToolCallCompleted { + tool_name: tool_name.to_string(), + tool_call_id: tool_call_id.to_string(), + output: json!("done"), + metadata: ToolOutputMetadata::default(), + is_error, + error_kind: None, + output_bytes_observed: 4, + output_bytes_retained: 4, + output_bytes_omitted: 0, + } +} + +/// A model-layer failure with every optional member set. +fn llm_error() -> ErrorData { + let mut error = ErrorData::new(ErrorKind::Llm, "rate limited: 429 Too Many Requests") + .with_provider("openai") + .with_model("gpt-5.2"); + error.llm_kind = Some(LlmErrorKind::RateLimit); + error.retry = Some(RetryClassification::after(Duration::from_millis(1500))); + error.status = Some(429); + error.provider_code = Some("rate_limit_exceeded".to_string()); + error.provider_retry_after_millis = Some(1500); + error.source_chain = vec!["429 Too Many Requests".to_string()]; + error +} + +// --- The spec as a JSON schema -------------------------------------------- + +#[expect( + clippy::disallowed_methods, + reason = "a synchronous test reads the spec from the repository once" +)] +fn spec() -> Value { + let text = std::fs::read_to_string(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../../docs/public/api-reference/fabro-api.yaml" + )) + .expect("the OpenAPI spec is in the repository"); + let yaml: serde_yaml::Value = serde_yaml::from_str(&text).expect("the spec parses"); + serde_json::to_value(yaml).expect("the spec is JSON-compatible") +} + +/// Validates `value` against one component schema, with the whole document +/// as the root so `$ref`s resolve. +fn assert_valid(schema_name: &str, value: &Value) { + let mut root = spec(); + root["$ref"] = json!(format!("#/components/schemas/{schema_name}")); + let validator = jsonschema::validator_for(&root).expect("the spec compiles as a JSON schema"); + let errors: Vec = validator + .iter_errors(value) + .map(|error| format!("{error} at {}", error.instance_path())) + .collect(); + assert!( + errors.is_empty(), + "{schema_name} rejects {value:#}:\n{}", + errors.join("\n") + ); +} + +fn resolve<'a>(spec: &'a Value, schema: &'a Value) -> &'a Value { + match schema.get("$ref").and_then(Value::as_str) { + Some(reference) => resolve( + spec, + spec.pointer(reference.trim_start_matches('#')) + .unwrap_or_else(|| panic!("unresolved {reference}")), + ), + None => schema, + } +} + +/// Every key `value` serializes is a declared property of `schema`, and +/// every required property is present, recursively. This is what catches a +/// pebble field the spec does not know yet, since the schemas do not forbid +/// additional properties. +fn assert_declared(spec: &Value, schema: &Value, value: &Value, path: &str) { + let schema = resolve(spec, schema); + if let Some(variants) = schema.get("oneOf").and_then(Value::as_array) { + if value.is_null() { + return; + } + let chosen = match schema.get("discriminator") { + Some(discriminator) => { + let property = discriminator["propertyName"] + .as_str() + .expect("a discriminator names its property"); + let tag = value[property] + .as_str() + .unwrap_or_else(|| panic!("{path}: no `{property}` tag in {value}")); + let target = discriminator["mapping"][tag] + .as_str() + .unwrap_or_else(|| panic!("{path}: `{tag}` is not a mapped variant")); + spec.pointer(target.trim_start_matches('#')) + .unwrap_or_else(|| panic!("unresolved {target}")) + } + None => variants + .iter() + .map(|variant| resolve(spec, variant)) + .find(|variant| variant.get("type").and_then(Value::as_str) != Some("null")) + .unwrap_or_else(|| panic!("{path}: no non-null variant")), + }; + assert_declared(spec, chosen, value, path); + return; + } + match value { + Value::Object(object) => { + if let Some(additional) = schema.get("additionalProperties") { + if additional.is_object() { + for (key, member) in object { + assert_declared(spec, additional, member, &format!("{path}.{key}")); + } + } + return; + } + let properties = schema + .get("properties") + .and_then(Value::as_object) + .unwrap_or_else(|| panic!("{path}: the schema declares no properties")); + for (key, member) in object { + let property = properties + .get(key) + .unwrap_or_else(|| panic!("{path}.{key} is serialized but not declared")); + assert_declared(spec, property, member, &format!("{path}.{key}")); + } + for required in schema + .get("required") + .and_then(Value::as_array) + .into_iter() + .flatten() + { + let key = required.as_str().expect("required names are strings"); + assert!( + object.contains_key(key), + "{path}.{key} is required but not serialized" + ); + } + } + Value::Array(items) => { + if let Some(item_schema) = schema.get("items") { + for (index, item) in items.iter().enumerate() { + assert_declared(spec, item_schema, item, &format!("{path}[{index}]")); + } + } + } + Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {} + } +} + +fn assert_same_type() { + assert_eq!( + TypeId::of::(), + TypeId::of::(), + "{} should be the same type as {}", + type_name::(), + type_name::() + ); +} diff --git a/lib/foundation/fabro-types/src/run_projection.rs b/lib/foundation/fabro-types/src/run_projection.rs index f4aa80b63..9f8e446b8 100644 --- a/lib/foundation/fabro-types/src/run_projection.rs +++ b/lib/foundation/fabro-types/src/run_projection.rs @@ -9,6 +9,7 @@ use pebble_coding_agent::events::{ ContextWindowStaleness, ContextWindowWarning, LlmOutputKind, PermissionLevel, SkillActivationSource, SkillSummary, TodoListProjection, ToolSummary, }; +use pebble_coding_agent::projection::SessionProjection; use strum::{Display, EnumString, IntoStaticStr}; use crate::run_event::{AgentSessionActivatedProps, StagePromptProps}; @@ -332,6 +333,19 @@ pub struct StageProjection { pub acp_started_at: Option>, #[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. + /// + /// 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. + /// `activity` reads `running` on stages stored before + /// `agent.processing.end` was kept; `state` is the authority on whether + /// a stage is done. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub agent: Option, pub state: StageState, } @@ -499,6 +513,7 @@ impl StageProjection { inference: None, acp_started_at: None, agent_control: AgentControlState::default(), + agent: None, provider_used: None, diff: None, script_invocation: None, diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES index 88e1306a8..1c312fcf2 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES +++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES @@ -29,9 +29,30 @@ 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 +models/agent-session-compaction.ts +models/agent-session-descendant-account.ts +models/agent-session-failover-stop.ts +models/agent-session-mcp-server.ts +models/agent-session-projection.ts +models/agent-session-prompt-delta.ts +models/agent-session-route-failover.ts +models/agent-session-route.ts +models/agent-session-skills.ts +models/agent-session-subagent-counts.ts +models/agent-session-subagent-status-closed.ts +models/agent-session-subagent-status-completed.ts +models/agent-session-subagent-status-failed.ts +models/agent-session-subagent-status-running.ts +models/agent-session-subagent-status.ts +models/agent-session-subagent.ts +models/agent-session-tool-activity.ts models/agent-tools-available-props.ts models/aggregate-billing-totals.ts models/aggregate-billing.ts @@ -80,6 +101,7 @@ models/close-run-pull-request-response.ts models/code-location.ts models/command-log-response.ts models/command-termination.ts +models/compaction-reason.ts models/completion-content-part.ts models/completion-cost.ts models/completion-message.ts @@ -147,6 +169,8 @@ models/exec-output-tail.ts models/execute-query-request.ts models/execute-query-response-rows-inner-inner.ts models/execute-query-response.ts +models/failover-continuation.ts +models/failover-stop.ts models/failure-category.ts models/failure-detail.ts models/failure-reason.ts @@ -203,6 +227,10 @@ models/interview-provider-settings.ts models/interview-question-record.ts models/link-run-pull-request-request.ts models/llm-output-kind.ts +models/llm-retry-classification-after.ts +models/llm-retry-classification-never.ts +models/llm-retry-classification-safe.ts +models/llm-retry-classification.ts models/log-destination.ts models/manifest-args.ts models/manifest-config.ts @@ -222,6 +250,7 @@ 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 models/mcp-transport-sandbox.ts models/mcp-transport-stdio.ts @@ -531,6 +560,7 @@ models/todo-list-kind.ts models/todo-list-projection.ts models/todo-projection.ts models/todo-status.ts +models/token-usage.ts models/tool-category.ts models/tool-source-application.ts models/tool-source-mcp.ts diff --git a/lib/packages/fabro-api-client/src/models/agent-error-data.ts b/lib/packages/fabro-api-client/src/models/agent-error-data.ts new file mode 100644 index 000000000..3172dd416 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/agent-error-data.ts @@ -0,0 +1,64 @@ +/* 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 { AgentErrorKind } from './agent-error-kind'; +// May contain unused imports in some cases +// @ts-ignore +import type { LlmRetryClassification } from './llm-retry-classification'; + +/** + * A failure as the coding agent\'s event stream carries it: category, safe message, retry advice, provider and model context, and the rendered source chain. Never a raw provider response body. + */ +export interface AgentErrorData { + 'kind': AgentErrorKind; + /** + * The whole failure rendered for a person, cause included. + */ + 'message': string; + /** + * The model-layer category, when a model call failed. + */ + 'llm_kind'?: string; + /** + * Whether repeating the same model call is safe, when a model call failed. + */ + 'retry'?: LlmRetryClassification; + /** + * The provider that produced the failure, when one was selected. + */ + 'provider'?: string; + /** + * The model that produced the failure, when one was selected. + */ + 'model'?: string; + /** + * The HTTP status, when the failure came from an HTTP response. + */ + 'status'?: number; + /** + * The provider\'s own error code, as reported on the wire. + */ + 'provider_code'?: string; + /** + * The provider\'s advised wait in milliseconds. + */ + 'provider_retry_after_millis'?: number; + /** + * The text of each cause below `message`, outermost first. + */ + 'source_chain'?: Array; +} diff --git a/lib/packages/fabro-api-client/src/models/agent-error-kind.ts b/lib/packages/fabro-api-client/src/models/agent-error-kind.ts new file mode 100644 index 000000000..771f9d41b --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/agent-error-kind.ts @@ -0,0 +1,35 @@ +/* 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 stable category of a coding agent failure. + */ + +export const AgentErrorKind = { + LLM: 'llm', + COMPACTION: 'compaction', + AGENT: 'agent', + INVALID_INPUT: 'invalid_input', + SESSION_CLOSED: 'session_closed', + INVALID_STATE: 'invalid_state', + TOOL_EXECUTION: 'tool_execution', + INTERRUPTED: 'interrupted', + TOOL_ROUNDS_EXHAUSTED: 'tool_rounds_exhausted', + TASK: 'task', + EVENT_STREAM: 'event_stream' +} as const; + +export type AgentErrorKind = typeof AgentErrorKind[keyof typeof AgentErrorKind]; diff --git a/lib/packages/fabro-api-client/src/models/agent-session-activated-skill.ts b/lib/packages/fabro-api-client/src/models/agent-session-activated-skill.ts new file mode 100644 index 000000000..f98711cec --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/agent-session-activated-skill.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.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'; + +/** + * A skill the session activated. + */ +export interface AgentSessionActivatedSkill { + 'name': string; + 'source': SkillActivationSource; +} diff --git a/lib/packages/fabro-api-client/src/models/agent-session-activity.ts b/lib/packages/fabro-api-client/src/models/agent-session-activity.ts new file mode 100644 index 000000000..ddbe4363f --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/agent-session-activity.ts @@ -0,0 +1,28 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.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. + */ + + + +/** + * Where a session stands, as its events tell it. + */ + +export const AgentSessionActivity = { + IDLE: 'idle', + RUNNING: 'running', + WAITING_FOR_STEER: 'waiting_for_steer', + ENDED: 'ended' +} as const; + +export type AgentSessionActivity = typeof AgentSessionActivity[keyof typeof AgentSessionActivity]; diff --git a/lib/packages/fabro-api-client/src/models/agent-session-compaction.ts b/lib/packages/fabro-api-client/src/models/agent-session-compaction.ts new file mode 100644 index 000000000..e1c74598d --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/agent-session-compaction.ts @@ -0,0 +1,40 @@ +/* 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 { CompactionReason } from './compaction-reason'; +// May contain unused imports in some cases +// @ts-ignore +import type { TokenUsage } from './token-usage'; + +/** + * One compaction the root session completed. + */ +export interface AgentSessionCompaction { + 'reason': CompactionReason; + 'original_turn_count': number; + 'preserved_turn_count': number; + 'summary_token_estimate': number; + 'tracked_file_count': number; + /** + * The summary call\'s tokens: a breakdown of the session\'s and the prompt\'s usage, which already include them. Zero on compactions recorded before it was kept. + */ + 'usage'?: TokenUsage; + /** + * The summary call\'s provider-reported cost, included in the totals the same way. + */ + 'cost_usd_micros'?: number; +} diff --git a/lib/packages/fabro-api-client/src/models/agent-session-descendant-account.ts b/lib/packages/fabro-api-client/src/models/agent-session-descendant-account.ts new file mode 100644 index 000000000..8b21d712c --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/agent-session-descendant-account.ts @@ -0,0 +1,46 @@ +/* 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 { TokenUsage } from './token-usage'; + +/** + * What one descendant session spent, as its own events reported it. + */ +export interface AgentSessionDescendantAccount { + /** + * The session that spawned it. + */ + 'parent': string; + /** + * The provider it runs on, as its `SessionStarted` reported it. + */ + 'provider'?: string; + /** + * The model it runs on, from its `SessionStarted`; when the start was not seen, the model of its first answer. + */ + 'model'?: string; + 'usage': TokenUsage; + 'cost_usd_micros': number | null; + /** + * Committed assistant messages. + */ + 'messages': number; + /** + * Compactions it completed. + */ + 'compactions': number; +} diff --git a/lib/packages/fabro-api-client/src/models/agent-session-failover-stop.ts b/lib/packages/fabro-api-client/src/models/agent-session-failover-stop.ts new file mode 100644 index 000000000..90ae80cd4 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/agent-session-failover-stop.ts @@ -0,0 +1,40 @@ +/* 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 { AgentErrorData } from './agent-error-data'; +// May contain unused imports in some cases +// @ts-ignore +import type { FailoverStop } from './failover-stop'; + +/** + * Why a prompt stayed on its route and ended there although fallback routes were named. + */ +export interface AgentSessionFailoverStop { + /** + * The `provider/model` the prompt ended on. + */ + 'route': string; + /** + * How many fallback routes the prompt had moved through; `0` on the route it started on. + */ + 'attempt': number; + 'reason': FailoverStop; + /** + * The failure that ended the prompt. + */ + 'error': AgentErrorData; +} diff --git a/lib/packages/fabro-api-client/src/models/agent-session-mcp-server.ts b/lib/packages/fabro-api-client/src/models/agent-session-mcp-server.ts new file mode 100644 index 000000000..5e6d91bed --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/agent-session-mcp-server.ts @@ -0,0 +1,41 @@ +/* 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 { McpToolSummary } from './mcp-tool-summary'; + +/** + * One MCP server the session configured, and whether it has been called. `disconnected` set means the server came up and its connection then closed; otherwise `error` set means it did not start; otherwise it is ready with `tools`. + */ +export interface AgentSessionMcpServer { + 'tools': Array; + /** + * Why it did not start, when it did not. + */ + 'error': string | null; + /** + * Whether any of its tools has been called. + */ + 'invoked': boolean; + /** + * What closed its connection during the session, when it closed. + */ + 'disconnected'?: string; + /** + * Milliseconds from launch to its outcome: to its tools being listed, or to the failure. Absent until either has been seen. + */ + 'startup_ms'?: number; +} diff --git a/lib/packages/fabro-api-client/src/models/agent-session-projection.ts b/lib/packages/fabro-api-client/src/models/agent-session-projection.ts new file mode 100644 index 000000000..2ec0ff6e7 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/agent-session-projection.ts @@ -0,0 +1,131 @@ +/* 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 { AgentSessionActivity } from './agent-session-activity'; +// May contain unused imports in some cases +// @ts-ignore +import type { AgentSessionCompaction } from './agent-session-compaction'; +// May contain unused imports in some cases +// @ts-ignore +import type { AgentSessionDescendantAccount } from './agent-session-descendant-account'; +// May contain unused imports in some cases +// @ts-ignore +import type { AgentSessionFailoverStop } from './agent-session-failover-stop'; +// May contain unused imports in some cases +// @ts-ignore +import type { AgentSessionMcpServer } from './agent-session-mcp-server'; +// May contain unused imports in some cases +// @ts-ignore +import type { AgentSessionPromptDelta } from './agent-session-prompt-delta'; +// May contain unused imports in some cases +// @ts-ignore +import type { AgentSessionRoute } from './agent-session-route'; +// May contain unused imports in some cases +// @ts-ignore +import type { AgentSessionRouteFailover } from './agent-session-route-failover'; +// May contain unused imports in some cases +// @ts-ignore +import type { AgentSessionSkills } from './agent-session-skills'; +// May contain unused imports in some cases +// @ts-ignore +import type { AgentSessionSubagent } from './agent-session-subagent'; +// May contain unused imports in some cases +// @ts-ignore +import type { AgentSessionSubagentCounts } from './agent-session-subagent-counts'; +// May contain unused imports in some cases +// @ts-ignore +import type { AgentSessionToolActivity } from './agent-session-tool-activity'; +// 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 { TodoListProjection } from './todo-list-projection'; +// May contain unused imports in some cases +// @ts-ignore +import type { TokenUsage } from './token-usage'; + +/** + * The coding agent\'s fold of one stage\'s event stream: token counts and provider-reported cost for the root session and each descendant, the route and where it moved, the context window, tools, MCP servers, skills, todo lists, subagents, compactions, files touched, and the prompt in progress. Counts only; pricing a count from the catalog is fabro\'s, and lives in `StageProjection.usage`. + */ +export interface AgentSessionProjection { + /** + * The root session, once an event named it. + */ + 'root_session_id': string | null; + 'route': AgentSessionRoute; + 'activity': AgentSessionActivity; + /** + * The root session\'s usage over the stage. + */ + 'usage': TokenUsage; + /** + * The root session\'s provider-reported cost, when a provider reported one. + */ + 'cost_usd_micros': number | null; + /** + * Committed assistant messages from the root session. + */ + 'messages': number; + /** + * Every descendant session\'s account, by session id. + */ + 'descendants': { [key: string]: AgentSessionDescendantAccount; }; + 'context_window': ContextWindowSnapshot | null; + /** + * Every tool called anywhere in the tree, by the name the model used. + */ + 'tools': { [key: string]: AgentSessionToolActivity; }; + /** + * Model calls retried after a failed attempt, across the tree. + */ + 'retries'?: number; + /** + * Every MCP server the root configured, by name. + */ + 'mcp_servers': { [key: string]: AgentSessionMcpServer; }; + 'skills': AgentSessionSkills; + 'subagent_counts': AgentSessionSubagentCounts; + /** + * Every todo list in the tree, by list id. The root agent\'s own list is the one whose id ends with `root_session_id`. + */ + 'todos': { [key: string]: TodoListProjection; }; + 'subagents': Array; + /** + * The root session\'s compactions, in order. + */ + 'compactions': Array; + /** + * Every move the root made to a fallback route, in order. + */ + 'failovers'?: Array; + 'failover_stopped'?: AgentSessionFailoverStop | null; + /** + * Files written or edited across the tree, sorted. + */ + 'files_touched': Array; + 'last_file_touched': string | null; + /** + * How many prompts have started. + */ + 'prompts': number; + 'prompt': AgentSessionPromptDelta; + /** + * In-flight bookkeeping, not a fact about the session: the paths a write or edit tool call named, by tool call id, between its start and its completion. Present only while such a call is open; a settled projection has no such member. + */ + 'pending_writes'?: { [key: string]: Array; }; +} diff --git a/lib/packages/fabro-api-client/src/models/agent-session-prompt-delta.ts b/lib/packages/fabro-api-client/src/models/agent-session-prompt-delta.ts new file mode 100644 index 000000000..ce0467016 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/agent-session-prompt-delta.ts @@ -0,0 +1,79 @@ +/* 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 { AgentSessionCompaction } from './agent-session-compaction'; +// May contain unused imports in some cases +// @ts-ignore +import type { AgentSessionDescendantAccount } from './agent-session-descendant-account'; +// May contain unused imports in some cases +// @ts-ignore +import type { AgentSessionSubagentCounts } from './agent-session-subagent-counts'; +// 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 { TokenUsage } from './token-usage'; + +/** + * What the prompt in progress, or the last one, did: reset when a prompt starts, complete once `completed` is set. + */ +export interface AgentSessionPromptDelta { + /** + * Whether the prompt reached its end. + */ + 'completed': boolean; + /** + * The root session\'s usage over the prompt. + */ + 'usage': TokenUsage; + 'cost_usd_micros': number | null; + /** + * Committed assistant messages. + */ + 'messages': number; + 'context_window': ContextWindowSnapshot | null; + /** + * Tool calls started, across the tree. + */ + 'tool_calls': number; + /** + * Model calls retried after a failed attempt, across the tree. + */ + 'retries'?: number; + /** + * Moves the root made to a fallback route during the prompt. + */ + 'failovers'?: number; + /** + * What each descendant spent during the prompt, by session id. + */ + 'descendants': { [key: string]: AgentSessionDescendantAccount; }; + /** + * Child lifecycle events during the prompt. + */ + 'subagents': AgentSessionSubagentCounts; + /** + * Compactions the root completed during the prompt. + */ + 'compactions': Array; + /** + * Files written or edited during the prompt, across the tree, sorted. + */ + 'files_touched': Array; + 'last_file_touched': string | null; +} diff --git a/lib/packages/fabro-api-client/src/models/agent-session-route-failover.ts b/lib/packages/fabro-api-client/src/models/agent-session-route-failover.ts new file mode 100644 index 000000000..31c868f7d --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/agent-session-route-failover.ts @@ -0,0 +1,60 @@ +/* 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 { AgentErrorData } from './agent-error-data'; +// May contain unused imports in some cases +// @ts-ignore +import type { FailoverContinuation } from './failover-continuation'; +// May contain unused imports in some cases +// @ts-ignore +import type { TokenUsage } from './token-usage'; + +/** + * One move the root session made to a fallback route, as the stream reported it from the route it moved to. + */ +export interface AgentSessionRouteFailover { + /** + * The `provider/model` that failed. + */ + 'from': string; + /** + * The `provider/model` the prompt continued on. + */ + 'to': string; + /** + * How many routes the prompt had moved through, this one included. + */ + 'attempt': number; + /** + * The failure that ended the previous route. + */ + 'error': AgentErrorData; + /** + * What the prompt spent on the failed route. Already in the session\'s and the prompt\'s totals through that route\'s committed answers: a breakdown, not an addition. + */ + 'usage': TokenUsage; + 'cost_usd_micros'?: number; + /** + * Milliseconds the prompt spent waiting on the failed route\'s model. + */ + 'inference_ms': number; + /** + * Milliseconds the prompt spent running tools on the failed route. + */ + 'tool_ms': number; + 'continuation': FailoverContinuation; +} diff --git a/lib/packages/fabro-api-client/src/models/agent-session-route.ts b/lib/packages/fabro-api-client/src/models/agent-session-route.ts new file mode 100644 index 000000000..6b1ae3d7d --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/agent-session-route.ts @@ -0,0 +1,23 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.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 route a session runs on, as it reported it. + */ +export interface AgentSessionRoute { + 'provider': string | null; + 'model': string | null; +} diff --git a/lib/packages/fabro-api-client/src/models/agent-session-skills.ts b/lib/packages/fabro-api-client/src/models/agent-session-skills.ts new file mode 100644 index 000000000..42fb6d875 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/agent-session-skills.ts @@ -0,0 +1,29 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.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 { AgentSessionActivatedSkill } from './agent-session-activated-skill'; +// May contain unused imports in some cases +// @ts-ignore +import type { SkillSummary } from './skill-summary'; + +/** + * The skills the root session found and the ones activated anywhere in the tree. + */ +export interface AgentSessionSkills { + 'available': Array; + 'activated': Array; +} diff --git a/lib/packages/fabro-api-client/src/models/agent-session-subagent-counts.ts b/lib/packages/fabro-api-client/src/models/agent-session-subagent-counts.ts new file mode 100644 index 000000000..b52d062f6 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/agent-session-subagent-counts.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.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. + */ + + + +/** + * How many child lifecycle events the tree recorded. + */ +export interface AgentSessionSubagentCounts { + 'spawned': number; + 'turns_started': number; + 'completed': number; + 'failed': number; + 'closed': number; +} diff --git a/lib/packages/fabro-api-client/src/models/agent-session-subagent-status-closed.ts b/lib/packages/fabro-api-client/src/models/agent-session-subagent-status-closed.ts new file mode 100644 index 000000000..a5396a395 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/agent-session-subagent-status-closed.ts @@ -0,0 +1,25 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.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 AgentSessionSubagentStatusClosed { + 'status': AgentSessionSubagentStatusClosedStatusEnum; +} + +export const AgentSessionSubagentStatusClosedStatusEnum = { + CLOSED: 'closed' +} as const; + +export type AgentSessionSubagentStatusClosedStatusEnum = typeof AgentSessionSubagentStatusClosedStatusEnum[keyof typeof AgentSessionSubagentStatusClosedStatusEnum]; diff --git a/lib/packages/fabro-api-client/src/models/agent-session-subagent-status-completed.ts b/lib/packages/fabro-api-client/src/models/agent-session-subagent-status-completed.ts new file mode 100644 index 000000000..869af8c88 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/agent-session-subagent-status-completed.ts @@ -0,0 +1,27 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.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 AgentSessionSubagentStatusCompleted { + 'status': AgentSessionSubagentStatusCompletedStatusEnum; + 'success': boolean; + 'turns_used': number; +} + +export const AgentSessionSubagentStatusCompletedStatusEnum = { + COMPLETED: 'completed' +} as const; + +export type AgentSessionSubagentStatusCompletedStatusEnum = typeof AgentSessionSubagentStatusCompletedStatusEnum[keyof typeof AgentSessionSubagentStatusCompletedStatusEnum]; diff --git a/lib/packages/fabro-api-client/src/models/agent-session-subagent-status-failed.ts b/lib/packages/fabro-api-client/src/models/agent-session-subagent-status-failed.ts new file mode 100644 index 000000000..b45d826c8 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/agent-session-subagent-status-failed.ts @@ -0,0 +1,29 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.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 { AgentErrorData } from './agent-error-data'; + +export interface AgentSessionSubagentStatusFailed { + 'status': AgentSessionSubagentStatusFailedStatusEnum; + 'error': AgentErrorData; +} + +export const AgentSessionSubagentStatusFailedStatusEnum = { + FAILED: 'failed' +} as const; + +export type AgentSessionSubagentStatusFailedStatusEnum = typeof AgentSessionSubagentStatusFailedStatusEnum[keyof typeof AgentSessionSubagentStatusFailedStatusEnum]; diff --git a/lib/packages/fabro-api-client/src/models/agent-session-subagent-status-running.ts b/lib/packages/fabro-api-client/src/models/agent-session-subagent-status-running.ts new file mode 100644 index 000000000..645cb74ba --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/agent-session-subagent-status-running.ts @@ -0,0 +1,25 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.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 AgentSessionSubagentStatusRunning { + 'status': AgentSessionSubagentStatusRunningStatusEnum; +} + +export const AgentSessionSubagentStatusRunningStatusEnum = { + RUNNING: 'running' +} as const; + +export type AgentSessionSubagentStatusRunningStatusEnum = typeof AgentSessionSubagentStatusRunningStatusEnum[keyof typeof AgentSessionSubagentStatusRunningStatusEnum]; diff --git a/lib/packages/fabro-api-client/src/models/agent-session-subagent-status.ts b/lib/packages/fabro-api-client/src/models/agent-session-subagent-status.ts new file mode 100644 index 000000000..5a0341f49 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/agent-session-subagent-status.ts @@ -0,0 +1,36 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.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 { AgentErrorData } from './agent-error-data'; +// May contain unused imports in some cases +// @ts-ignore +import type { AgentSessionSubagentStatusClosed } from './agent-session-subagent-status-closed'; +// May contain unused imports in some cases +// @ts-ignore +import type { AgentSessionSubagentStatusCompleted } from './agent-session-subagent-status-completed'; +// May contain unused imports in some cases +// @ts-ignore +import type { AgentSessionSubagentStatusFailed } from './agent-session-subagent-status-failed'; +// May contain unused imports in some cases +// @ts-ignore +import type { AgentSessionSubagentStatusRunning } from './agent-session-subagent-status-running'; + +/** + * @type AgentSessionSubagentStatus + * Where a child stands. + */ +export type AgentSessionSubagentStatus = { status: 'closed' } & AgentSessionSubagentStatusClosed | { status: 'completed' } & AgentSessionSubagentStatusCompleted | { status: 'failed' } & AgentSessionSubagentStatusFailed | { status: 'running' } & AgentSessionSubagentStatusRunning; diff --git a/lib/packages/fabro-api-client/src/models/agent-session-subagent.ts b/lib/packages/fabro-api-client/src/models/agent-session-subagent.ts new file mode 100644 index 000000000..6710e1f66 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/agent-session-subagent.ts @@ -0,0 +1,28 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.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 { AgentSessionSubagentStatus } from './agent-session-subagent-status'; + +/** + * One child the root spawned. A reused child stays one row; every event after the spawn moves its status. + */ +export interface AgentSessionSubagent { + 'agent_id': string; + 'depth': number; + 'task': string; + 'status': AgentSessionSubagentStatus; +} diff --git a/lib/packages/fabro-api-client/src/models/agent-session-tool-activity.ts b/lib/packages/fabro-api-client/src/models/agent-session-tool-activity.ts new file mode 100644 index 000000000..1a08d6996 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/agent-session-tool-activity.ts @@ -0,0 +1,33 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.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. + */ + + + +/** + * How one tool has been used across the tree. + */ +export interface AgentSessionToolActivity { + /** + * Calls started. + */ + 'calls': number; + /** + * Calls that completed as errors. + */ + 'errors': number; + /** + * Calls started and not yet completed. + */ + 'open': number; +} diff --git a/lib/packages/fabro-api-client/src/models/compaction-reason.ts b/lib/packages/fabro-api-client/src/models/compaction-reason.ts new file mode 100644 index 000000000..26fb5035a --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/compaction-reason.ts @@ -0,0 +1,27 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.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. + */ + + + +/** + * Why a conversation compaction ran. + */ + +export const CompactionReason = { + THRESHOLD: 'threshold', + MANUAL: 'manual', + OVERFLOW: 'overflow' +} as const; + +export type CompactionReason = typeof CompactionReason[keyof typeof CompactionReason]; diff --git a/lib/packages/fabro-api-client/src/models/failover-continuation.ts b/lib/packages/fabro-api-client/src/models/failover-continuation.ts new file mode 100644 index 000000000..397341e47 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/failover-continuation.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.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. + */ + + + +/** + * How a prompt carries on after a failover. `replay_prompt` when nothing the prompt committed is in the conversation, so the new route is asked the prompt again; `continue_turn` when the conversation holds output or tool results this prompt committed, so the new route continues the turn from where it stood. + */ + +export const FailoverContinuation = { + REPLAY_PROMPT: 'replay_prompt', + CONTINUE_TURN: 'continue_turn' +} as const; + +export type FailoverContinuation = typeof FailoverContinuation[keyof typeof FailoverContinuation]; diff --git a/lib/packages/fabro-api-client/src/models/failover-stop.ts b/lib/packages/fabro-api-client/src/models/failover-stop.ts new file mode 100644 index 000000000..50412cf28 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/failover-stop.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.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. + */ + + + +/** + * Why a model failure ends a prompt on its route when fallback routes were named. `ineligible` when the failure follows the request, so another route would fail the same way; `exhausted` when every named route has been taken. + */ + +export const FailoverStop = { + INELIGIBLE: 'ineligible', + EXHAUSTED: 'exhausted' +} as const; + +export type FailoverStop = typeof FailoverStop[keyof typeof FailoverStop]; diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index a0e1730fa..5771fc062 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -1,8 +1,29 @@ 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'; +export * from './agent-session-compaction'; +export * from './agent-session-descendant-account'; +export * from './agent-session-failover-stop'; +export * from './agent-session-mcp-server'; +export * from './agent-session-projection'; +export * from './agent-session-prompt-delta'; +export * from './agent-session-route'; +export * from './agent-session-route-failover'; +export * from './agent-session-skills'; +export * from './agent-session-subagent'; +export * from './agent-session-subagent-counts'; +export * from './agent-session-subagent-status'; +export * from './agent-session-subagent-status-closed'; +export * from './agent-session-subagent-status-completed'; +export * from './agent-session-subagent-status-failed'; +export * from './agent-session-subagent-status-running'; +export * from './agent-session-tool-activity'; export * from './agent-tools-available-props'; export * from './aggregate-billing'; export * from './aggregate-billing-totals'; @@ -51,6 +72,7 @@ export * from './close-run-pull-request-response'; export * from './code-location'; export * from './command-log-response'; export * from './command-termination'; +export * from './compaction-reason'; export * from './completion-content-part'; export * from './completion-cost'; export * from './completion-message'; @@ -118,6 +140,8 @@ export * from './exec-output-tail'; export * from './execute-query-request'; export * from './execute-query-response'; export * from './execute-query-response-rows-inner-inner'; +export * from './failover-continuation'; +export * from './failover-stop'; export * from './failure-category'; export * from './failure-detail'; export * from './failure-reason'; @@ -173,6 +197,10 @@ export * from './interview-provider-settings'; export * from './interview-question-record'; export * from './link-run-pull-request-request'; export * from './llm-output-kind'; +export * from './llm-retry-classification'; +export * from './llm-retry-classification-after'; +export * from './llm-retry-classification-never'; +export * from './llm-retry-classification-safe'; export * from './log-destination'; export * from './manifest-args'; export * from './manifest-config'; @@ -192,6 +220,7 @@ 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'; export * from './mcp-transport-sandbox'; @@ -501,6 +530,7 @@ export * from './todo-list-kind'; export * from './todo-list-projection'; export * from './todo-projection'; export * from './todo-status'; +export * from './token-usage'; export * from './tool-category'; export * from './tool-source'; export * from './tool-source-application'; diff --git a/lib/packages/fabro-api-client/src/models/llm-retry-classification-after.ts b/lib/packages/fabro-api-client/src/models/llm-retry-classification-after.ts new file mode 100644 index 000000000..a05044dcd --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/llm-retry-classification-after.ts @@ -0,0 +1,32 @@ +/* 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. + */ + + + +/** + * Repeating the call is safe after the given delay. + */ +export interface LlmRetryClassificationAfter { + 'type': LlmRetryClassificationAfterTypeEnum; + /** + * The delay in milliseconds. + */ + 'after_millis': number; +} + +export const LlmRetryClassificationAfterTypeEnum = { + AFTER: 'after' +} as const; + +export type LlmRetryClassificationAfterTypeEnum = typeof LlmRetryClassificationAfterTypeEnum[keyof typeof LlmRetryClassificationAfterTypeEnum]; diff --git a/lib/packages/fabro-api-client/src/models/llm-retry-classification-never.ts b/lib/packages/fabro-api-client/src/models/llm-retry-classification-never.ts new file mode 100644 index 000000000..0729e7f9b --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/llm-retry-classification-never.ts @@ -0,0 +1,28 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.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. + */ + + + +/** + * Repeating the call cannot succeed. + */ +export interface LlmRetryClassificationNever { + 'type': LlmRetryClassificationNeverTypeEnum; +} + +export const LlmRetryClassificationNeverTypeEnum = { + NEVER: 'never' +} as const; + +export type LlmRetryClassificationNeverTypeEnum = typeof LlmRetryClassificationNeverTypeEnum[keyof typeof LlmRetryClassificationNeverTypeEnum]; diff --git a/lib/packages/fabro-api-client/src/models/llm-retry-classification-safe.ts b/lib/packages/fabro-api-client/src/models/llm-retry-classification-safe.ts new file mode 100644 index 000000000..d3f8da578 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/llm-retry-classification-safe.ts @@ -0,0 +1,28 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.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. + */ + + + +/** + * Repeating the call is safe on the caller\'s own schedule. + */ +export interface LlmRetryClassificationSafe { + 'type': LlmRetryClassificationSafeTypeEnum; +} + +export const LlmRetryClassificationSafeTypeEnum = { + SAFE: 'safe' +} as const; + +export type LlmRetryClassificationSafeTypeEnum = typeof LlmRetryClassificationSafeTypeEnum[keyof typeof LlmRetryClassificationSafeTypeEnum]; diff --git a/lib/packages/fabro-api-client/src/models/llm-retry-classification.ts b/lib/packages/fabro-api-client/src/models/llm-retry-classification.ts new file mode 100644 index 000000000..a75037097 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/llm-retry-classification.ts @@ -0,0 +1,30 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.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 { LlmRetryClassificationAfter } from './llm-retry-classification-after'; +// May contain unused imports in some cases +// @ts-ignore +import type { LlmRetryClassificationNever } from './llm-retry-classification-never'; +// May contain unused imports in some cases +// @ts-ignore +import type { LlmRetryClassificationSafe } from './llm-retry-classification-safe'; + +/** + * @type LlmRetryClassification + * Whether repeating the same resolved model call is safe. + */ +export type LlmRetryClassification = { type: 'after' } & LlmRetryClassificationAfter | { type: 'never' } & LlmRetryClassificationNever | { type: 'safe' } & LlmRetryClassificationSafe; diff --git a/lib/packages/fabro-api-client/src/models/mcp-tool-summary.ts b/lib/packages/fabro-api-client/src/models/mcp-tool-summary.ts new file mode 100644 index 000000000..21d752a04 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/mcp-tool-summary.ts @@ -0,0 +1,29 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.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. + */ + + + +/** + * One tool an MCP server advertised, as the coding agent\'s registry named it. + */ +export interface McpToolSummary { + /** + * The name the model calls: `mcp__{server}__{tool}`. + */ + 'name': string; + /** + * The server\'s own name for the tool. + */ + 'original_name': string; +} 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 aa97b10b4..ccf1d548f 100644 --- a/lib/packages/fabro-api-client/src/models/stage-projection.ts +++ b/lib/packages/fabro-api-client/src/models/stage-projection.ts @@ -18,6 +18,9 @@ import type { AgentControlState } from './agent-control-state'; // May contain unused imports in some cases // @ts-ignore +import type { AgentSessionProjection } from './agent-session-projection'; +// May contain unused imports in some cases +// @ts-ignore import type { BilledTokenCounts } from './billed-token-counts'; // May contain unused imports in some cases // @ts-ignore @@ -142,6 +145,7 @@ export interface StageProjection { * Whether the agent is executing normally or waiting for steering after an interrupt. */ 'agent_control': AgentControlState; + 'agent'?: AgentSessionProjection | null; /** * Lifecycle state of the stage projection. */ diff --git a/lib/packages/fabro-api-client/src/models/token-usage.ts b/lib/packages/fabro-api-client/src/models/token-usage.ts new file mode 100644 index 000000000..1a9946f89 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/token-usage.ts @@ -0,0 +1,41 @@ +/* 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. + */ + + + +/** + * Token accounting as the coding agent counts it. The five buckets are disjoint: every token is counted in exactly one, so their plain sum is the total. A bucket that is absent reads as zero. + */ +export interface TokenUsage { + /** + * Prompt tokens that were neither read from nor written to a cache. + */ + 'input'?: number; + /** + * Completion tokens that are not reasoning tokens. + */ + 'output'?: number; + /** + * Completion tokens spent on reasoning, billed at the output rate. + */ + 'reasoning'?: number; + /** + * Prompt tokens served from a provider cache. + */ + 'cache_read'?: number; + /** + * Prompt tokens written into a provider cache. + */ + 'cache_write'?: number; +}