Merge pull request #863 from fabro-sh/stage-view-from-agent

Read the stage view from the agent's fold
This commit is contained in:
Bryan Helmkamp 2026-09-13 10:36:21 -04:00 • committed by GitHub
commit f146312cda
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 683 additions and 2843 deletions

View file

@ -3,17 +3,25 @@ import TestRenderer, { act } from "react-test-renderer";
import { MemoryRouter } from "react-router";
import {
AgentSessionActivity,
CompactionReason,
ContextWindowCategory,
ContextWindowCountMethod,
ContextWindowStaleness,
FailoverContinuation,
FailoverStop,
SkillActivationSource,
TodoListKind,
TodoStatus,
ToolCategory,
} from "@qltysh/fabro-api-client";
import type {
AgentErrorData,
AgentSessionProjection,
McpToolSummary,
StageContextWindow,
StageProjection,
TokenUsage,
} from "@qltysh/fabro-api-client";
import { StageInsightsSidebar } from "./stage-insights-sidebar";
@ -33,6 +41,58 @@ function makeStage(overrides: Partial<StageProjection> = {}): StageProjection {
};
}
const NO_TOKENS: TokenUsage = { input: 0, output: 0, reasoning: 0, cache_read: 0, cache_write: 0 };
/** The coding agent's fold of a stage that has seen nothing yet. */
function makeAgent(overrides: Partial<AgentSessionProjection> = {}): AgentSessionProjection {
return {
root_session_id: "ses_root",
route: { provider: "anthropic", model: "claude-opus-4-7" },
activity: AgentSessionActivity.RUNNING,
usage: NO_TOKENS,
cost_usd_micros: null,
messages: 0,
descendants: {},
context_window: null,
tools: {},
mcp_servers: {},
skills: { available: [], activated: [] },
subagent_counts: { spawned: 0, turns_started: 0, completed: 0, failed: 0, closed: 0 },
todos: {},
subagents: [],
compactions: [],
failovers: [],
files_touched: [],
last_file_touched: null,
prompts: 1,
prompt: {
completed: false,
usage: NO_TOKENS,
cost_usd_micros: null,
messages: 0,
context_window: null,
tool_calls: 0,
descendants: {},
subagents: { spawned: 0, turns_started: 0, completed: 0, failed: 0, closed: 0 },
compactions: [],
files_touched: [],
last_file_touched: null,
},
...overrides,
};
}
function mcpTools(count: number): McpToolSummary[] {
return Array.from({ length: count }, (_, i) => ({
name: `mcp__server__tool_${i}`,
original_name: `tool_${i}`,
}));
}
function agentError(message: string): AgentErrorData {
return { kind: "llm", message };
}
function makeContextWindow(overrides: Partial<StageContextWindow> = {}): StageContextWindow {
return {
stage_id: "implement@1",
@ -60,13 +120,13 @@ function makeContextWindow(overrides: Partial<StageContextWindow> = {}): StageCo
// bun:test runs in a node-like env without a DOM, so shim `window.localStorage`
// once — the sidebar feature-detects `typeof window` to decide whether to
// persist collapse state. Seeding the shim lets us open default-collapsed
// sections (Skills, MCPs) in the assertions below. Other test files (e.g.
// sections in the assertions below. Other test files (e.g.
// services-panel.test.tsx) install their own window and rely on
// `delete globalThis.window` cleanup, so this descriptor stays configurable.
let restoreWindow: (() => void) | null = null;
beforeAll(() => {
const store = new Map<string, string>();
for (const key of ["todos", "context", "tools", "skills", "mcps"]) {
for (const key of ["todos", "context", "files", "subagents", "tools", "skills", "mcps"]) {
store.set(`fabro:stage-insights-section:${key}`, "1");
}
const stub = {
@ -108,23 +168,34 @@ function render(stage: StageProjection | undefined, contextWindow: StageContextW
}
describe("StageInsightsSidebar", () => {
test("renders todo done/total ratio", () => {
test("renders the root agent's todo list and counts the subagent lists apart", () => {
const stage = makeStage({
todos: {
kind: TodoListKind.ANTHROPIC_TASKS,
list_id: "anthropic_tasks:root",
items: [
{ id: "1", status: TodoStatus.COMPLETED, order: 0, subject: "Plan refactor" },
{ id: "2", status: TodoStatus.COMPLETED, order: 1, subject: "Add tests" },
{ id: "3", status: TodoStatus.IN_PROGRESS, order: 2, subject: "Land migration" },
{ id: "4", status: TodoStatus.PENDING, order: 3, subject: "Review with Kieran" },
],
},
agent: makeAgent({
todos: {
"anthropic_tasks:ses_root": {
kind: TodoListKind.ANTHROPIC_TASKS,
list_id: "anthropic_tasks:ses_root",
items: [
{ id: "1", status: TodoStatus.COMPLETED, order: 0, subject: "Plan refactor" },
{ id: "2", status: TodoStatus.COMPLETED, order: 1, subject: "Add tests" },
{ id: "3", status: TodoStatus.IN_PROGRESS, order: 2, subject: "Land migration" },
{ id: "4", status: TodoStatus.PENDING, order: 3, subject: "Review with Kieran" },
],
},
"openai_plan:ses_child": {
kind: TodoListKind.OPENAI_PLAN,
list_id: "openai_plan:ses_child",
items: [{ id: "c1", status: TodoStatus.PENDING, order: 0, subject: "Child plan step" }],
},
},
}),
});
const dom = render(stage, null);
expect(dom).toContain("2/4");
expect(dom).toContain("Plan refactor");
expect(dom).toContain("Land migration");
expect(dom).not.toContain("Child plan step");
expect(dom).toContain("+1 subagent list");
});
test("renders context window percent and breakdown labels", () => {
@ -147,6 +218,24 @@ describe("StageInsightsSidebar", () => {
expect(dom).not.toContain("31%");
});
test("renders the compactions row under the context window", () => {
const stage = makeStage({
agent: makeAgent({
compactions: [
{
reason: CompactionReason.THRESHOLD,
original_turn_count: 20,
preserved_turn_count: 6,
summary_token_estimate: 500,
tracked_file_count: 3,
},
],
}),
});
const dom = render(stage, makeContextWindow());
expect(dom).toContain("1 compaction · last kept 6 of 20 turns");
});
test("renders projected agent tool names and invoked state", () => {
const dom = render(
makeStage({
@ -179,29 +268,16 @@ describe("StageInsightsSidebar", () => {
expect(dom).toContain("Used");
});
test("renders mcp server used/total count, marks invoked servers as 'used'", () => {
test("derives mcp server status from the fold and marks invoked servers as 'used'", () => {
const dom = render(
makeStage({
mcp_servers: [
{
server_name: "context7",
tool_count: 12,
status: { kind: "ready", tools: [] },
invoked: true,
agent: makeAgent({
mcp_servers: {
context7: { tools: mcpTools(12), error: null, invoked: true },
filesystem: { tools: mcpTools(3), error: null, invoked: false },
atlassian: { tools: [], error: "auth failed", invoked: false },
},
{
server_name: "filesystem",
tool_count: 3,
status: { kind: "ready", tools: [] },
invoked: false,
},
{
server_name: "atlassian",
tool_count: 0,
status: { kind: "failed", error: "auth failed" },
invoked: false,
},
],
}),
}),
null,
);
@ -213,19 +289,17 @@ describe("StageInsightsSidebar", () => {
expect(dom).toContain("3 tools");
expect(dom).toContain("atlassian");
expect(dom).toContain("Failed");
expect(dom).toContain("auth failed");
});
test("renders a disconnected mcp server as disconnected, still counted as used", () => {
const dom = render(
makeStage({
mcp_servers: [
{
server_name: "github",
tool_count: 4,
status: { kind: "disconnected", error: "transport closed" },
invoked: true,
agent: makeAgent({
mcp_servers: {
github: { tools: mcpTools(4), error: null, invoked: true, disconnected: "transport closed" },
},
],
}),
}),
null,
);
@ -238,18 +312,20 @@ describe("StageInsightsSidebar", () => {
test("shows skill activated/available ratio with source label", () => {
const dom = render(
makeStage({
skills: {
activated: [
{ name: "frontend-design", source: SkillActivationSource.SLASH },
{ name: "debug", source: SkillActivationSource.TOOL },
],
available: [
{ name: "frontend-design", description: "" },
{ name: "debug", description: "" },
{ name: "tdd", description: "" },
{ name: "ce-review", description: "" },
],
},
agent: makeAgent({
skills: {
activated: [
{ name: "frontend-design", source: SkillActivationSource.SLASH },
{ name: "debug", source: SkillActivationSource.TOOL },
],
available: [
{ name: "frontend-design", description: "" },
{ name: "debug", description: "" },
{ name: "tdd", description: "" },
{ name: "ce-review", description: "" },
],
},
}),
}),
null,
);
@ -259,9 +335,83 @@ describe("StageInsightsSidebar", () => {
expect(dom).toContain("+2 more available");
});
test("lists the files the session tree wrote and marks the last one", () => {
const dom = render(
makeStage({
agent: makeAgent({
files_touched: ["/workspace/src/lib.rs", "/workspace/src/main.rs"],
last_file_touched: "/workspace/src/main.rs",
}),
}),
null,
);
expect(dom).toContain("src/lib.rs");
expect(dom).toContain("src/main.rs");
expect(dom).toContain("last");
// Full paths stay in the tooltip.
expect(dom).toContain("/workspace/src/lib.rs");
});
test("renders subagents with their status", () => {
const dom = render(
makeStage({
agent: makeAgent({
subagents: [
{ agent_id: "sub-1", depth: 1, task: "Review the module", status: { status: "completed", success: true, turns_used: 3 } },
{ agent_id: "sub-2", depth: 1, task: "Check the tests", status: { status: "failed", error: agentError("boom") } },
{ agent_id: "sub-3", depth: 1, task: "Still looking", status: { status: "running" } },
],
}),
}),
null,
);
expect(dom).toContain("2/3");
expect(dom).toContain("Review the module");
expect(dom).toContain("3 turns");
expect(dom).toContain("Check the tests");
expect(dom).toContain("Failed");
expect(dom).toContain("boom");
expect(dom).toContain("Still looking");
expect(dom).toContain("running");
});
test("shows a failover badge with the route the session moved to and why it stopped", () => {
const dom = render(
makeStage({
agent: makeAgent({
route: { provider: "openai", model: "gpt-5.4" },
failovers: [
{
from: "anthropic/claude-opus-4-7",
to: "openai/gpt-5.4",
attempt: 1,
error: agentError("rate limited"),
usage: NO_TOKENS,
inference_ms: 120,
tool_ms: 30,
continuation: FailoverContinuation.CONTINUE_TURN,
},
],
failover_stopped: {
route: "openai/gpt-5.4",
attempt: 1,
reason: FailoverStop.EXHAUSTED,
error: agentError("key revoked"),
},
}),
}),
null,
);
expect(dom).toContain("Moved to openai/gpt-5.4 after 1 attempt");
expect(dom).toContain("Stopped: routes exhausted");
expect(dom).toContain("rate limited");
expect(dom).toContain("key revoked");
});
test("renders empty-friendly content when stage projection is missing", () => {
const dom = render(undefined, null);
// sidebar still renders even with no data
expect(dom).toContain("Agent");
expect(dom).not.toContain("Moved to");
});
});

View file

@ -11,24 +11,33 @@ import {
XCircleIcon,
} from "@heroicons/react/24/solid";
import {
ArrowsRightLeftIcon,
CheckBadgeIcon,
CommandLineIcon,
DocumentTextIcon,
ListBulletIcon,
PuzzlePieceIcon,
ServerStackIcon,
Squares2X2Icon,
UserGroupIcon,
WrenchScrewdriverIcon,
} from "@heroicons/react/24/outline";
import {
ContextWindowCategory,
ContextWindowStaleness,
FailoverStop,
SkillActivationSource,
TodoStatus,
} from "@qltysh/fabro-api-client";
import type {
ActivatedSkill,
AgentSessionActivatedSkill,
AgentSessionCompaction,
AgentSessionFailoverStop,
AgentSessionMcpServer,
AgentSessionProjection,
AgentSessionRouteFailover,
AgentSessionSubagent,
ContextWindowBreakdownItem,
McpServerProjection,
SkillSummary,
StageContextWindow,
StageProjection,
@ -41,14 +50,16 @@ import { formatTokenCount } from "../lib/format";
const COLLAPSED_STORAGE_KEY = "fabro:stage-insights-sidebar-collapsed";
const SECTION_STORAGE_PREFIX = "fabro:stage-insights-section:";
type SectionKey = "todos" | "context" | "tools" | "skills" | "mcps";
type SectionKey = "todos" | "context" | "files" | "subagents" | "tools" | "skills" | "mcps";
const SECTIONS_DEFAULT_OPEN: Record<SectionKey, boolean> = {
todos: true,
context: false,
tools: false,
skills: false,
mcps: false,
todos: true,
context: false,
files: false,
subagents: false,
tools: false,
skills: false,
mcps: false,
};
export interface StageInsightsSidebarProps {
@ -58,6 +69,11 @@ export interface StageInsightsSidebarProps {
contextWindow: StageContextWindow | null | undefined;
}
/**
* The agent stage's sidebar. Everything about the agent's session comes from
* `stage.agent`, the coding agent's own fold of the stage's events; the
* stage itself contributes the tool catalog it was handed.
*/
export function StageInsightsSidebar({ stage, contextWindow }: StageInsightsSidebarProps) {
const [collapsed, setCollapsed] = useState(loadStoredCollapsed);
const toggleCollapsed = useCallback(() => {
@ -68,14 +84,24 @@ export function StageInsightsSidebar({ stage, contextWindow }: StageInsightsSide
});
}, []);
const todos = stage?.todos ?? null;
const skills = stage?.skills ?? { activated: [], available: [] };
const agent = stage?.agent ?? null;
const todoLists = agent ? Object.values(agent.todos) : [];
const todos = rootTodoList(agent);
const otherTodoLists = todos ? todoLists.length - 1 : todoLists.length;
const skills = agent?.skills ?? { activated: [], available: [] };
const agentTools = stage?.agent_tools ?? [];
const mcpServers = stage?.mcp_servers ?? [];
const mcpServers = agent ? mcpServerRows(agent.mcp_servers) : [];
const files = agent?.files_touched ?? [];
const lastFile = agent?.last_file_touched ?? null;
const subagents = agent?.subagents ?? [];
const failovers = agent?.failovers ?? [];
const failoverStopped = agent?.failover_stopped ?? null;
const compactions = agent?.compactions ?? [];
const todoStats = countTodoStats(todos);
const activatedSkillNames = new Set(skills.activated.map((s) => s.name));
const invokedToolCount = agentTools.filter((tool) => tool.invoked).length;
const finishedSubagents = subagents.filter((s) => s.status.status !== "running").length;
return (
<aside
@ -104,6 +130,8 @@ export function StageInsightsSidebar({ stage, contextWindow }: StageInsightsSide
</button>
</div>
<FailoverBadge collapsed={collapsed} failovers={failovers} stopped={failoverStopped} />
<div className="mt-3 flex flex-col gap-4">
{todoStats.total > 0 && (
<CollapsibleSection
@ -114,11 +142,39 @@ export function StageInsightsSidebar({ stage, contextWindow }: StageInsightsSide
count={`${todoStats.done}/${todoStats.total}`}
empty={false}
>
<TodoSection todos={todos} />
<TodoSection todos={todos} otherLists={otherTodoLists} />
</CollapsibleSection>
)}
<ContextWindowSection collapsed={collapsed} snapshot={contextWindow ?? null} />
<ContextWindowSection
collapsed={collapsed}
snapshot={contextWindow ?? null}
compactions={compactions}
/>
<CollapsibleSection
sectionKey="files"
title="Files"
icon={DocumentTextIcon}
collapsed={collapsed}
count={files.length}
empty={files.length === 0}
hideCountWhenCollapsed
>
<FilesSection files={files} lastFile={lastFile} />
</CollapsibleSection>
<CollapsibleSection
sectionKey="subagents"
title="Subagents"
icon={UserGroupIcon}
collapsed={collapsed}
count={`${finishedSubagents}/${subagents.length}`}
empty={subagents.length === 0}
hideCountWhenCollapsed
>
<SubagentsSection subagents={subagents} />
</CollapsibleSection>
<CollapsibleSection
sectionKey="skills"
@ -228,6 +284,52 @@ function CollapsibleSection({
);
}
// ---------- Failover ----------
interface FailoverBadgeProps {
collapsed: boolean;
failovers: AgentSessionRouteFailover[];
stopped: AgentSessionFailoverStop | null;
}
/**
* Where the session's model moved: the last fallback route the root took,
* and whether the prompt then stopped although routes were named. The
* failures that caused each are the hover text.
*/
function FailoverBadge({ collapsed, failovers, stopped }: FailoverBadgeProps) {
const last = failovers.length > 0 ? failovers[failovers.length - 1] : null;
if (!last && !stopped) return null;
const details = [
...failovers.map((move) => `${move.from} failed: ${move.error.message}`),
stopped ? `${stopped.route}: ${stopped.error.message}` : null,
].filter((line): line is string => line != null);
if (collapsed) {
return (
<div className="mt-2 flex justify-center" title={details.join("\n")}>
<ArrowsRightLeftIcon className="size-4 shrink-0 text-amber" aria-label="Model failover" />
</div>
);
}
return (
<div className="mt-2 space-y-0.5 px-2 text-[11px] text-amber" title={details.join("\n")}>
{last && (
<p className="flex items-center gap-1">
<ArrowsRightLeftIcon className="size-3.5 shrink-0" aria-hidden="true" />
<span className="min-w-0 truncate">
{`Moved to ${last.to} after ${last.attempt} ${last.attempt === 1 ? "attempt" : "attempts"}`}
</span>
</p>
)}
{stopped && (
<p className="pl-4.5">
{`Stopped: ${stopped.reason === FailoverStop.EXHAUSTED ? "routes exhausted" : "failure not eligible for failover"}`}
</p>
)}
</div>
);
}
// ---------- Todos ----------
interface TodoStats {
@ -235,6 +337,18 @@ interface TodoStats {
total: number;
}
/**
* The root agent's own list: the one keyed by the root session's id. Subagent
* plans are separate lists in the same map.
*/
function rootTodoList(agent: AgentSessionProjection | null): TodoListProjection | null {
if (!agent) return null;
const lists = Object.values(agent.todos);
const rootId = agent.root_session_id;
const root = rootId ? lists.find((list) => list.list_id.endsWith(rootId)) : undefined;
return root ?? (lists.length === 1 ? lists[0] : null);
}
function countTodoStats(list: TodoListProjection | null): TodoStats {
const items = list?.items ?? [];
let done = 0;
@ -244,16 +358,23 @@ function countTodoStats(list: TodoListProjection | null): TodoStats {
return { done, total: items.length };
}
function TodoSection({ todos }: { todos: TodoListProjection | null }) {
function TodoSection({ todos, otherLists }: { todos: TodoListProjection | null; otherLists: number }) {
if (!todos || (todos.items?.length ?? 0) === 0) return <p className="text-xs text-fg-muted">No todos.</p>;
const items = Array.from(todos.items ?? []);
items.sort((a, b) => a.order - b.order);
return (
<ul className="space-y-1">
{items.map((item) => (
<TodoRow key={item.id} todo={item} />
))}
</ul>
<div className="space-y-2">
<ul className="space-y-1">
{items.map((item) => (
<TodoRow key={item.id} todo={item} />
))}
</ul>
{otherLists > 0 && (
<p className="text-[11px] text-fg-muted">
{`+${otherLists} subagent ${otherLists === 1 ? "list" : "lists"}`}
</p>
)}
</div>
);
}
@ -297,9 +418,10 @@ function EmptyCircleIcon({ className }: { className?: string }) {
interface ContextWindowSectionProps {
collapsed: boolean;
snapshot: StageContextWindow | null;
compactions: AgentSessionCompaction[];
}
function ContextWindowSection({ collapsed, snapshot }: ContextWindowSectionProps) {
function ContextWindowSection({ collapsed, snapshot, compactions }: ContextWindowSectionProps) {
const [open, setOpen] = useState(() => loadStoredSectionOpen("context"));
const toggle = useCallback(() => {
setOpen((prev) => {
@ -343,12 +465,25 @@ function ContextWindowSection({ collapsed, snapshot }: ContextWindowSectionProps
<ContextBar snapshot={snapshot} />
</div>
<ContextBreakdown snapshot={snapshot} />
<CompactionsRow compactions={compactions} />
</>
)}
</div>
);
}
/** How often the conversation was compacted, and what the last one kept. */
function CompactionsRow({ compactions }: { compactions: AgentSessionCompaction[] }) {
if (compactions.length === 0) return null;
const last = compactions[compactions.length - 1];
const noun = compactions.length === 1 ? "compaction" : "compactions";
return (
<p className="mt-2 px-2 text-[11px] text-fg-muted" title={`Last compaction: ${last.reason}`}>
{`${compactions.length} ${noun} · last kept ${last.preserved_turn_count} of ${last.original_turn_count} turns`}
</p>
);
}
function ContextBar({ snapshot }: { snapshot: StageContextWindow | null }) {
if (!snapshot || snapshot.usage_percent == null) {
return (
@ -484,10 +619,102 @@ function categoryLabel(category: ContextWindowCategory): string {
}
}
// ---------- Files ----------
/** Files the stage's agent and its subagents wrote or edited, sorted. */
function FilesSection({ files, lastFile }: { files: string[]; lastFile: string | null }) {
if (files.length === 0) return <p className="text-xs text-fg-muted">No files written.</p>;
return (
<ul className="space-y-1">
{files.map((path) => {
const isLast = path === lastFile;
return (
<li key={path} title={path} className="flex items-center gap-1.5">
<DocumentTextIcon className="size-3.5 shrink-0 text-fg-muted" aria-hidden="true" />
<span className={`min-w-0 flex-1 truncate font-mono text-[11px] ${isLast ? "text-fg-2" : "text-fg-3"}`}>
{fileLabel(path)}
</span>
{isLast && (
<span className="text-[10px] uppercase tracking-wider text-fg-muted">last</span>
)}
</li>
);
})}
</ul>
);
}
/** The path from its last two segments, so a deep tree still reads. */
function fileLabel(path: string): string {
const segments = path.split("/").filter((segment) => segment.length > 0);
return segments.length <= 2 ? path : segments.slice(-2).join("/");
}
// ---------- Subagents ----------
function SubagentsSection({ subagents }: { subagents: AgentSessionSubagent[] }) {
if (subagents.length === 0) return <p className="text-xs text-fg-muted">No subagents.</p>;
return (
<ul className="space-y-1">
{subagents.map((subagent) => (
<SubagentRow key={subagent.agent_id} subagent={subagent} />
))}
</ul>
);
}
function SubagentRow({ subagent }: { subagent: AgentSessionSubagent }) {
const { status } = subagent;
const title = status.status === "failed" ? `${subagent.task}\n${status.error.message}` : subagent.task;
return (
<li className="flex items-center gap-1.5" title={title}>
<SubagentStatusIcon subagent={subagent} />
<span className="min-w-0 flex-1 truncate text-xs text-fg-2">{subagent.task}</span>
<SubagentStatusBadge subagent={subagent} />
</li>
);
}
function SubagentStatusIcon({ subagent }: { subagent: AgentSessionSubagent }) {
const { status } = subagent;
switch (status.status) {
case "running":
return <ArrowPathIcon className="size-3.5 shrink-0 animate-spin text-teal-500" aria-label="Running" />;
case "completed":
return status.success ? (
<CheckCircleIcon className="size-3.5 shrink-0 text-mint" aria-label="Completed" />
) : (
<ExclamationTriangleIcon className="size-3.5 shrink-0 text-amber" aria-label="Completed without success" />
);
case "failed":
return <XCircleIcon className="size-3.5 shrink-0 text-coral" aria-label="Failed" />;
case "closed":
return <EmptyCircleIcon className="size-3.5 shrink-0 text-fg-muted" aria-label="Closed" />;
}
}
function SubagentStatusBadge({ subagent }: { subagent: AgentSessionSubagent }) {
const { status } = subagent;
switch (status.status) {
case "running":
return <span className="text-[10px] uppercase tracking-wider text-teal-500">running</span>;
case "completed":
return (
<span className="font-mono text-[10px] tabular-nums text-fg-muted">
{`${status.turns_used} ${status.turns_used === 1 ? "turn" : "turns"}`}
</span>
);
case "failed":
return <span className="text-[10px] uppercase tracking-wider text-coral">Failed</span>;
case "closed":
return <span className="text-[10px] uppercase tracking-wider text-fg-muted">closed</span>;
}
}
// ---------- Skills ----------
interface SkillsSectionProps {
activated: ActivatedSkill[];
activated: AgentSessionActivatedSkill[];
available: SkillSummary[];
activatedNames: Set<string>;
}
@ -517,7 +744,7 @@ function SkillsSection({ activated, available, activatedNames }: SkillsSectionPr
);
}
function SkillSourceIcon({ source }: { source: ActivatedSkill["source"] }) {
function SkillSourceIcon({ source }: { source: AgentSessionActivatedSkill["source"] }) {
const Icon = source === SkillActivationSource.SLASH ? CommandLineIcon : PuzzlePieceIcon;
return <Icon className="size-3.5 shrink-0 text-fg-muted" />;
}
@ -549,20 +776,49 @@ function AgentToolsSection({ tools }: { tools: ToolSummary[] }) {
// ---------- MCPs ----------
function McpSection({ servers }: { servers: McpServerProjection[] }) {
type McpStatus = "ready" | "failed" | "disconnected";
interface McpServerRow {
name: string;
status: McpStatus;
/** Why it failed, or what closed its connection. */
error: string | null;
toolCount: number;
invoked: boolean;
}
/**
* The agent's MCP servers as rows. A closed connection outranks a failed
* start, which outranks ready; the tool count is the tools it advertised.
*/
function mcpServerRows(servers: Record<string, AgentSessionMcpServer>): McpServerRow[] {
return Object.entries(servers).map(([name, server]) => {
const status: McpStatus =
server.disconnected != null ? "disconnected" : server.error != null ? "failed" : "ready";
return {
name,
status,
error: server.disconnected ?? server.error ?? null,
toolCount: server.tools.length,
invoked: server.invoked,
};
});
}
function McpSection({ servers }: { servers: McpServerRow[] }) {
if (servers.length === 0) return <p className="text-xs text-fg-muted">No MCP servers.</p>;
return (
<ul className="space-y-1">
{servers.map((server) => {
// Dim unused servers so the eye lands on the invoked ones first;
// failed and disconnected servers keep their tone regardless.
const nameClass = server.status.kind === "ready" && !server.invoked
const nameClass = server.status === "ready" && !server.invoked
? "min-w-0 flex-1 truncate text-xs text-fg-muted"
: "min-w-0 flex-1 truncate text-xs text-fg-2";
return (
<li key={server.server_name} className="flex items-center gap-1.5">
<li key={server.name} className="flex items-center gap-1.5" title={server.error ?? undefined}>
<McpStatusIcon status={server.status} />
<span className={nameClass}>{server.server_name}</span>
<span className={nameClass}>{server.name}</span>
<McpStatusBadge server={server} />
</li>
);
@ -571,8 +827,8 @@ function McpSection({ servers }: { servers: McpServerProjection[] }) {
);
}
function McpStatusIcon({ status }: { status: McpServerProjection["status"] }) {
switch (status.kind) {
function McpStatusIcon({ status }: { status: McpStatus }) {
switch (status) {
case "ready":
return <CheckCircleIcon className="size-3.5 shrink-0 text-mint" aria-label="Ready" />;
case "disconnected":
@ -587,14 +843,14 @@ function McpStatusIcon({ status }: { status: McpServerProjection["status"] }) {
}
}
function McpStatusBadge({ server }: { server: McpServerProjection }) {
switch (server.status.kind) {
function McpStatusBadge({ server }: { server: McpServerRow }) {
switch (server.status) {
case "ready":
return (
<span className="font-mono text-[10px] tabular-nums text-fg-muted">
{server.invoked
? "used"
: `${server.tool_count} ${server.tool_count === 1 ? "tool" : "tools"}`}
: `${server.toolCount} ${server.toolCount === 1 ? "tool" : "tools"}`}
</span>
);
case "disconnected":

View file

@ -514,3 +514,27 @@ async function waitFor(condition: () => boolean, timeoutMs = 200) {
}
throw new Error("condition did not become true before timeout");
}
describe("agent session events", () => {
test("refresh the run state the stage sidebar reads its agent facts from", () => {
for (const event of [
"agent.route.failover",
"agent.route.failover.stopped",
"agent.mcp.server.ready",
"agent.mcp.server.failed",
"agent.mcp.server.disconnected",
"agent.skills.discovered",
"agent.skill.activated",
"agent.sub.spawned",
"agent.sub.completed",
"agent.sub.failed",
"agent.compaction.completed",
]) {
expect(queryKeysForRunEvent("run-1", event, "code@1")).toEqual([
queryKeys.runs.state("run-1"),
queryKeys.runs.events("run-1", 1000),
queryKeys.runs.stageEvents("run-1", "code@1"),
]);
}
});
});

View file

@ -152,6 +152,26 @@ const TODO_EVENTS = new Set([
"todo.updated",
"todo.deleted",
]);
// The coding agent's own events whose facts the stage sidebar reads from
// `StageProjection.agent`: route moves, MCP server outcomes, skills,
// subagent lifecycle, and compactions. Each refreshes the run state the
// sidebar renders from, like a todo mutation does. Files the tree wrote
// arrive with `agent.tool.completed`, which already refreshes it.
const AGENT_SESSION_EVENTS = new Set([
"agent.route.failover",
"agent.route.failover.stopped",
"agent.mcp.server.ready",
"agent.mcp.server.failed",
"agent.mcp.server.disconnected",
"agent.skills.discovered",
"agent.skill.activated",
"agent.sub.spawned",
"agent.sub.turn.started",
"agent.sub.completed",
"agent.sub.failed",
"agent.sub.closed",
"agent.compaction.completed",
]);
function liveTimingKeys(runId: string): Key[] {
return [
@ -288,7 +308,7 @@ export function queryKeysForRunEvent(
: [];
}
if (TODO_EVENTS.has(event)) {
if (TODO_EVENTS.has(event) || AGENT_SESSION_EVENTS.has(event)) {
const keys: Key[] = [
queryKeys.runs.state(runId),
queryKeys.runs.events(runId, 1000),

View file

@ -1660,9 +1660,9 @@ Pebble's `RouteFailover`, `McpServerReady`, `McpServerFailed`, and
`McpServerDisconnected` events, stored verbatim with pebble's envelope in
`properties` like every other pebble event. Fabro also mirrors each onto
its own `agent.failover`, `agent.mcp.ready`, `agent.mcp.failed`, and
`agent.mcp.disconnected`, which the store folds into `StageProjection`'s
`mcp_servers`; the pebble events feed `StageProjection.agent`. The mirrors
go once every reader is on `agent`.
`agent.mcp.disconnected`. The stage view reads MCP state from
`StageProjection.agent`, which the pebble events feed; the mirrors change
nothing on the stage any more and go next.
### `agent.route.failover.stopped`

View file

@ -11029,8 +11029,11 @@ components:
example: "2026-05-23T12:34:56Z"
event_seq:
type: ["integer", "null"]
format: uint32
minimum: 1
format: uint64
minimum: 0
description: >-
The coding agent's own event sequence for the snapshot, when it
carried one; not the run event sequence.
example: 42
breakdown:
type: array
@ -11170,23 +11173,6 @@ components:
oneOf:
- $ref: "#/components/schemas/BillingModelRef"
- type: "null"
todos:
oneOf:
- $ref: "#/components/schemas/TodoListProjection"
- type: "null"
description: |
Todo / task list owned by this stage's root agent session. OpenAI
child sessions have separate per-session plans that do not appear
here. Anthropic task lists are root-scoped and shared with child
sessions, so child mutations of that shared list do appear here.
subagents:
type: array
description: Subagents spawned by this stage, in replay/insertion order.
items:
$ref: "#/components/schemas/SubAgentProjection"
skills:
$ref: "#/components/schemas/SkillsProjection"
description: Agent skills discovered and activated during this stage.
permission_level:
oneOf:
- $ref: "#/components/schemas/PermissionLevel"
@ -11199,16 +11185,6 @@ components:
Tool parameter schemas are intentionally omitted from this projection.
items:
$ref: "#/components/schemas/ToolSummary"
mcp_servers:
type: array
description: MCP servers observed by this stage.
items:
$ref: "#/components/schemas/McpServerProjection"
context_window:
oneOf:
- $ref: "#/components/schemas/ContextWindowSnapshot"
- type: "null"
description: Latest content-free context-window snapshot for this agent stage.
inference:
oneOf:
- $ref: "#/components/schemas/StageInferenceProjection"
@ -11340,102 +11316,6 @@ components:
- text
- tool_call
SubAgentProjection:
description: Current projected state for one subagent spawned by an agent stage.
type: object
required:
- agent_id
- depth
- task
- status
properties:
agent_id:
type: string
depth:
type: integer
minimum: 0
task:
type: string
status:
$ref: "#/components/schemas/SubAgentStatus"
SubAgentStatus:
description: Projected lifecycle status for a subagent.
oneOf:
- $ref: "#/components/schemas/SubAgentStatusRunning"
- $ref: "#/components/schemas/SubAgentStatusCompleted"
- $ref: "#/components/schemas/SubAgentStatusFailed"
- $ref: "#/components/schemas/SubAgentStatusClosed"
discriminator:
propertyName: kind
mapping:
running: "#/components/schemas/SubAgentStatusRunning"
completed: "#/components/schemas/SubAgentStatusCompleted"
failed: "#/components/schemas/SubAgentStatusFailed"
closed: "#/components/schemas/SubAgentStatusClosed"
SubAgentStatusRunning:
type: object
required:
- kind
properties:
kind:
type: string
enum: [running]
SubAgentStatusCompleted:
type: object
required:
- kind
- success
- turns_used
properties:
kind:
type: string
enum: [completed]
success:
type: boolean
turns_used:
type: integer
minimum: 0
SubAgentStatusFailed:
type: object
required:
- kind
- error
properties:
kind:
type: string
enum: [failed]
error:
description: Provider/tool error payload captured by the subagent event.
SubAgentStatusClosed:
type: object
required:
- kind
properties:
kind:
type: string
enum: [closed]
SkillsProjection:
description: Agent skills discovered and activated during a stage.
type: object
required:
- available
- activated
properties:
available:
type: array
items:
$ref: "#/components/schemas/SkillSummary"
activated:
type: array
items:
$ref: "#/components/schemas/ActivatedSkill"
SkillSummary:
description: Summary of an available agent skill.
type: object
@ -11448,18 +11328,6 @@ components:
description:
type: string
ActivatedSkill:
description: One observed agent skill activation.
type: object
required:
- name
- source
properties:
name:
type: string
source:
$ref: "#/components/schemas/SkillActivationSource"
SkillActivationSource:
description: Source that activated an agent skill.
type: string
@ -11556,91 +11424,6 @@ components:
type: string
enum: [read, write, shell, subagent, other]
McpServerProjection:
description: Projected state for one MCP server observed by an agent stage.
type: object
required:
- server_name
- tool_count
- status
- invoked
properties:
server_name:
type: string
tool_count:
type: integer
minimum: 0
status:
$ref: "#/components/schemas/McpServerStatus"
invoked:
type: boolean
description: True once the agent has invoked at least one tool from this server during the stage.
McpServerStatus:
description: Projected MCP server readiness status.
oneOf:
- $ref: "#/components/schemas/McpServerStatusReady"
- $ref: "#/components/schemas/McpServerStatusFailed"
- $ref: "#/components/schemas/McpServerStatusDisconnected"
discriminator:
propertyName: kind
mapping:
ready: "#/components/schemas/McpServerStatusReady"
failed: "#/components/schemas/McpServerStatusFailed"
disconnected: "#/components/schemas/McpServerStatusDisconnected"
McpServerStatusReady:
type: object
required:
- kind
- tools
properties:
kind:
type: string
enum: [ready]
tools:
type: array
items:
$ref: "#/components/schemas/AgentMcpToolSummary"
McpServerStatusFailed:
type: object
required:
- kind
- error
properties:
kind:
type: string
enum: [failed]
error:
type: string
McpServerStatusDisconnected:
description: The server was ready and then its connection closed during the stage; its tools fail until the session ends.
type: object
required:
- kind
- error
properties:
kind:
type: string
enum: [disconnected]
error:
type: string
description: What closed the connection, as the client observed it.
AgentMcpToolSummary:
description: Summary of one tool exposed by an MCP server.
type: object
required:
- name
- original_name
properties:
name:
type: string
original_name:
type: string
AgentSessionProjection:
description: >-
The coding agent's fold of one stage's event stream: token counts and

View file

@ -1772,7 +1772,11 @@ async fn get_run_stage_context_window(
.into_response();
}
let Some(snapshot) = stage.context_window.as_ref() else {
let Some(snapshot) = stage
.agent
.as_ref()
.and_then(|agent| agent.context_window.as_ref())
else {
return Json(StageContextWindow::unavailable(
stage_id,
StageContextWindowUnavailableReason::NotObserved,
@ -1789,7 +1793,11 @@ async fn get_run_stage_context_window(
}
fn is_agent_context_window_stage(stage: &StageProjection) -> bool {
if stage.context_window.is_some() {
if stage
.agent
.as_ref()
.is_some_and(|agent| agent.context_window.is_some())
{
return true;
}
if stage.handler == Some(StageHandler::Agent) {

File diff suppressed because it is too large Load diff

View file

@ -404,10 +404,6 @@ fn main() {
&[],
),
("TodoListProjection", "fabro_types::TodoListProjection", &[]),
("SubAgentProjection", "fabro_types::SubAgentProjection", &[]),
("SubAgentStatus", "fabro_types::SubAgentStatus", &[]),
("SkillsProjection", "fabro_types::SkillsProjection", &[]),
("ActivatedSkill", "fabro_types::ActivatedSkill", &[]),
("SkillSummary", "fabro_types::SkillSummary", &[]),
(
"SkillActivationSource",
@ -423,17 +419,6 @@ fn main() {
"fabro_types::AgentToolsAvailableProps",
&[],
),
(
"McpServerProjection",
"fabro_types::McpServerProjection",
&[],
),
("McpServerStatus", "fabro_types::McpServerStatus", &[]),
(
"AgentMcpToolSummary",
"fabro_types::AgentMcpToolSummary",
&[],
),
// Pebble's own fold of a stage's agent events, embedded in
// `StageProjection.agent`. Every nested type is pebble's; the schema
// names carry an `AgentSession` prefix where fabro already has a

View file

@ -38,17 +38,16 @@ pub mod types {
BlockedReason, FailureReason, PendingReason, RunControlAction, RunStatus, SuccessReason,
};
pub use fabro_types::{
ActivatedSkill, AgentControlState, AgentEventProps, AgentMcpToolSummary,
AgentToolsAvailableProps, AskFabro, AuthMethod, AutomationRef, BilledModelUsage,
BilledTokenCounts, BlobHash, CommandTermination, Conclusion, ContextWindowBreakdownItem,
ContextWindowCategory, ContextWindowCountMethod, ContextWindowSnapshot,
ContextWindowStaleness, ContextWindowWarning, CreateVariableRequest, DiffStats,
DiffSummary, DirtyStatus, EventEnvelope, ExecOutputTail, FailureCategory, FailureDetail,
FailureSignature, GitContext, GitRunTarget, GitRunTarget as AutomationGitWorkflowSource,
IdpIdentity, IntegrationConnectionKind, IntegrationConnectionState,
IntegrationConnectionStatus, IntegrationProvider, IntegrationStatus, InterviewOption,
InterviewQuestionRecord, LlmOutputKind, McpServerDraft as CreateMcpServerRequest,
McpServerProjection, McpServerReplace as ReplaceMcpServerRequest, McpServerStatus,
AgentControlState, AgentEventProps, AgentToolsAvailableProps, AskFabro, AuthMethod,
AutomationRef, BilledModelUsage, BilledTokenCounts, BlobHash, CommandTermination,
Conclusion, ContextWindowBreakdownItem, ContextWindowCategory, ContextWindowCountMethod,
ContextWindowSnapshot, ContextWindowStaleness, ContextWindowWarning, CreateVariableRequest,
DiffStats, DiffSummary, DirtyStatus, EventEnvelope, ExecOutputTail, FailureCategory,
FailureDetail, FailureSignature, GitContext, GitRunTarget,
GitRunTarget as AutomationGitWorkflowSource, IdpIdentity, IntegrationConnectionKind,
IntegrationConnectionState, IntegrationConnectionStatus, IntegrationProvider,
IntegrationStatus, InterviewOption, InterviewQuestionRecord, LlmOutputKind,
McpServerDraft as CreateMcpServerRequest, McpServerReplace as ReplaceMcpServerRequest,
McpServerView as McpServer, McpTransportView, Model, ModelControls, ModelCosts,
ModelFeatures, ModelLimits, ModelRef as BillingModelRef, ModelTestMode, PairId,
PairMessageId, PairMessageRecord, PairMessageRequest, PairRecord, PairStartRequest,
@ -65,13 +64,13 @@ pub mod types {
SandboxDetails, SandboxInfo, SandboxListMeta, SandboxListResponse, SandboxProviderKind,
SandboxProviderLookupError, SandboxService, SandboxServiceListResponse, SecretMetadata,
SecretType, ServerSettings, SessionDetail, SessionId, SessionStatus, SessionSummary,
SessionTurn, SkillActivationSource, SkillSummary, SkillsProjection, StageCompletion,
StageContextWindow, StageContextWindowUnavailableReason, StageHandler, StageId,
StageInferenceProjection, StageModelUsage, StageOutcome, StageProjection, StageState,
StageToolBatchProjection, SubAgentProjection, SubAgentStatus, SystemActorKind,
SystemIntegrationStatus, SystemIntegrationsResponse, TodoListProjection, ToolCategory,
ToolSource, ToolSummary, TurnId, UpdateVariableRequest, UserPrincipal, Variable,
VariableListResponse, WorkflowPath, WorkflowSettings, WorkflowVersion, WorkflowVersionId,
SessionTurn, SkillActivationSource, SkillSummary, StageCompletion, StageContextWindow,
StageContextWindowUnavailableReason, StageHandler, StageId, StageInferenceProjection,
StageModelUsage, StageOutcome, StageProjection, StageState, StageToolBatchProjection,
SystemActorKind, SystemIntegrationStatus, SystemIntegrationsResponse, TodoListProjection,
ToolCategory, ToolSource, ToolSummary, TurnId, UpdateVariableRequest, UserPrincipal,
Variable, VariableListResponse, WorkflowPath, WorkflowSettings, WorkflowVersion,
WorkflowVersionId,
};
pub use lithos_llm::catalog::{ModelHandle, ProviderId};
pub use lithos_llm::types::{

View file

@ -1,8 +1,7 @@
use std::any::{TypeId, type_name};
use fabro_api::types::{
ActivatedSkill as ApiActivatedSkill, AgentControlState as ApiAgentControlState,
AgentMcpToolSummary as ApiAgentMcpToolSummary,
AgentControlState as ApiAgentControlState,
AgentToolsAvailableProps as ApiAgentToolsAvailableProps,
BilledModelUsage as ApiBilledModelUsage,
ContextWindowBreakdownItem as ApiContextWindowBreakdownItem,
@ -11,26 +10,23 @@ use fabro_api::types::{
ContextWindowSnapshot as ApiContextWindowSnapshot,
ContextWindowStaleness as ApiContextWindowStaleness,
ContextWindowWarning as ApiContextWindowWarning, LlmOutputKind as ApiLlmOutputKind,
McpServerProjection as ApiMcpServerProjection, McpServerStatus as ApiMcpServerStatus,
ParallelBranchResult as ApiParallelBranchResult, PermissionLevel as ApiPermissionLevel,
SkillActivationSource as ApiSkillActivationSource, SkillSummary as ApiSkillSummary,
SkillsProjection as ApiSkillsProjection, StageContextWindow as ApiStageContextWindow,
StageContextWindow as ApiStageContextWindow,
StageContextWindowUnavailableReason as ApiStageContextWindowUnavailableReason,
StageInferenceProjection as ApiStageInferenceProjection, StageProjection as ApiStageProjection,
StageToolBatchProjection as ApiStageToolBatchProjection,
SubAgentProjection as ApiSubAgentProjection, SubAgentStatus as ApiSubAgentStatus,
TodoListProjection as ApiTodoListProjection, ToolCategory as ApiToolCategory,
ToolSource as ApiToolSource, ToolSummary as ApiToolSummary,
};
use fabro_types::{
ActivatedSkill, AgentControlState, AgentMcpToolSummary, AgentToolsAvailableProps,
BilledModelUsage, ContextWindowBreakdownItem, ContextWindowCategory, ContextWindowCountMethod,
ContextWindowSnapshot, ContextWindowStaleness, ContextWindowWarning, LlmOutputKind,
McpServerProjection, McpServerStatus, ModelRef, ParallelBranchId, ParallelBranchResult,
PermissionLevel, SkillActivationSource, SkillSummary, SkillsProjection, StageContextWindow,
AgentControlState, AgentToolsAvailableProps, BilledModelUsage, ContextWindowBreakdownItem,
ContextWindowCategory, ContextWindowCountMethod, ContextWindowSnapshot, ContextWindowStaleness,
ContextWindowWarning, LlmOutputKind, ModelRef, ParallelBranchId, ParallelBranchResult,
PermissionLevel, SkillActivationSource, SkillSummary, StageContextWindow,
StageContextWindowUnavailableReason, StageId, StageInferenceProjection, StageProjection,
StageToolBatchProjection, SubAgentProjection, SubAgentStatus, TodoListKind, TodoListProjection,
ToolCategory, ToolSource, ToolSummary,
StageToolBatchProjection, TodoListKind, TodoListProjection, ToolCategory, ToolSource,
ToolSummary,
};
use lithos_llm::catalog::{ModelId, ProviderId};
use lithos_llm::types::TokenCounts;
@ -116,19 +112,12 @@ fn stage_projection_reuses_nested_agent_state_types() {
assert_same_type::<ApiAgentControlState, AgentControlState>();
assert_same_type::<ApiParallelBranchResult, ParallelBranchResult>();
assert_same_type::<ApiTodoListProjection, TodoListProjection>();
assert_same_type::<ApiSubAgentProjection, SubAgentProjection>();
assert_same_type::<ApiSubAgentStatus, SubAgentStatus>();
assert_same_type::<ApiSkillsProjection, SkillsProjection>();
assert_same_type::<ApiActivatedSkill, ActivatedSkill>();
assert_same_type::<ApiSkillSummary, SkillSummary>();
assert_same_type::<ApiSkillActivationSource, SkillActivationSource>();
assert_same_type::<ApiToolSummary, ToolSummary>();
assert_same_type::<ApiToolSource, ToolSource>();
assert_same_type::<ApiToolCategory, ToolCategory>();
assert_same_type::<ApiAgentToolsAvailableProps, AgentToolsAvailableProps>();
assert_same_type::<ApiMcpServerProjection, McpServerProjection>();
assert_same_type::<ApiMcpServerStatus, McpServerStatus>();
assert_same_type::<ApiAgentMcpToolSummary, AgentMcpToolSummary>();
assert_same_type::<ApiPermissionLevel, PermissionLevel>();
assert_same_type::<ApiStageContextWindow, StageContextWindow>();
assert_same_type::<ApiContextWindowSnapshot, ContextWindowSnapshot>();
@ -300,45 +289,6 @@ fn stage_projection_round_trips_representative_json() {
"cache_read_tokens": 0,
"cache_write_tokens": 0
},
"todos": {
"kind": "openai_plan",
"list_id": "openai_plan:ses_root",
"items": [
{
"id": "todo-1",
"status": "in_progress",
"order": 0,
"subject": "Write tests",
"active_form": "Writing tests"
}
]
},
"subagents": [
{
"agent_id": "sub-1",
"depth": 1,
"task": "Investigate failing test",
"status": {
"kind": "completed",
"success": true,
"turns_used": 3
}
}
],
"skills": {
"available": [
{
"name": "rust",
"description": "Rust workflow help"
}
],
"activated": [
{
"name": "rust",
"source": "slash"
}
]
},
"permission_level": "read-only",
"agent_tools": [
{
@ -360,41 +310,6 @@ fn stage_projection_round_trips_representative_json() {
"invoked": false
}
],
"mcp_servers": [
{
"server_name": "filesystem",
"tool_count": 1,
"status": {
"kind": "ready",
"tools": [
{
"name": "read_file",
"original_name": "read_file"
}
]
},
"invoked": true
}
],
"context_window": {
"provider": "openai",
"model": "gpt-5.4",
"context_window_tokens": 400000,
"input_tokens": 123456,
"usage_percent": 30.864,
"count_method": "provider_api_scaled_breakdown",
"staleness": "live",
"generated_at": "2026-05-23T12:34:56.000Z",
"event_seq": 42,
"breakdown": [
{
"category": "system_prompt",
"tokens": 30000,
"usage_percent": 7.5
}
],
"warnings": []
},
"inference": {
"session_id": "ses_root",
"started_at": "2026-04-29T12:34:00Z",
@ -461,7 +376,7 @@ fn permission_level_matches_openapi_json_shape() {
}
#[test]
fn nested_agent_state_types_match_openapi_json_shape() {
fn todo_list_and_skill_types_match_openapi_json_shape() {
for (kind, list_id, wire_kind) in [
(
TodoListKind::OpenAiPlan,
@ -484,32 +399,6 @@ fn nested_agent_state_types_match_openapi_json_shape() {
assert_eq!(api_todo_list, todo_list);
}
let subagent = SubAgentProjection {
agent_id: "sub-1".to_string(),
depth: 1,
task: "Investigate failing test".to_string(),
status: SubAgentStatus::Completed {
success: true,
turns_used: 3,
},
};
let subagent_json = serde_json::to_value(&subagent).unwrap();
assert_eq!(
subagent_json,
json!({
"agent_id": "sub-1",
"depth": 1,
"task": "Investigate failing test",
"status": {
"kind": "completed",
"success": true,
"turns_used": 3
}
})
);
let api_subagent: ApiSubAgentProjection = serde_json::from_value(subagent_json).unwrap();
assert_eq!(api_subagent, subagent);
let skill = SkillSummary {
name: "rust".to_string(),
description: "Rust workflow help".to_string(),
@ -529,103 +418,6 @@ fn nested_agent_state_types_match_openapi_json_shape() {
assert_eq!(source_json, json!("slash"));
let api_source: ApiSkillActivationSource = serde_json::from_value(source_json).unwrap();
assert_eq!(api_source, SkillActivationSource::Slash);
let activated = ActivatedSkill {
name: "rust".to_string(),
source: SkillActivationSource::Slash,
};
let skills = SkillsProjection {
available: vec![skill],
activated: vec![activated],
};
let skills_json = serde_json::to_value(&skills).unwrap();
assert_eq!(
skills_json,
json!({
"available": [
{
"name": "rust",
"description": "Rust workflow help"
}
],
"activated": [
{
"name": "rust",
"source": "slash"
}
]
})
);
let api_skills: ApiSkillsProjection = serde_json::from_value(skills_json).unwrap();
assert_eq!(api_skills, skills);
let tool = AgentMcpToolSummary {
name: "read_file".to_string(),
original_name: "read_file".to_string(),
};
let tool_json = serde_json::to_value(&tool).unwrap();
assert_eq!(
tool_json,
json!({
"name": "read_file",
"original_name": "read_file"
})
);
let api_tool: ApiAgentMcpToolSummary = serde_json::from_value(tool_json).unwrap();
assert_eq!(api_tool, tool);
let mcp_server = McpServerProjection {
server_name: "filesystem".to_string(),
tool_count: 1,
status: McpServerStatus::Ready { tools: vec![tool] },
invoked: true,
};
let mcp_json = serde_json::to_value(&mcp_server).unwrap();
assert_eq!(
mcp_json,
json!({
"server_name": "filesystem",
"tool_count": 1,
"status": {
"kind": "ready",
"tools": [
{
"name": "read_file",
"original_name": "read_file"
}
]
},
"invoked": true,
})
);
let api_mcp: ApiMcpServerProjection = serde_json::from_value(mcp_json).unwrap();
assert_eq!(api_mcp, mcp_server);
assert_eq!(mcp_server.tool_count, 1);
let disconnected = McpServerProjection {
server_name: "filesystem".to_string(),
tool_count: 1,
status: McpServerStatus::Disconnected {
error: "transport closed".to_string(),
},
invoked: true,
};
let disconnected_json = serde_json::to_value(&disconnected).unwrap();
assert_eq!(
disconnected_json,
json!({
"server_name": "filesystem",
"tool_count": 1,
"status": {
"kind": "disconnected",
"error": "transport closed"
},
"invoked": true,
})
);
let api_disconnected: ApiMcpServerProjection =
serde_json::from_value(disconnected_json).unwrap();
assert_eq!(api_disconnected, disconnected);
}
#[test]

View file

@ -143,10 +143,9 @@ pub use run_intent::{
TargetValidationError, ValidatedGitRunTarget, ValidatedRunTarget,
};
pub use run_projection::{
ActivatedSkill, AgentControlState, CheckpointRecord, McpServerProjection, McpServerStatus,
PendingInterviewRecord, RunProjection, SkillsProjection, StageContextWindow,
AgentControlState, CheckpointRecord, PendingInterviewRecord, RunProjection, StageContextWindow,
StageContextWindowUnavailableReason, StageInferenceProjection, StageModelUsage,
StageProjection, StageToolBatchProjection, SubAgentProjection, SubAgentStatus, first_event_seq,
StageProjection, StageToolBatchProjection, first_event_seq,
};
pub use run_sandbox::{
RunSandbox, RunSandboxFailure, RunSandboxInstance, RunSandboxKind, RunSandboxPlan,

View file

@ -6,19 +6,18 @@ use chrono::{DateTime, Utc};
use lithos_llm::types::{ReasoningEffort, Speed};
use pebble_coding_agent::events::{
ContextWindowBreakdownItem, ContextWindowCountMethod, ContextWindowSnapshot,
ContextWindowStaleness, ContextWindowWarning, LlmOutputKind, PermissionLevel,
SkillActivationSource, SkillSummary, TodoListProjection, ToolSummary,
ContextWindowStaleness, ContextWindowWarning, LlmOutputKind, PermissionLevel, ToolSummary,
};
use pebble_coding_agent::projection::SessionProjection;
use strum::{Display, EnumString, IntoStaticStr};
use crate::run_event::{AgentSessionActivatedProps, StagePromptProps};
use crate::{
AgentBackend, AgentMcpToolSummary, BilledModelUsage, BilledTokenCounts, Checkpoint, Conclusion,
GitIdentity, InterviewQuestionRecord, InvalidTransition, ModelRef, ParallelBranchId,
PullRequestCreation, PullRequestLink, RunApproval, RunControlAction, RunDiff, RunId,
RunSandbox, RunSpec, RunStatus, RunTiming, StageCompletion, StageHandler, StageId, StageState,
StageTiming, StartRecord, timing,
AgentBackend, BilledModelUsage, BilledTokenCounts, Checkpoint, Conclusion, GitIdentity,
InterviewQuestionRecord, InvalidTransition, ModelRef, ParallelBranchId, PullRequestCreation,
PullRequestLink, RunApproval, RunControlAction, RunDiff, RunId, RunSandbox, RunSpec, RunStatus,
RunTiming, StageCompletion, StageHandler, StageId, StageState, StageTiming, StartRecord,
timing,
};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
@ -304,25 +303,10 @@ pub struct StageProjection {
/// `usage` to `model`.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub billing_by_model: Vec<BilledModelUsage>,
/// Todo/task list owned by the stage's root agent session.
///
/// OpenAI child sessions own separate per-session plans and do not appear
/// here. Anthropic task lists are root-scoped and shared with child
/// sessions, so child mutations of that shared list do appear here.
#[serde(default, rename = "todos", skip_serializing_if = "Option::is_none")]
pub root_agent_todos: Option<TodoListProjection>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub subagents: Vec<SubAgentProjection>,
#[serde(default, skip_serializing_if = "SkillsProjection::is_empty")]
pub skills: SkillsProjection,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub permission_level: Option<PermissionLevel>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub agent_tools: Vec<ToolSummary>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub mcp_servers: Vec<McpServerProjection>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context_window: Option<ContextWindowSnapshot>,
/// Open inference bracket for this stage, if the event log contains one.
///
/// `Some` means exactly *"an `agent.llm.started` was recorded and no
@ -341,10 +325,12 @@ pub struct StageProjection {
#[serde(default)]
pub agent_control: AgentControlState,
/// Pebble's fold of this stage's agent events: the one agent projection,
/// fed every `agent.*` and `todo.*` event stored on the stage. Present
/// for pebble-backed agent stages once their first agent event is
/// stored; `None` for prompt, command, ACP, human, parallel, and
/// conditional stages.
/// fed every `agent.*` and `todo.*` event stored on the stage, and what
/// the stage view reads for todos, subagents, skills, MCP servers, files,
/// failovers, compactions, and the context window. Present for
/// pebble-backed agent stages once their first agent event is stored;
/// `None` for prompt, command, ACP, human, parallel, and conditional
/// stages.
///
/// Its lifetime fields are the stage's totals across every prompt the
/// stage ran, because each stage gets its own fold over its own events.
@ -427,67 +413,6 @@ pub enum AgentControlState {
WaitingForSteer,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct SubAgentProjection {
pub agent_id: String,
pub depth: usize,
pub task: String,
pub status: SubAgentStatus,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum SubAgentStatus {
Running,
Completed { success: bool, turns_used: usize },
Failed { error: serde_json::Value },
Closed,
}
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct SkillsProjection {
pub available: Vec<SkillSummary>,
pub activated: Vec<ActivatedSkill>,
}
impl SkillsProjection {
#[must_use]
pub fn is_empty(&self) -> bool {
self.available.is_empty() && self.activated.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ActivatedSkill {
pub name: String,
pub source: SkillActivationSource,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct McpServerProjection {
pub server_name: String,
pub tool_count: usize,
pub status: McpServerStatus,
/// True once any tool from this server has been invoked during the stage.
pub invoked: bool,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum McpServerStatus {
Ready {
tools: Vec<AgentMcpToolSummary>,
},
Failed {
error: String,
},
/// The server was ready and then its connection closed during the
/// stage; its tools fail until the session ends.
Disconnected {
error: String,
},
}
/// Convert a 1-based event sequence number into the `NonZeroU32` form used for
/// `StageProjection::first_event_seq`. Run event seqs always start at 1.
#[must_use]
@ -510,13 +435,8 @@ impl StageProjection {
tool_batch: None,
usage: BilledTokenCounts::default(),
model: None,
root_agent_todos: None,
subagents: Vec::new(),
skills: SkillsProjection::default(),
permission_level: None,
agent_tools: Vec::new(),
mcp_servers: Vec::new(),
context_window: None,
inference: None,
acp_started_at: None,
agent_control: AgentControlState::default(),

View file

@ -27,12 +27,10 @@ base.ts
common.ts
configuration.ts
index.ts
models/activated-skill.ts
models/agent-control-state.ts
models/agent-error-data.ts
models/agent-error-kind.ts
models/agent-event-props.ts
models/agent-mcp-tool-summary.ts
models/agent-session-activated-props.ts
models/agent-session-activated-skill.ts
models/agent-session-activity.ts
@ -244,12 +242,7 @@ models/manifest-workflow.ts
models/mcp-http-protocol.ts
models/mcp-server-list-meta.ts
models/mcp-server-list-response.ts
models/mcp-server-projection.ts
models/mcp-server-settings.ts
models/mcp-server-status-disconnected.ts
models/mcp-server-status-failed.ts
models/mcp-server-status-ready.ts
models/mcp-server-status.ts
models/mcp-server.ts
models/mcp-tool-summary.ts
models/mcp-transport-http.ts
@ -508,7 +501,6 @@ models/session-summary.ts
models/session-turn.ts
models/skill-activation-source.ts
models/skill-summary.ts
models/skills-projection.ts
models/slack-integration-settings.ts
models/ssh-access-request.ts
models/ssh-access-response.ts
@ -527,12 +519,6 @@ models/stage-tool-batch-projection.ts
models/start-record.ts
models/start-run-request.ts
models/steer-run-request.ts
models/sub-agent-projection.ts
models/sub-agent-status-closed.ts
models/sub-agent-status-completed.ts
models/sub-agent-status-failed.ts
models/sub-agent-status-running.ts
models/sub-agent-status.ts
models/submit-answer-multi-selected-request.ts
models/submit-answer-no-request.ts
models/submit-answer-request.ts

View file

@ -1,26 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
// May contain unused imports in some cases
// @ts-ignore
import type { SkillActivationSource } from './skill-activation-source';
/**
* One observed agent skill activation.
*/
export interface ActivatedSkill {
'name': string;
'source': SkillActivationSource;
}

View file

@ -1,23 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
* Summary of one tool exposed by an MCP server.
*/
export interface AgentMcpToolSummary {
'name': string;
'original_name': string;
}

View file

@ -1,9 +1,7 @@
export * from './activated-skill';
export * from './agent-control-state';
export * from './agent-error-data';
export * from './agent-error-kind';
export * from './agent-event-props';
export * from './agent-mcp-tool-summary';
export * from './agent-session-activated-props';
export * from './agent-session-activated-skill';
export * from './agent-session-activity';
@ -215,12 +213,7 @@ export * from './mcp-http-protocol';
export * from './mcp-server';
export * from './mcp-server-list-meta';
export * from './mcp-server-list-response';
export * from './mcp-server-projection';
export * from './mcp-server-settings';
export * from './mcp-server-status';
export * from './mcp-server-status-disconnected';
export * from './mcp-server-status-failed';
export * from './mcp-server-status-ready';
export * from './mcp-tool-summary';
export * from './mcp-transport';
export * from './mcp-transport-http';
@ -478,7 +471,6 @@ export * from './session-summary';
export * from './session-turn';
export * from './skill-activation-source';
export * from './skill-summary';
export * from './skills-projection';
export * from './slack-integration-settings';
export * from './ssh-access-request';
export * from './ssh-access-response';
@ -497,12 +489,6 @@ export * from './stage-tool-batch-projection';
export * from './start-record';
export * from './start-run-request';
export * from './steer-run-request';
export * from './sub-agent-projection';
export * from './sub-agent-status';
export * from './sub-agent-status-closed';
export * from './sub-agent-status-completed';
export * from './sub-agent-status-failed';
export * from './sub-agent-status-running';
export * from './submit-answer-multi-selected-request';
export * from './submit-answer-no-request';
export * from './submit-answer-request';

View file

@ -1,31 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
// May contain unused imports in some cases
// @ts-ignore
import type { McpServerStatus } from './mcp-server-status';
/**
* Projected state for one MCP server observed by an agent stage.
*/
export interface McpServerProjection {
'server_name': string;
'tool_count': number;
'status': McpServerStatus;
/**
* True once the agent has invoked at least one tool from this server during the stage.
*/
'invoked': boolean;
}

View file

@ -1,32 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
* The server was ready and then its connection closed during the stage; its tools fail until the session ends.
*/
export interface McpServerStatusDisconnected {
'kind': McpServerStatusDisconnectedKindEnum;
/**
* What closed the connection, as the client observed it.
*/
'error': string;
}
export const McpServerStatusDisconnectedKindEnum = {
DISCONNECTED: 'disconnected'
} as const;
export type McpServerStatusDisconnectedKindEnum = typeof McpServerStatusDisconnectedKindEnum[keyof typeof McpServerStatusDisconnectedKindEnum];

View file

@ -1,26 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface McpServerStatusFailed {
'kind': McpServerStatusFailedKindEnum;
'error': string;
}
export const McpServerStatusFailedKindEnum = {
FAILED: 'failed'
} as const;
export type McpServerStatusFailedKindEnum = typeof McpServerStatusFailedKindEnum[keyof typeof McpServerStatusFailedKindEnum];

View file

@ -1,29 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
// May contain unused imports in some cases
// @ts-ignore
import type { AgentMcpToolSummary } from './agent-mcp-tool-summary';
export interface McpServerStatusReady {
'kind': McpServerStatusReadyKindEnum;
'tools': Array<AgentMcpToolSummary>;
}
export const McpServerStatusReadyKindEnum = {
READY: 'ready'
} as const;
export type McpServerStatusReadyKindEnum = typeof McpServerStatusReadyKindEnum[keyof typeof McpServerStatusReadyKindEnum];

View file

@ -1,33 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
// May contain unused imports in some cases
// @ts-ignore
import type { AgentMcpToolSummary } from './agent-mcp-tool-summary';
// May contain unused imports in some cases
// @ts-ignore
import type { McpServerStatusDisconnected } from './mcp-server-status-disconnected';
// May contain unused imports in some cases
// @ts-ignore
import type { McpServerStatusFailed } from './mcp-server-status-failed';
// May contain unused imports in some cases
// @ts-ignore
import type { McpServerStatusReady } from './mcp-server-status-ready';
/**
* @type McpServerStatus
* Projected MCP server readiness status.
*/
export type McpServerStatus = { kind: 'disconnected' } & McpServerStatusDisconnected | { kind: 'failed' } & McpServerStatusFailed | { kind: 'ready' } & McpServerStatusReady;

View file

@ -1,29 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
// May contain unused imports in some cases
// @ts-ignore
import type { ActivatedSkill } from './activated-skill';
// May contain unused imports in some cases
// @ts-ignore
import type { SkillSummary } from './skill-summary';
/**
* Agent skills discovered and activated during a stage.
*/
export interface SkillsProjection {
'available': Array<SkillSummary>;
'activated': Array<ActivatedSkill>;
}

View file

@ -50,6 +50,9 @@ export interface StageContextWindow {
'count_method': ContextWindowCountMethod | null;
'staleness': ContextWindowStaleness;
'generated_at': string | null;
/**
* The coding agent\'s own event sequence for the snapshot, when it carried one; not the run event sequence.
*/
'event_seq': number | null;
'breakdown': Array<ContextWindowBreakdownItem>;
'warnings': Array<ContextWindowWarning>;

View file

@ -33,21 +33,12 @@ import type { BillingModelRef } from './billing-model-ref';
import type { CommandTermination } from './command-termination';
// May contain unused imports in some cases
// @ts-ignore
import type { ContextWindowSnapshot } from './context-window-snapshot';
// May contain unused imports in some cases
// @ts-ignore
import type { McpServerProjection } from './mcp-server-projection';
// May contain unused imports in some cases
// @ts-ignore
import type { ParallelBranchResult } from './parallel-branch-result';
// May contain unused imports in some cases
// @ts-ignore
import type { PermissionLevel } from './permission-level';
// May contain unused imports in some cases
// @ts-ignore
import type { SkillsProjection } from './skills-projection';
// May contain unused imports in some cases
// @ts-ignore
import type { StageCompletion } from './stage-completion';
// May contain unused imports in some cases
// @ts-ignore
@ -66,12 +57,6 @@ import type { StageTiming } from './stage-timing';
import type { StageToolBatchProjection } from './stage-tool-batch-projection';
// May contain unused imports in some cases
// @ts-ignore
import type { SubAgentProjection } from './sub-agent-projection';
// May contain unused imports in some cases
// @ts-ignore
import type { TodoListProjection } from './todo-list-projection';
// May contain unused imports in some cases
// @ts-ignore
import type { ToolSummary } from './tool-summary';
/**
@ -120,25 +105,11 @@ export interface StageProjection {
'tool_batch'?: StageToolBatchProjection | null;
'usage': BilledTokenCounts;
'model'?: BillingModelRef | null;
'todos'?: TodoListProjection | null;
/**
* Subagents spawned by this stage, in replay/insertion order.
*/
'subagents'?: Array<SubAgentProjection>;
/**
* Agent skills discovered and activated during this stage.
*/
'skills'?: SkillsProjection;
'permission_level'?: PermissionLevel | null;
/**
* Effective model-callable tools exposed to this agent stage session. Tool parameter schemas are intentionally omitted from this projection.
*/
'agent_tools'?: Array<ToolSummary>;
/**
* MCP servers observed by this stage.
*/
'mcp_servers'?: Array<McpServerProjection>;
'context_window'?: ContextWindowSnapshot | null;
'inference'?: StageInferenceProjection | null;
/**
* Start of an external ACP agent process, if one is running. ACP agents do not expose Fabro\'s internal LLM brackets, so the process lifetime supplies their live inference estimate.

View file

@ -1,28 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
// May contain unused imports in some cases
// @ts-ignore
import type { SubAgentStatus } from './sub-agent-status';
/**
* Current projected state for one subagent spawned by an agent stage.
*/
export interface SubAgentProjection {
'agent_id': string;
'depth': number;
'task': string;
'status': SubAgentStatus;
}

View file

@ -1,25 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface SubAgentStatusClosed {
'kind': SubAgentStatusClosedKindEnum;
}
export const SubAgentStatusClosedKindEnum = {
CLOSED: 'closed'
} as const;
export type SubAgentStatusClosedKindEnum = typeof SubAgentStatusClosedKindEnum[keyof typeof SubAgentStatusClosedKindEnum];

View file

@ -1,27 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface SubAgentStatusCompleted {
'kind': SubAgentStatusCompletedKindEnum;
'success': boolean;
'turns_used': number;
}
export const SubAgentStatusCompletedKindEnum = {
COMPLETED: 'completed'
} as const;
export type SubAgentStatusCompletedKindEnum = typeof SubAgentStatusCompletedKindEnum[keyof typeof SubAgentStatusCompletedKindEnum];

View file

@ -1,26 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface SubAgentStatusFailed {
'kind': SubAgentStatusFailedKindEnum;
'error': any;
}
export const SubAgentStatusFailedKindEnum = {
FAILED: 'failed'
} as const;
export type SubAgentStatusFailedKindEnum = typeof SubAgentStatusFailedKindEnum[keyof typeof SubAgentStatusFailedKindEnum];

View file

@ -1,25 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface SubAgentStatusRunning {
'kind': SubAgentStatusRunningKindEnum;
}
export const SubAgentStatusRunningKindEnum = {
RUNNING: 'running'
} as const;
export type SubAgentStatusRunningKindEnum = typeof SubAgentStatusRunningKindEnum[keyof typeof SubAgentStatusRunningKindEnum];

View file

@ -1,33 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
// May contain unused imports in some cases
// @ts-ignore
import type { SubAgentStatusClosed } from './sub-agent-status-closed';
// May contain unused imports in some cases
// @ts-ignore
import type { SubAgentStatusCompleted } from './sub-agent-status-completed';
// May contain unused imports in some cases
// @ts-ignore
import type { SubAgentStatusFailed } from './sub-agent-status-failed';
// May contain unused imports in some cases
// @ts-ignore
import type { SubAgentStatusRunning } from './sub-agent-status-running';
/**
* @type SubAgentStatus
* Projected lifecycle status for a subagent.
*/
export type SubAgentStatus = { kind: 'closed' } & SubAgentStatusClosed | { kind: 'completed' } & SubAgentStatusCompleted | { kind: 'failed' } & SubAgentStatusFailed | { kind: 'running' } & SubAgentStatusRunning;