fabro/run.json
Fabro a084b7551b checkpoint
⚒️ Generated with [Fabro](https://fabro.sh)
2026-05-24 14:09:44 -04:00

2091 lines
No EOL
325 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"title": "---",
"spec": {
"run_id": "01KSDFKD9JJS7D2ZJ6JQYXV58Z",
"settings": {
"project": {
"name": null,
"description": null,
"metadata": {}
},
"workflow": {
"name": null,
"description": null,
"graph": "workflow.fabro",
"metadata": {}
},
"run": {
"goal": {
"type": "inline",
"value": "---\ntitle: feat: StageProjection agent tools API\ntype: feat\nstatus: active\ndate: 2026-05-24\n---\n\n# feat: StageProjection Agent Tools API\n\n## Overview\n\nExpose the complete effective tool list for each agent-backed stage through `StageProjection`, so UI and API consumers can show actual tools such as `apply_patch`, `grep`, `glob`, `read_file`, MCP tools, skill tools, and subagent tools without inferring them from `permission_level`.\n\nThe API should expose tool summaries with `name`, `description`, `source`, `category`, and `invoked`. It must not expose full JSON parameter schemas in the run projection.\n\n## Problem Frame\n\nThe stage sidebar currently has permission metadata such as \"Full access\", but `permission_level` is only an access mode. It does not tell consumers which tools were actually exposed to the model after provider profile setup, optional tool registration, MCP integration, and tool access policy filtering.\n\nThe authoritative list already exists at request-build time in the agent session registry. The API needs to capture that effective list once per stage session and project it onto the stage.\n\n## Requirements Trace\n\n- R1. Add a StageProjection API field containing the complete effective tools for a stage session.\n- R2. Include `name`, `description`, `source`, `category`, and `invoked` for each tool.\n- R3. Do not infer tools from `permission_level` in backend or frontend code.\n- R4. Do not expose full tool parameter schemas through StageProjection.\n- R5. Preserve existing `permission_level` and `mcp_servers` fields for compatibility.\n- R6. Mark individual tools as invoked when matching `agent.tool.started` events are projected.\n- R7. Keep legacy runs backward compatible by defaulting missing tool lists to empty.\n\n## Scope Boundaries\n\n- Do not remove or rename `StageProjection.permission_level`.\n- Do not remove the existing MCP server projection or `AgentMcpToolSummary`.\n- Do not change completion API tool definitions.\n- Do not add ACP-native tool discovery in this pass unless an ACP path already has an equivalent effective tool list available.\n- Do not render parameter schemas in the web UI.\n\n## Context & Research\n\n### Relevant Code and Patterns\n\n- OpenAPI is the source of truth for HTTP contracts in `docs/public/api-reference/fabro-api.yaml`.\n- Shared API/projection DTOs should live in `fabro-types`, with `fabro-api/build.rs` replacements to avoid duplicate generated Rust types.\n- `StageProjection` lives in `lib/crates/fabro-types/src/run_projection.rs`.\n- Durable run event props live in `lib/crates/fabro-types/src/run_event/agent.rs` and `lib/crates/fabro-types/src/run_event/mod.rs`.\n- Workflow event conversion and event names live in `lib/crates/fabro-workflow/src/event/convert.rs`, `events.rs`, and `names.rs`.\n- Projection replay lives in `lib/crates/fabro-store/src/run_state.rs`.\n- The effective request tool list is built in `lib/crates/fabro-agent/src/session.rs` from `ToolRegistry::definitions_with_source_for_policy`.\n- Tool source metadata already exists as `ToolSource` and `ToolDefinitionWithSource` in `lib/crates/fabro-agent/src/tool_registry.rs`.\n- Tool category mapping already exists in `lib/crates/fabro-agent/src/tool_permissions.rs`.\n- The sidebar display lives in `apps/fabro-web/app/components/stage-insights-sidebar.tsx`.\n\n### Strategy Docs\n\n- Read `docs/internal/events-strategy.md` before adding the new durable event.\n- Read `docs/internal/testing-strategy.md` before adding or reorganizing tests.\n- Follow the OpenAPI type ownership guidance in `AGENTS.md`: reuse `fabro-types` through `fabro-api/build.rs` replacements when the API schema has the same product meaning and serde shape.\n\n## Key Technical Decisions\n\n- Add a new StageProjection field named `agent_tools`, not `tools`, to avoid ambiguity with MCP nested tools and completion tool definitions.\n- Add a dedicated durable event named `agent.tools.available` instead of overloading `agent.session.activated`.\n- Capture the effective tool list after session setup and filtering, using the same path as model request construction.\n- Store descriptions in the summary because they are useful API/UI metadata; omit parameter schemas to keep projection payloads small and avoid leaking full implementation detail.\n- Keep `AgentMcpToolSummary` MCP-only. Add a new general-purpose `AgentToolSummary` instead of stretching the MCP type beyond its meaning.\n- Treat `invoked` as projected state. The availability event should emit tools with `invoked: false`; replay of `agent.tool.started` flips matching tools to true.\n\n## API Contract\n\nAdd these schemas to OpenAPI and map them to `fabro_types` replacements:\n\n- `AgentToolSummary`\n - required: `name`, `description`, `source`, `category`, `invoked`\n - `name`: exposed tool name, e.g. `apply_patch` or `mcp__filesystem__read_file`\n - `description`: model-facing tool description\n - `source`: `AgentToolSource`\n - `category`: `AgentToolCategory`\n - `invoked`: boolean\n- `AgentToolSource`\n - tagged by `kind`\n - `native`\n - `mcp` with `server_name` and `original_name`\n - `skill`\n- `AgentToolCategory`\n - enum: `read`, `write`, `shell`, `subagent`, `other`\n- `AgentToolsAvailableProps`\n - required: `tools`, `visit`\n - `tools`: array of `AgentToolSummary`\n - `visit`: stage visit number\n\nAdd to `StageProjection`:\n\n- `agent_tools`: array of `AgentToolSummary`\n- Default to an empty array when omitted.\n- Skip serializing when empty, matching existing projection optional-list style.\n\nAdd event body:\n\n- Serialized event name: `agent.tools.available`\n- Event body type: `AgentToolsAvailableProps`\n\n## Implementation Units\n\n- [ ] **Unit 1: Add shared tool summary types**\n\n**Goal:** Define the canonical API/projection DTOs in `fabro-types`.\n\n**Files:**\n- Modify: `lib/crates/fabro-types/src/run_event/agent.rs`\n- Modify: `lib/crates/fabro-types/src/run_event/mod.rs`\n- Modify: `lib/crates/fabro-types/src/run_projection.rs`\n- Modify: `lib/crates/fabro-types/src/lib.rs`\n\n**Work:**\n- Add `AgentToolSummary`, `AgentToolSource`, `AgentToolCategory`, and `AgentToolsAvailableProps`.\n- Add `EventBody::AgentToolsAvailable` serialized as `agent.tools.available`.\n- Add `agent_tools: Vec<AgentToolSummary>` to `StageProjection`.\n- Ensure all new fields default cleanly for older persisted events/projections.\n- Export the new public types from `fabro-types`.\n\n- [ ] **Unit 2: Capture effective session tools**\n\n**Goal:** Provide an authoritative one-time source for the list that the model can actually call.\n\n**Files:**\n- Modify: `lib/crates/fabro-agent/src/session.rs`\n- Modify: `lib/crates/fabro-workflow/src/handler/llm/api.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/events.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/names.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/convert.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/stored_fields.rs` only if the new event needs non-standard stored fields.\n\n**Work:**\n- Add `Session::available_tools()` that returns the same effective `ToolDefinitionWithSource` list as `build_request()`.\n- Map `ToolDefinitionWithSource` to `AgentToolSummary` at the workflow boundary.\n- Populate `description` from `ToolDefinition.description`.\n- Populate `source` from `ToolSource`.\n- For MCP tools, include the server name from `ToolSource::Mcp` and derive `original_name` from the qualified exposed name using the existing MCP naming convention.\n- Populate `category` from the existing tool category mapping for known exposed names; use `other` when no category mapping applies.\n- Emit `agent.tools.available` once for the stage session after session setup and filtering are complete.\n\n- [ ] **Unit 3: Project available and invoked tools**\n\n**Goal:** Make `StageProjection.agent_tools` replay-authoritative.\n\n**Files:**\n- Modify: `lib/crates/fabro-store/src/run_state.rs`\n\n**Work:**\n- On `EventBody::AgentToolsAvailable`, replace the current stage visit's `agent_tools` with the event tools.\n- On `EventBody::AgentToolStarted`, find a matching `agent_tools` entry by exposed `tool_name` and set `invoked = true`.\n- Keep the existing MCP server `invoked` update unchanged.\n- If a legacy run has no availability event, do not synthesize a full list from permissions.\n\n- [ ] **Unit 4: Update OpenAPI and generated clients**\n\n**Goal:** Expose the new projection field and event contract through public API clients without duplicate Rust API types.\n\n**Files:**\n- Modify: `docs/public/api-reference/fabro-api.yaml`\n- Modify: `lib/crates/fabro-api/build.rs`\n- Modify: `lib/crates/fabro-api/src/lib.rs`\n- Modify generated files under `lib/crates/fabro-api/src/generated.rs` via `cargo build -p fabro-api`.\n- Modify generated files under `lib/packages/fabro-api-client/src` via TypeScript client generation.\n\n**Work:**\n- Add the OpenAPI schemas listed in the API Contract section.\n- Add `StageProjection.agent_tools`.\n- Add `fabro-api/build.rs` replacements for the new `fabro-types` types.\n- Regenerate Rust API code.\n- Regenerate TypeScript API client code.\n\n- [ ] **Unit 5: Render tools in the web sidebar**\n\n**Goal:** Replace the permission-derived sidebar display with the actual projected tool list.\n\n**Files:**\n- Modify: `apps/fabro-web/app/components/stage-insights-sidebar.tsx`\n- Modify: `apps/fabro-web/app/components/stage-insights-sidebar.test.tsx`\n\n**Work:**\n- Render `stage.agent_tools` when present.\n- Show each tool's name, description, source/category, and invoked state.\n- Keep permission level as secondary metadata or fallback for legacy stages with no `agent_tools`.\n- Do not infer tool availability from permission level.\n\n## Test Plan\n\n- `fabro-types`\n - JSON round-trip for `agent.tools.available`.\n - Serialization checks for `AgentToolSource` and `AgentToolCategory`.\n - Backward compatibility check that missing `agent_tools` deserializes to an empty list.\n\n- `fabro-workflow`\n - Event name and conversion tests for `agent.tools.available`.\n - Capture test proving native tools such as `apply_patch`, `grep`, and `glob` are emitted from the effective registry path.\n - MCP source mapping test for a qualified MCP tool name.\n\n- `fabro-store`\n - Projection test that `agent.tools.available` populates `StageProjection.agent_tools`.\n - Projection test that `agent.tool.started` marks only the matching tool as invoked.\n - Regression test that MCP server `invoked` status still updates as before.\n\n- `fabro-api`\n - Type identity/parity tests confirming API types reuse `fabro_types::AgentToolSummary`, `AgentToolSource`, `AgentToolCategory`, and `AgentToolsAvailableProps`.\n - StageProjection round-trip test including `agent_tools`.\n\n- `fabro-web`\n - Sidebar test rendering tool names and descriptions from `stage.agent_tools`.\n - Sidebar test showing invoked state.\n - Legacy fallback test for stages without `agent_tools`.\n\n## Run Checks\n\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo nextest run -p fabro-types -p fabro-workflow -p fabro-store -p fabro-api`\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cd apps/fabro-web && bun test && bun run typecheck`\n\n## Assumptions\n\n- The first implementation targets normal API-backed agent sessions, not ACP-native sessions.\n- `description` is safe to expose because it is already model-facing tool metadata, but parameter schemas remain out of scope for StageProjection.\n- `agent_tools` is a complete list only for runs that emit `agent.tools.available`; legacy runs return an empty list and may still show existing permission/MCP metadata.\n- If tool registration becomes mutable later, the event contract can be re-emitted and projection replacement semantics will still work.\n"
},
"working_dir": null,
"metadata": {},
"inputs": {},
"model": {
"provider": "anthropic",
"name": "claude-sonnet-4-6",
"fallbacks": [],
"controls": {
"reasoning_effort": null,
"speed": null
}
},
"git": {
"author": null
},
"prepare": {
"commands": [],
"timeout_ms": 300000
},
"execution": {
"mode": "normal",
"approval": "prompt"
},
"checkpoint": {
"exclude_globs": [],
"skip_git_hooks": false
},
"clone": {
"enabled": true
},
"run_branch": {
"enabled": true,
"push": true
},
"meta_branch": {
"enabled": true,
"push": true
},
"environment": {
"id": "fabro-dev",
"provider": "daytona",
"image": {
"ref": "fabro-v12",
"dockerfile": {
"type": "inline",
"value": "FROM ubuntu:24.04\n\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n curl git ripgrep ca-certificates build-essential pkg-config libssl-dev unzip python3 \\\n xvfb xfce4 xfce4-terminal x11vnc novnc dbus-x11 \\\n libx11-6 libxrandr2 libxext6 libxrender1 libxfixes3 libxss1 libxtst6 libxi6 \\\n && rm -rf /var/lib/apt/lists/*\n\n# Install real Chromium (not the snap stub) via xtradeb PPA\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n software-properties-common curl gnupg \\\n && add-apt-repository -y ppa:xtradeb/apps \\\n && apt-get update \\\n && apt-get install -y --no-install-recommends chromium \\\n && rm -rf /var/lib/apt/lists/*\n\n# Wrapper: Chromium needs --no-sandbox when running as root in a container,\n# and --disable-dev-shm-usage avoids crashes from small /dev/shm\nRUN printf '#!/bin/bash\\nexec /usr/bin/chromium --no-sandbox --disable-dev-shm-usage \"$@\"\\n' \\\n > /usr/local/bin/chromium-wrapper \\\n && chmod +x /usr/local/bin/chromium-wrapper\n\n# Make the wrapper the default in the system .desktop file and via alternatives\nRUN sed -i 's|^Exec=.*|Exec=/usr/local/bin/chromium-wrapper %U|' \\\n /usr/share/applications/chromium.desktop \\\n && update-alternatives --install /usr/bin/x-www-browser x-www-browser \\\n /usr/local/bin/chromium-wrapper 100\n\n# Tell XFCE's exo-open that Chromium is the WebBrowser helper (system-wide)\nRUN mkdir -p /etc/xdg/xfce4 /usr/share/xfce4/helpers \\\n && printf 'WebBrowser=custom-WebBrowser\\n' > /etc/xdg/xfce4/helpers.rc \\\n && printf '[Desktop Entry]\\n\\\nVersion=1.0\\n\\\nType=X-XFCE-Helper\\n\\\nName=Chromium\\n\\\nIcon=chromium\\n\\\nX-XFCE-Category=WebBrowser\\n\\\nX-XFCE-CommandsWithParameter=/usr/local/bin/chromium-wrapper \"%%s\"\\n\\\nX-XFCE-Commands=/usr/local/bin/chromium-wrapper\\n' \\\n > /usr/share/xfce4/helpers/custom-WebBrowser.desktop\n\n# GitHub CLI\nRUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \\\n | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \\\n && echo \"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main\" \\\n | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \\\n && apt-get update && apt-get install -y --no-install-recommends gh \\\n && rm -rf /var/lib/apt/lists/*\n\n# Rust\nRUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y\nENV PATH=\"/root/.cargo/bin:${PATH}\"\nRUN rustup toolchain install nightly-2026-04-14 --profile minimal --component clippy,rustfmt\nRUN cargo install cargo-nextest --locked\nENV CARGO_INCREMENTAL=0\n\n# Bun\nRUN curl -fsSL https://bun.sh/install | bash\nENV PATH=\"/root/.bun/bin:${PATH}\"\n\nWORKDIR /root\n"
}
},
"resources": {
"cpu": 8,
"memory": "16GB",
"disk": "20GB"
},
"network": {
"mode": "allow_all",
"allow": []
},
"lifecycle": {
"preserve": false,
"stop_on_terminal": true,
"auto_stop": "30m"
},
"labels": {
"repo": "fabro-sh/fabro"
},
"volumes": [],
"env": {}
},
"notifications": {},
"interviews": {
"provider": null,
"slack": null
},
"agent": {
"fabro_tools": false,
"permissions": null,
"mcps": {}
},
"hooks": [],
"scm": {
"provider": null,
"owner": null,
"repository": null,
"github": null
},
"pull_request": {
"enabled": true,
"draft": false,
"auto_merge": false,
"merge_strategy": "squash"
},
"artifacts": {
"include": []
},
"integrations": {
"github": {
"permissions": {}
}
}
}
},
"graph": {
"name": "ImplementPlan",
"nodes": {
"fix_lints": {
"id": "fix_lints",
"attrs": {
"prompt": {
"String": "The preflight lint step failed. Read the build output from context and fix all clippy lint warnings."
},
"max_visits": {
"Integer": 3
},
"provider": {
"String": "anthropic"
},
"model": {
"String": "claude-opus-4-7"
},
"label": {
"String": "Fix Lints"
}
}
},
"fixup": {
"id": "fixup",
"attrs": {
"prompt": {
"String": "The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures."
},
"max_visits": {
"Integer": 3
},
"model": {
"String": "claude-opus-4-7"
},
"label": {
"String": "Fixup"
},
"provider": {
"String": "anthropic"
}
}
},
"implement": {
"id": "implement",
"attrs": {
"prompt": {
"String": "Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."
},
"model": {
"String": "gpt-5.5"
},
"label": {
"String": "Implement"
},
"reasoning_effort": {
"String": "xhigh"
},
"provider": {
"String": "openai"
}
}
},
"toolchain": {
"id": "toolchain",
"attrs": {
"label": {
"String": "Toolchain"
},
"provider": {
"String": "anthropic"
},
"max_retries": {
"Integer": 0
},
"script": {
"String": "command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1"
},
"shape": {
"String": "parallelogram"
},
"model": {
"String": "claude-opus-4-7"
}
}
},
"simplify_opus": {
"id": "simplify_opus",
"attrs": {
"provider": {
"String": "anthropic"
},
"prompt": {
"String": "# Simplify: Code Review and Cleanup\n\nReview changes vs. origin for reuse, quality, and efficiency. Fix any issues found.\n\n## Phase 1: Identify Changes\n\nRun git diff (or git diff HEAD if there are staged changes) to see what changed. If there are no git changes, review the most recently modified files that the user mentioned or that you edited earlier in this conversation.\n\n## Phase 2: Launch Three Review Agents in Parallel\n\nUse the Agent tool to launch all three agents concurrently in a single message. Pass each agent the full diff so it has the complete context.\n\n### Agent 1: Code Reuse Review\n\nFor each change:\n\n1. Search for existing utilities and helpers that could replace newly written code. Use Grep to find similar patterns elsewhere in the codebase — common locations are utility directories, shared modules, and files adjacent to the changed ones.\n2. Flag any new function that duplicates existing functionality. Suggest the existing function to use instead.\n3. Flag any inline logic that could use an existing utility — hand-rolled string manipulation, manual path handling, custom environment checks, ad-hoc type guards, and similar patterns are common candidates.\n\nNote: This is a greenfield app, so focus on maximizing simplicity and don't worry about changing things to achieve it.\n\n### Agent 2: Code Quality Review\n\nReview the same changes for hacky patterns:\n\n1. Redundant state: state that duplicates existing state, cached values that could be derived, observers/effects that could be direct calls\n2. Parameter sprawl: adding new parameters to a function instead of generalizing or restructuring existing ones\n3. Copy-paste with slight variation: near-duplicate code blocks that should be unified with a shared abstraction\n4. Leaky abstractions: exposing internal details that should be encapsulated, or breaking existing abstraction boundaries\n5. Stringly-typed code: using raw strings where constants, enums (string unions), or branded types already exist in the codebase\n\nNote: This is a greenfield app, so be aggressive in optimizing quality.\n\n### Agent 3: Efficiency Review\n\nReview the same changes for efficiency:\n\n1. Unnecessary work: redundant computations, repeated file reads, duplicate network/API calls, N+1 patterns\n2. Missed concurrency: independent operations run sequentially when they could run in parallel\n3. Hot-path bloat: new blocking work added to startup or per-request/per-render hot paths\n4. Unnecessary existence checks: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error\n5. Memory: unbounded data structures, missing cleanup, event listener leaks\n6. Overly broad operations: reading entire files when only a portion is needed, loading all items when filtering for one\n\n## Phase 3: Fix Issues\n\nWait for all three agents to complete. Aggregate their findings and fix each issue directly. If a finding is a false positive or not worth addressing, note it and move on — do not argue with the finding, just skip it.\n\nWhen done, briefly summarize what was fixed (or confirm the code was already clean)."
},
"model": {
"String": "claude-opus-4-7"
},
"label": {
"String": "Simplify (Opus)"
}
}
},
"verify": {
"id": "verify",
"attrs": {
"shape": {
"String": "parallelogram"
},
"retry_target": {
"String": "fixup"
},
"script": {
"String": "git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\\bActorRef\\b|\\bActorKind\\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\\s*==\\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1"
},
"model": {
"String": "claude-opus-4-7"
},
"provider": {
"String": "anthropic"
},
"goal_gate": {
"Boolean": true
},
"label": {
"String": "Verify"
}
}
},
"exit": {
"id": "exit",
"attrs": {
"shape": {
"String": "Msquare"
},
"provider": {
"String": "anthropic"
},
"label": {
"String": "Exit"
},
"model": {
"String": "claude-opus-4-7"
}
}
},
"preflight_lint": {
"id": "preflight_lint",
"attrs": {
"max_retries": {
"Integer": 0
},
"label": {
"String": "Preflight Lint"
},
"shape": {
"String": "parallelogram"
},
"model": {
"String": "claude-opus-4-7"
},
"script": {
"String": "cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1"
},
"provider": {
"String": "anthropic"
}
}
},
"start": {
"id": "start",
"attrs": {
"label": {
"String": "Start"
},
"provider": {
"String": "anthropic"
},
"model": {
"String": "claude-opus-4-7"
},
"shape": {
"String": "Mdiamond"
}
}
},
"simplify_gpt": {
"id": "simplify_gpt",
"attrs": {
"prompt": {
"String": "# Simplify: Code Review and Cleanup\n\nReview changes vs. origin for reuse, quality, and efficiency. Fix any issues found.\n\n## Phase 1: Identify Changes\n\nRun git diff (or git diff HEAD if there are staged changes) to see what changed. If there are no git changes, review the most recently modified files that the user mentioned or that you edited earlier in this conversation.\n\n## Phase 2: Launch Three Review Agents in Parallel\n\nUse the Agent tool to launch all three agents concurrently in a single message. Pass each agent the full diff so it has the complete context.\n\n### Agent 1: Code Reuse Review\n\nFor each change:\n\n1. Search for existing utilities and helpers that could replace newly written code. Use Grep to find similar patterns elsewhere in the codebase — common locations are utility directories, shared modules, and files adjacent to the changed ones.\n2. Flag any new function that duplicates existing functionality. Suggest the existing function to use instead.\n3. Flag any inline logic that could use an existing utility — hand-rolled string manipulation, manual path handling, custom environment checks, ad-hoc type guards, and similar patterns are common candidates.\n\nNote: This is a greenfield app, so focus on maximizing simplicity and don't worry about changing things to achieve it.\n\n### Agent 2: Code Quality Review\n\nReview the same changes for hacky patterns:\n\n1. Redundant state: state that duplicates existing state, cached values that could be derived, observers/effects that could be direct calls\n2. Parameter sprawl: adding new parameters to a function instead of generalizing or restructuring existing ones\n3. Copy-paste with slight variation: near-duplicate code blocks that should be unified with a shared abstraction\n4. Leaky abstractions: exposing internal details that should be encapsulated, or breaking existing abstraction boundaries\n5. Stringly-typed code: using raw strings where constants, enums (string unions), or branded types already exist in the codebase\n\nNote: This is a greenfield app, so be aggressive in optimizing quality.\n\n### Agent 3: Efficiency Review\n\nReview the same changes for efficiency:\n\n1. Unnecessary work: redundant computations, repeated file reads, duplicate network/API calls, N+1 patterns\n2. Missed concurrency: independent operations run sequentially when they could run in parallel\n3. Hot-path bloat: new blocking work added to startup or per-request/per-render hot paths\n4. Unnecessary existence checks: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error\n5. Memory: unbounded data structures, missing cleanup, event listener leaks\n6. Overly broad operations: reading entire files when only a portion is needed, loading all items when filtering for one\n\n## Phase 3: Fix Issues\n\nWait for all three agents to complete. Aggregate their findings and fix each issue directly. If a finding is a false positive or not worth addressing, note it and move on — do not argue with the finding, just skip it.\n\nWhen done, briefly summarize what was fixed (or confirm the code was already clean)."
},
"label": {
"String": "Simplify (GPT-55)"
},
"provider": {
"String": "openai"
},
"model": {
"String": "gpt-5.5"
}
}
},
"preflight_compile": {
"id": "preflight_compile",
"attrs": {
"shape": {
"String": "parallelogram"
},
"script": {
"String": "cargo check -q --workspace 2>&1"
},
"model": {
"String": "claude-opus-4-7"
},
"max_retries": {
"Integer": 0
},
"provider": {
"String": "anthropic"
},
"label": {
"String": "Preflight Compile"
}
}
}
},
"edges": [
{
"from": "start",
"to": "toolchain",
"attrs": {}
},
{
"from": "toolchain",
"to": "preflight_compile",
"attrs": {
"condition": {
"String": "outcome=succeeded"
}
}
},
{
"from": "toolchain",
"to": "exit",
"attrs": {}
},
{
"from": "preflight_compile",
"to": "preflight_lint",
"attrs": {
"condition": {
"String": "outcome=succeeded"
}
}
},
{
"from": "preflight_compile",
"to": "exit",
"attrs": {}
},
{
"from": "preflight_lint",
"to": "implement",
"attrs": {
"condition": {
"String": "outcome=succeeded"
}
}
},
{
"from": "preflight_lint",
"to": "fix_lints",
"attrs": {}
},
{
"from": "fix_lints",
"to": "preflight_lint",
"attrs": {}
},
{
"from": "implement",
"to": "simplify_opus",
"attrs": {}
},
{
"from": "simplify_opus",
"to": "simplify_gpt",
"attrs": {}
},
{
"from": "simplify_gpt",
"to": "verify",
"attrs": {}
},
{
"from": "verify",
"to": "exit",
"attrs": {
"condition": {
"String": "outcome=succeeded"
}
}
},
{
"from": "verify",
"to": "fixup",
"attrs": {}
},
{
"from": "fixup",
"to": "verify",
"attrs": {}
}
],
"attrs": {
"rankdir": {
"String": "LR"
},
"goal": {
"String": "---\ntitle: feat: StageProjection agent tools API\ntype: feat\nstatus: active\ndate: 2026-05-24\n---\n\n# feat: StageProjection Agent Tools API\n\n## Overview\n\nExpose the complete effective tool list for each agent-backed stage through `StageProjection`, so UI and API consumers can show actual tools such as `apply_patch`, `grep`, `glob`, `read_file`, MCP tools, skill tools, and subagent tools without inferring them from `permission_level`.\n\nThe API should expose tool summaries with `name`, `description`, `source`, `category`, and `invoked`. It must not expose full JSON parameter schemas in the run projection.\n\n## Problem Frame\n\nThe stage sidebar currently has permission metadata such as \"Full access\", but `permission_level` is only an access mode. It does not tell consumers which tools were actually exposed to the model after provider profile setup, optional tool registration, MCP integration, and tool access policy filtering.\n\nThe authoritative list already exists at request-build time in the agent session registry. The API needs to capture that effective list once per stage session and project it onto the stage.\n\n## Requirements Trace\n\n- R1. Add a StageProjection API field containing the complete effective tools for a stage session.\n- R2. Include `name`, `description`, `source`, `category`, and `invoked` for each tool.\n- R3. Do not infer tools from `permission_level` in backend or frontend code.\n- R4. Do not expose full tool parameter schemas through StageProjection.\n- R5. Preserve existing `permission_level` and `mcp_servers` fields for compatibility.\n- R6. Mark individual tools as invoked when matching `agent.tool.started` events are projected.\n- R7. Keep legacy runs backward compatible by defaulting missing tool lists to empty.\n\n## Scope Boundaries\n\n- Do not remove or rename `StageProjection.permission_level`.\n- Do not remove the existing MCP server projection or `AgentMcpToolSummary`.\n- Do not change completion API tool definitions.\n- Do not add ACP-native tool discovery in this pass unless an ACP path already has an equivalent effective tool list available.\n- Do not render parameter schemas in the web UI.\n\n## Context & Research\n\n### Relevant Code and Patterns\n\n- OpenAPI is the source of truth for HTTP contracts in `docs/public/api-reference/fabro-api.yaml`.\n- Shared API/projection DTOs should live in `fabro-types`, with `fabro-api/build.rs` replacements to avoid duplicate generated Rust types.\n- `StageProjection` lives in `lib/crates/fabro-types/src/run_projection.rs`.\n- Durable run event props live in `lib/crates/fabro-types/src/run_event/agent.rs` and `lib/crates/fabro-types/src/run_event/mod.rs`.\n- Workflow event conversion and event names live in `lib/crates/fabro-workflow/src/event/convert.rs`, `events.rs`, and `names.rs`.\n- Projection replay lives in `lib/crates/fabro-store/src/run_state.rs`.\n- The effective request tool list is built in `lib/crates/fabro-agent/src/session.rs` from `ToolRegistry::definitions_with_source_for_policy`.\n- Tool source metadata already exists as `ToolSource` and `ToolDefinitionWithSource` in `lib/crates/fabro-agent/src/tool_registry.rs`.\n- Tool category mapping already exists in `lib/crates/fabro-agent/src/tool_permissions.rs`.\n- The sidebar display lives in `apps/fabro-web/app/components/stage-insights-sidebar.tsx`.\n\n### Strategy Docs\n\n- Read `docs/internal/events-strategy.md` before adding the new durable event.\n- Read `docs/internal/testing-strategy.md` before adding or reorganizing tests.\n- Follow the OpenAPI type ownership guidance in `AGENTS.md`: reuse `fabro-types` through `fabro-api/build.rs` replacements when the API schema has the same product meaning and serde shape.\n\n## Key Technical Decisions\n\n- Add a new StageProjection field named `agent_tools`, not `tools`, to avoid ambiguity with MCP nested tools and completion tool definitions.\n- Add a dedicated durable event named `agent.tools.available` instead of overloading `agent.session.activated`.\n- Capture the effective tool list after session setup and filtering, using the same path as model request construction.\n- Store descriptions in the summary because they are useful API/UI metadata; omit parameter schemas to keep projection payloads small and avoid leaking full implementation detail.\n- Keep `AgentMcpToolSummary` MCP-only. Add a new general-purpose `AgentToolSummary` instead of stretching the MCP type beyond its meaning.\n- Treat `invoked` as projected state. The availability event should emit tools with `invoked: false`; replay of `agent.tool.started` flips matching tools to true.\n\n## API Contract\n\nAdd these schemas to OpenAPI and map them to `fabro_types` replacements:\n\n- `AgentToolSummary`\n - required: `name`, `description`, `source`, `category`, `invoked`\n - `name`: exposed tool name, e.g. `apply_patch` or `mcp__filesystem__read_file`\n - `description`: model-facing tool description\n - `source`: `AgentToolSource`\n - `category`: `AgentToolCategory`\n - `invoked`: boolean\n- `AgentToolSource`\n - tagged by `kind`\n - `native`\n - `mcp` with `server_name` and `original_name`\n - `skill`\n- `AgentToolCategory`\n - enum: `read`, `write`, `shell`, `subagent`, `other`\n- `AgentToolsAvailableProps`\n - required: `tools`, `visit`\n - `tools`: array of `AgentToolSummary`\n - `visit`: stage visit number\n\nAdd to `StageProjection`:\n\n- `agent_tools`: array of `AgentToolSummary`\n- Default to an empty array when omitted.\n- Skip serializing when empty, matching existing projection optional-list style.\n\nAdd event body:\n\n- Serialized event name: `agent.tools.available`\n- Event body type: `AgentToolsAvailableProps`\n\n## Implementation Units\n\n- [ ] **Unit 1: Add shared tool summary types**\n\n**Goal:** Define the canonical API/projection DTOs in `fabro-types`.\n\n**Files:**\n- Modify: `lib/crates/fabro-types/src/run_event/agent.rs`\n- Modify: `lib/crates/fabro-types/src/run_event/mod.rs`\n- Modify: `lib/crates/fabro-types/src/run_projection.rs`\n- Modify: `lib/crates/fabro-types/src/lib.rs`\n\n**Work:**\n- Add `AgentToolSummary`, `AgentToolSource`, `AgentToolCategory`, and `AgentToolsAvailableProps`.\n- Add `EventBody::AgentToolsAvailable` serialized as `agent.tools.available`.\n- Add `agent_tools: Vec<AgentToolSummary>` to `StageProjection`.\n- Ensure all new fields default cleanly for older persisted events/projections.\n- Export the new public types from `fabro-types`.\n\n- [ ] **Unit 2: Capture effective session tools**\n\n**Goal:** Provide an authoritative one-time source for the list that the model can actually call.\n\n**Files:**\n- Modify: `lib/crates/fabro-agent/src/session.rs`\n- Modify: `lib/crates/fabro-workflow/src/handler/llm/api.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/events.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/names.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/convert.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/stored_fields.rs` only if the new event needs non-standard stored fields.\n\n**Work:**\n- Add `Session::available_tools()` that returns the same effective `ToolDefinitionWithSource` list as `build_request()`.\n- Map `ToolDefinitionWithSource` to `AgentToolSummary` at the workflow boundary.\n- Populate `description` from `ToolDefinition.description`.\n- Populate `source` from `ToolSource`.\n- For MCP tools, include the server name from `ToolSource::Mcp` and derive `original_name` from the qualified exposed name using the existing MCP naming convention.\n- Populate `category` from the existing tool category mapping for known exposed names; use `other` when no category mapping applies.\n- Emit `agent.tools.available` once for the stage session after session setup and filtering are complete.\n\n- [ ] **Unit 3: Project available and invoked tools**\n\n**Goal:** Make `StageProjection.agent_tools` replay-authoritative.\n\n**Files:**\n- Modify: `lib/crates/fabro-store/src/run_state.rs`\n\n**Work:**\n- On `EventBody::AgentToolsAvailable`, replace the current stage visit's `agent_tools` with the event tools.\n- On `EventBody::AgentToolStarted`, find a matching `agent_tools` entry by exposed `tool_name` and set `invoked = true`.\n- Keep the existing MCP server `invoked` update unchanged.\n- If a legacy run has no availability event, do not synthesize a full list from permissions.\n\n- [ ] **Unit 4: Update OpenAPI and generated clients**\n\n**Goal:** Expose the new projection field and event contract through public API clients without duplicate Rust API types.\n\n**Files:**\n- Modify: `docs/public/api-reference/fabro-api.yaml`\n- Modify: `lib/crates/fabro-api/build.rs`\n- Modify: `lib/crates/fabro-api/src/lib.rs`\n- Modify generated files under `lib/crates/fabro-api/src/generated.rs` via `cargo build -p fabro-api`.\n- Modify generated files under `lib/packages/fabro-api-client/src` via TypeScript client generation.\n\n**Work:**\n- Add the OpenAPI schemas listed in the API Contract section.\n- Add `StageProjection.agent_tools`.\n- Add `fabro-api/build.rs` replacements for the new `fabro-types` types.\n- Regenerate Rust API code.\n- Regenerate TypeScript API client code.\n\n- [ ] **Unit 5: Render tools in the web sidebar**\n\n**Goal:** Replace the permission-derived sidebar display with the actual projected tool list.\n\n**Files:**\n- Modify: `apps/fabro-web/app/components/stage-insights-sidebar.tsx`\n- Modify: `apps/fabro-web/app/components/stage-insights-sidebar.test.tsx`\n\n**Work:**\n- Render `stage.agent_tools` when present.\n- Show each tool's name, description, source/category, and invoked state.\n- Keep permission level as secondary metadata or fallback for legacy stages with no `agent_tools`.\n- Do not infer tool availability from permission level.\n\n## Test Plan\n\n- `fabro-types`\n - JSON round-trip for `agent.tools.available`.\n - Serialization checks for `AgentToolSource` and `AgentToolCategory`.\n - Backward compatibility check that missing `agent_tools` deserializes to an empty list.\n\n- `fabro-workflow`\n - Event name and conversion tests for `agent.tools.available`.\n - Capture test proving native tools such as `apply_patch`, `grep`, and `glob` are emitted from the effective registry path.\n - MCP source mapping test for a qualified MCP tool name.\n\n- `fabro-store`\n - Projection test that `agent.tools.available` populates `StageProjection.agent_tools`.\n - Projection test that `agent.tool.started` marks only the matching tool as invoked.\n - Regression test that MCP server `invoked` status still updates as before.\n\n- `fabro-api`\n - Type identity/parity tests confirming API types reuse `fabro_types::AgentToolSummary`, `AgentToolSource`, `AgentToolCategory`, and `AgentToolsAvailableProps`.\n - StageProjection round-trip test including `agent_tools`.\n\n- `fabro-web`\n - Sidebar test rendering tool names and descriptions from `stage.agent_tools`.\n - Sidebar test showing invoked state.\n - Legacy fallback test for stages without `agent_tools`.\n\n## Run Checks\n\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo nextest run -p fabro-types -p fabro-workflow -p fabro-store -p fabro-api`\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cd apps/fabro-web && bun test && bun run typecheck`\n\n## Assumptions\n\n- The first implementation targets normal API-backed agent sessions, not ACP-native sessions.\n- `description` is safe to expose because it is already model-facing tool metadata, but parameter schemas remain out of scope for StageProjection.\n- `agent_tools` is a complete list only for runs that emit `agent.tools.available`; legacy runs return an empty list and may still show existing permission/MCP metadata.\n- If tool registration becomes mutable later, the event contract can be re-emitted and projection replacement semantics will still work.\n"
},
"model_stylesheet": {
"String": "\n * { model: claude-opus-4-7; }\n "
}
}
},
"graph_source": "digraph ImplementPlan {\n graph [\n goal=\"Implement and simplify\",\n model_stylesheet=\"\n * { model: claude-opus-4-7; }\n \"\n ]\n rankdir=LR\n\n start [shape=Mdiamond, label=\"Start\"]\n exit [shape=Msquare, label=\"Exit\"]\n\n toolchain [label=\"Toolchain\", shape=parallelogram, script=\"command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1\", max_retries=0]\n preflight_compile [label=\"Preflight Compile\", shape=parallelogram, script=\"cargo check -q --workspace 2>&1\", max_retries=0]\n preflight_lint [label=\"Preflight Lint\", shape=parallelogram, script=\"cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1\", max_retries=0]\n fix_lints [label=\"Fix Lints\", prompt=\"The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.\", max_visits=3]\n implement [label=\"Implement\", prompt=\"Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.\", model=\"gpt-55\", reasoning_effort=\"xhigh\"]\n simplify_opus [label=\"Simplify (Opus)\", prompt=\"@prompts/simplify.md\"]\n simplify_gpt [label=\"Simplify (GPT-55)\", prompt=\"@prompts/simplify.md\", model=\"gpt-55\"]\n verify [label=\"Verify\", shape=parallelogram, script=\"git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\\bActorRef\\b|\\bActorKind\\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\\s*==\\s*\\\"disabled\\\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1\", goal_gate=true, retry_target=\"fixup\"]\n fixup [label=\"Fixup\", prompt=\"The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.\", max_visits=3]\n\n start -> toolchain\n toolchain -> preflight_compile [condition=\"outcome=succeeded\"]\n toolchain -> exit\n preflight_compile -> preflight_lint [condition=\"outcome=succeeded\"]\n preflight_compile -> exit\n preflight_lint -> implement [condition=\"outcome=succeeded\"]\n preflight_lint -> fix_lints\n fix_lints -> preflight_lint\n implement -> simplify_opus -> simplify_gpt -> verify\n verify -> exit [condition=\"outcome=succeeded\"]\n verify -> fixup\n fixup -> verify\n}\n",
"workflow_slug": "implement-plan",
"source_directory": "/Users/bhelmkamp/p/fabro-sh/fabro",
"provenance": {
"server": {
"version": "0.243.0-nightly.1"
},
"client": {
"user_agent": "fabro-cli/0.243.0-nightly.1",
"name": "fabro-cli",
"version": "0.243.0-nightly.1"
},
"subject": {
"kind": "user",
"identity": {
"issuer": "https://github.com",
"subject": "19"
},
"login": "brynary",
"auth_method": "github",
"avatar_url": "https://avatars.githubusercontent.com/u/19?v=4"
}
},
"manifest_blob": "2ac72f30d22079488f07fbb21b92676401e3c67b0c1df6d34d45e684ffeda9c3",
"definition_blob": "303626c83b308220ef99468ae5d93ca4e254084c746b7c13fb24072fe5b0bfb2",
"git": {
"origin_url": "https://github.com/fabro-sh/fabro",
"branch": "main",
"sha": "6a23014f0bec59628a311b92ef6ad02c5e3b7bc3",
"dirty": "dirty",
"push_outcome": {
"type": "not_attempted"
}
}
},
"web_url": "http://127.0.0.1:32276/runs/01KSDFKD9JJS7D2ZJ6JQYXV58Z",
"start": {
"start_time": "2026-05-24T17:13:10.096340Z",
"run_branch": "fabro/run/01KSDFKD9JJS7D2ZJ6JQYXV58Z",
"base_sha": "6a23014f0bec59628a311b92ef6ad02c5e3b7bc3"
},
"status": {
"kind": "running"
},
"status_updated_at": "2026-05-24T17:13:10.096457Z",
"last_event_at": "2026-05-24T18:01:10.177955Z",
"pending_control": null,
"checkpoints": [
{
"seq": 21,
"checkpoint": {
"timestamp": "2026-05-24T17:13:12.111528Z",
"current_node": "start",
"completed_nodes": [
"start"
],
"node_retries": {},
"context_values": {
"failure_class": "",
"internal.node_visit_count": 1,
"current_node": "start",
"graph.goal": "---\ntitle: feat: StageProjection agent tools API\ntype: feat\nstatus: active\ndate: 2026-05-24\n---\n\n# feat: StageProjection Agent Tools API\n\n## Overview\n\nExpose the complete effective tool list for each agent-backed stage through `StageProjection`, so UI and API consumers can show actual tools such as `apply_patch`, `grep`, `glob`, `read_file`, MCP tools, skill tools, and subagent tools without inferring them from `permission_level`.\n\nThe API should expose tool summaries with `name`, `description`, `source`, `category`, and `invoked`. It must not expose full JSON parameter schemas in the run projection.\n\n## Problem Frame\n\nThe stage sidebar currently has permission metadata such as \"Full access\", but `permission_level` is only an access mode. It does not tell consumers which tools were actually exposed to the model after provider profile setup, optional tool registration, MCP integration, and tool access policy filtering.\n\nThe authoritative list already exists at request-build time in the agent session registry. The API needs to capture that effective list once per stage session and project it onto the stage.\n\n## Requirements Trace\n\n- R1. Add a StageProjection API field containing the complete effective tools for a stage session.\n- R2. Include `name`, `description`, `source`, `category`, and `invoked` for each tool.\n- R3. Do not infer tools from `permission_level` in backend or frontend code.\n- R4. Do not expose full tool parameter schemas through StageProjection.\n- R5. Preserve existing `permission_level` and `mcp_servers` fields for compatibility.\n- R6. Mark individual tools as invoked when matching `agent.tool.started` events are projected.\n- R7. Keep legacy runs backward compatible by defaulting missing tool lists to empty.\n\n## Scope Boundaries\n\n- Do not remove or rename `StageProjection.permission_level`.\n- Do not remove the existing MCP server projection or `AgentMcpToolSummary`.\n- Do not change completion API tool definitions.\n- Do not add ACP-native tool discovery in this pass unless an ACP path already has an equivalent effective tool list available.\n- Do not render parameter schemas in the web UI.\n\n## Context & Research\n\n### Relevant Code and Patterns\n\n- OpenAPI is the source of truth for HTTP contracts in `docs/public/api-reference/fabro-api.yaml`.\n- Shared API/projection DTOs should live in `fabro-types`, with `fabro-api/build.rs` replacements to avoid duplicate generated Rust types.\n- `StageProjection` lives in `lib/crates/fabro-types/src/run_projection.rs`.\n- Durable run event props live in `lib/crates/fabro-types/src/run_event/agent.rs` and `lib/crates/fabro-types/src/run_event/mod.rs`.\n- Workflow event conversion and event names live in `lib/crates/fabro-workflow/src/event/convert.rs`, `events.rs`, and `names.rs`.\n- Projection replay lives in `lib/crates/fabro-store/src/run_state.rs`.\n- The effective request tool list is built in `lib/crates/fabro-agent/src/session.rs` from `ToolRegistry::definitions_with_source_for_policy`.\n- Tool source metadata already exists as `ToolSource` and `ToolDefinitionWithSource` in `lib/crates/fabro-agent/src/tool_registry.rs`.\n- Tool category mapping already exists in `lib/crates/fabro-agent/src/tool_permissions.rs`.\n- The sidebar display lives in `apps/fabro-web/app/components/stage-insights-sidebar.tsx`.\n\n### Strategy Docs\n\n- Read `docs/internal/events-strategy.md` before adding the new durable event.\n- Read `docs/internal/testing-strategy.md` before adding or reorganizing tests.\n- Follow the OpenAPI type ownership guidance in `AGENTS.md`: reuse `fabro-types` through `fabro-api/build.rs` replacements when the API schema has the same product meaning and serde shape.\n\n## Key Technical Decisions\n\n- Add a new StageProjection field named `agent_tools`, not `tools`, to avoid ambiguity with MCP nested tools and completion tool definitions.\n- Add a dedicated durable event named `agent.tools.available` instead of overloading `agent.session.activated`.\n- Capture the effective tool list after session setup and filtering, using the same path as model request construction.\n- Store descriptions in the summary because they are useful API/UI metadata; omit parameter schemas to keep projection payloads small and avoid leaking full implementation detail.\n- Keep `AgentMcpToolSummary` MCP-only. Add a new general-purpose `AgentToolSummary` instead of stretching the MCP type beyond its meaning.\n- Treat `invoked` as projected state. The availability event should emit tools with `invoked: false`; replay of `agent.tool.started` flips matching tools to true.\n\n## API Contract\n\nAdd these schemas to OpenAPI and map them to `fabro_types` replacements:\n\n- `AgentToolSummary`\n - required: `name`, `description`, `source`, `category`, `invoked`\n - `name`: exposed tool name, e.g. `apply_patch` or `mcp__filesystem__read_file`\n - `description`: model-facing tool description\n - `source`: `AgentToolSource`\n - `category`: `AgentToolCategory`\n - `invoked`: boolean\n- `AgentToolSource`\n - tagged by `kind`\n - `native`\n - `mcp` with `server_name` and `original_name`\n - `skill`\n- `AgentToolCategory`\n - enum: `read`, `write`, `shell`, `subagent`, `other`\n- `AgentToolsAvailableProps`\n - required: `tools`, `visit`\n - `tools`: array of `AgentToolSummary`\n - `visit`: stage visit number\n\nAdd to `StageProjection`:\n\n- `agent_tools`: array of `AgentToolSummary`\n- Default to an empty array when omitted.\n- Skip serializing when empty, matching existing projection optional-list style.\n\nAdd event body:\n\n- Serialized event name: `agent.tools.available`\n- Event body type: `AgentToolsAvailableProps`\n\n## Implementation Units\n\n- [ ] **Unit 1: Add shared tool summary types**\n\n**Goal:** Define the canonical API/projection DTOs in `fabro-types`.\n\n**Files:**\n- Modify: `lib/crates/fabro-types/src/run_event/agent.rs`\n- Modify: `lib/crates/fabro-types/src/run_event/mod.rs`\n- Modify: `lib/crates/fabro-types/src/run_projection.rs`\n- Modify: `lib/crates/fabro-types/src/lib.rs`\n\n**Work:**\n- Add `AgentToolSummary`, `AgentToolSource`, `AgentToolCategory`, and `AgentToolsAvailableProps`.\n- Add `EventBody::AgentToolsAvailable` serialized as `agent.tools.available`.\n- Add `agent_tools: Vec<AgentToolSummary>` to `StageProjection`.\n- Ensure all new fields default cleanly for older persisted events/projections.\n- Export the new public types from `fabro-types`.\n\n- [ ] **Unit 2: Capture effective session tools**\n\n**Goal:** Provide an authoritative one-time source for the list that the model can actually call.\n\n**Files:**\n- Modify: `lib/crates/fabro-agent/src/session.rs`\n- Modify: `lib/crates/fabro-workflow/src/handler/llm/api.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/events.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/names.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/convert.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/stored_fields.rs` only if the new event needs non-standard stored fields.\n\n**Work:**\n- Add `Session::available_tools()` that returns the same effective `ToolDefinitionWithSource` list as `build_request()`.\n- Map `ToolDefinitionWithSource` to `AgentToolSummary` at the workflow boundary.\n- Populate `description` from `ToolDefinition.description`.\n- Populate `source` from `ToolSource`.\n- For MCP tools, include the server name from `ToolSource::Mcp` and derive `original_name` from the qualified exposed name using the existing MCP naming convention.\n- Populate `category` from the existing tool category mapping for known exposed names; use `other` when no category mapping applies.\n- Emit `agent.tools.available` once for the stage session after session setup and filtering are complete.\n\n- [ ] **Unit 3: Project available and invoked tools**\n\n**Goal:** Make `StageProjection.agent_tools` replay-authoritative.\n\n**Files:**\n- Modify: `lib/crates/fabro-store/src/run_state.rs`\n\n**Work:**\n- On `EventBody::AgentToolsAvailable`, replace the current stage visit's `agent_tools` with the event tools.\n- On `EventBody::AgentToolStarted`, find a matching `agent_tools` entry by exposed `tool_name` and set `invoked = true`.\n- Keep the existing MCP server `invoked` update unchanged.\n- If a legacy run has no availability event, do not synthesize a full list from permissions.\n\n- [ ] **Unit 4: Update OpenAPI and generated clients**\n\n**Goal:** Expose the new projection field and event contract through public API clients without duplicate Rust API types.\n\n**Files:**\n- Modify: `docs/public/api-reference/fabro-api.yaml`\n- Modify: `lib/crates/fabro-api/build.rs`\n- Modify: `lib/crates/fabro-api/src/lib.rs`\n- Modify generated files under `lib/crates/fabro-api/src/generated.rs` via `cargo build -p fabro-api`.\n- Modify generated files under `lib/packages/fabro-api-client/src` via TypeScript client generation.\n\n**Work:**\n- Add the OpenAPI schemas listed in the API Contract section.\n- Add `StageProjection.agent_tools`.\n- Add `fabro-api/build.rs` replacements for the new `fabro-types` types.\n- Regenerate Rust API code.\n- Regenerate TypeScript API client code.\n\n- [ ] **Unit 5: Render tools in the web sidebar**\n\n**Goal:** Replace the permission-derived sidebar display with the actual projected tool list.\n\n**Files:**\n- Modify: `apps/fabro-web/app/components/stage-insights-sidebar.tsx`\n- Modify: `apps/fabro-web/app/components/stage-insights-sidebar.test.tsx`\n\n**Work:**\n- Render `stage.agent_tools` when present.\n- Show each tool's name, description, source/category, and invoked state.\n- Keep permission level as secondary metadata or fallback for legacy stages with no `agent_tools`.\n- Do not infer tool availability from permission level.\n\n## Test Plan\n\n- `fabro-types`\n - JSON round-trip for `agent.tools.available`.\n - Serialization checks for `AgentToolSource` and `AgentToolCategory`.\n - Backward compatibility check that missing `agent_tools` deserializes to an empty list.\n\n- `fabro-workflow`\n - Event name and conversion tests for `agent.tools.available`.\n - Capture test proving native tools such as `apply_patch`, `grep`, and `glob` are emitted from the effective registry path.\n - MCP source mapping test for a qualified MCP tool name.\n\n- `fabro-store`\n - Projection test that `agent.tools.available` populates `StageProjection.agent_tools`.\n - Projection test that `agent.tool.started` marks only the matching tool as invoked.\n - Regression test that MCP server `invoked` status still updates as before.\n\n- `fabro-api`\n - Type identity/parity tests confirming API types reuse `fabro_types::AgentToolSummary`, `AgentToolSource`, `AgentToolCategory`, and `AgentToolsAvailableProps`.\n - StageProjection round-trip test including `agent_tools`.\n\n- `fabro-web`\n - Sidebar test rendering tool names and descriptions from `stage.agent_tools`.\n - Sidebar test showing invoked state.\n - Legacy fallback test for stages without `agent_tools`.\n\n## Run Checks\n\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo nextest run -p fabro-types -p fabro-workflow -p fabro-store -p fabro-api`\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cd apps/fabro-web && bun test && bun run typecheck`\n\n## Assumptions\n\n- The first implementation targets normal API-backed agent sessions, not ACP-native sessions.\n- `description` is safe to expose because it is already model-facing tool metadata, but parameter schemas remain out of scope for StageProjection.\n- `agent_tools` is a complete list only for runs that emit `agent.tools.available`; legacy runs return an empty list and may still show existing permission/MCP metadata.\n- If tool registration becomes mutable later, the event contract can be re-emitted and projection replacement semantics will still work.\n",
"graph.rankdir": "LR",
"internal.work_dir": "/home/daytona/workspace/fabro",
"internal.fidelity": "compact",
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
"failure_signature": "",
"internal.run_id": "01KSDFKD9JJS7D2ZJ6JQYXV58Z",
"internal.thread_id": null,
"outcome": "succeeded",
"internal.retry_count.start": 0
},
"node_outcomes": {
"start": {
"status": "succeeded",
"usage": null
}
},
"next_node_id": "toolchain",
"node_visits": {
"start": 1
}
},
"diff": {}
},
{
"seq": 29,
"checkpoint": {
"timestamp": "2026-05-24T17:13:17.064451Z",
"current_node": "toolchain",
"completed_nodes": [
"start",
"toolchain"
],
"node_retries": {},
"context_values": {
"internal.retry_count.start": 0,
"internal.fidelity": "compact",
"graph.rankdir": "LR",
"internal.node_visit_count": 1,
"failure_signature": "",
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
"internal.retry_count.toolchain": 0,
"current_node": "toolchain",
"internal.work_dir": "/home/daytona/workspace/fabro",
"internal.run_id": "01KSDFKD9JJS7D2ZJ6JQYXV58Z",
"internal.thread_id": "start",
"graph.goal": "---\ntitle: feat: StageProjection agent tools API\ntype: feat\nstatus: active\ndate: 2026-05-24\n---\n\n# feat: StageProjection Agent Tools API\n\n## Overview\n\nExpose the complete effective tool list for each agent-backed stage through `StageProjection`, so UI and API consumers can show actual tools such as `apply_patch`, `grep`, `glob`, `read_file`, MCP tools, skill tools, and subagent tools without inferring them from `permission_level`.\n\nThe API should expose tool summaries with `name`, `description`, `source`, `category`, and `invoked`. It must not expose full JSON parameter schemas in the run projection.\n\n## Problem Frame\n\nThe stage sidebar currently has permission metadata such as \"Full access\", but `permission_level` is only an access mode. It does not tell consumers which tools were actually exposed to the model after provider profile setup, optional tool registration, MCP integration, and tool access policy filtering.\n\nThe authoritative list already exists at request-build time in the agent session registry. The API needs to capture that effective list once per stage session and project it onto the stage.\n\n## Requirements Trace\n\n- R1. Add a StageProjection API field containing the complete effective tools for a stage session.\n- R2. Include `name`, `description`, `source`, `category`, and `invoked` for each tool.\n- R3. Do not infer tools from `permission_level` in backend or frontend code.\n- R4. Do not expose full tool parameter schemas through StageProjection.\n- R5. Preserve existing `permission_level` and `mcp_servers` fields for compatibility.\n- R6. Mark individual tools as invoked when matching `agent.tool.started` events are projected.\n- R7. Keep legacy runs backward compatible by defaulting missing tool lists to empty.\n\n## Scope Boundaries\n\n- Do not remove or rename `StageProjection.permission_level`.\n- Do not remove the existing MCP server projection or `AgentMcpToolSummary`.\n- Do not change completion API tool definitions.\n- Do not add ACP-native tool discovery in this pass unless an ACP path already has an equivalent effective tool list available.\n- Do not render parameter schemas in the web UI.\n\n## Context & Research\n\n### Relevant Code and Patterns\n\n- OpenAPI is the source of truth for HTTP contracts in `docs/public/api-reference/fabro-api.yaml`.\n- Shared API/projection DTOs should live in `fabro-types`, with `fabro-api/build.rs` replacements to avoid duplicate generated Rust types.\n- `StageProjection` lives in `lib/crates/fabro-types/src/run_projection.rs`.\n- Durable run event props live in `lib/crates/fabro-types/src/run_event/agent.rs` and `lib/crates/fabro-types/src/run_event/mod.rs`.\n- Workflow event conversion and event names live in `lib/crates/fabro-workflow/src/event/convert.rs`, `events.rs`, and `names.rs`.\n- Projection replay lives in `lib/crates/fabro-store/src/run_state.rs`.\n- The effective request tool list is built in `lib/crates/fabro-agent/src/session.rs` from `ToolRegistry::definitions_with_source_for_policy`.\n- Tool source metadata already exists as `ToolSource` and `ToolDefinitionWithSource` in `lib/crates/fabro-agent/src/tool_registry.rs`.\n- Tool category mapping already exists in `lib/crates/fabro-agent/src/tool_permissions.rs`.\n- The sidebar display lives in `apps/fabro-web/app/components/stage-insights-sidebar.tsx`.\n\n### Strategy Docs\n\n- Read `docs/internal/events-strategy.md` before adding the new durable event.\n- Read `docs/internal/testing-strategy.md` before adding or reorganizing tests.\n- Follow the OpenAPI type ownership guidance in `AGENTS.md`: reuse `fabro-types` through `fabro-api/build.rs` replacements when the API schema has the same product meaning and serde shape.\n\n## Key Technical Decisions\n\n- Add a new StageProjection field named `agent_tools`, not `tools`, to avoid ambiguity with MCP nested tools and completion tool definitions.\n- Add a dedicated durable event named `agent.tools.available` instead of overloading `agent.session.activated`.\n- Capture the effective tool list after session setup and filtering, using the same path as model request construction.\n- Store descriptions in the summary because they are useful API/UI metadata; omit parameter schemas to keep projection payloads small and avoid leaking full implementation detail.\n- Keep `AgentMcpToolSummary` MCP-only. Add a new general-purpose `AgentToolSummary` instead of stretching the MCP type beyond its meaning.\n- Treat `invoked` as projected state. The availability event should emit tools with `invoked: false`; replay of `agent.tool.started` flips matching tools to true.\n\n## API Contract\n\nAdd these schemas to OpenAPI and map them to `fabro_types` replacements:\n\n- `AgentToolSummary`\n - required: `name`, `description`, `source`, `category`, `invoked`\n - `name`: exposed tool name, e.g. `apply_patch` or `mcp__filesystem__read_file`\n - `description`: model-facing tool description\n - `source`: `AgentToolSource`\n - `category`: `AgentToolCategory`\n - `invoked`: boolean\n- `AgentToolSource`\n - tagged by `kind`\n - `native`\n - `mcp` with `server_name` and `original_name`\n - `skill`\n- `AgentToolCategory`\n - enum: `read`, `write`, `shell`, `subagent`, `other`\n- `AgentToolsAvailableProps`\n - required: `tools`, `visit`\n - `tools`: array of `AgentToolSummary`\n - `visit`: stage visit number\n\nAdd to `StageProjection`:\n\n- `agent_tools`: array of `AgentToolSummary`\n- Default to an empty array when omitted.\n- Skip serializing when empty, matching existing projection optional-list style.\n\nAdd event body:\n\n- Serialized event name: `agent.tools.available`\n- Event body type: `AgentToolsAvailableProps`\n\n## Implementation Units\n\n- [ ] **Unit 1: Add shared tool summary types**\n\n**Goal:** Define the canonical API/projection DTOs in `fabro-types`.\n\n**Files:**\n- Modify: `lib/crates/fabro-types/src/run_event/agent.rs`\n- Modify: `lib/crates/fabro-types/src/run_event/mod.rs`\n- Modify: `lib/crates/fabro-types/src/run_projection.rs`\n- Modify: `lib/crates/fabro-types/src/lib.rs`\n\n**Work:**\n- Add `AgentToolSummary`, `AgentToolSource`, `AgentToolCategory`, and `AgentToolsAvailableProps`.\n- Add `EventBody::AgentToolsAvailable` serialized as `agent.tools.available`.\n- Add `agent_tools: Vec<AgentToolSummary>` to `StageProjection`.\n- Ensure all new fields default cleanly for older persisted events/projections.\n- Export the new public types from `fabro-types`.\n\n- [ ] **Unit 2: Capture effective session tools**\n\n**Goal:** Provide an authoritative one-time source for the list that the model can actually call.\n\n**Files:**\n- Modify: `lib/crates/fabro-agent/src/session.rs`\n- Modify: `lib/crates/fabro-workflow/src/handler/llm/api.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/events.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/names.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/convert.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/stored_fields.rs` only if the new event needs non-standard stored fields.\n\n**Work:**\n- Add `Session::available_tools()` that returns the same effective `ToolDefinitionWithSource` list as `build_request()`.\n- Map `ToolDefinitionWithSource` to `AgentToolSummary` at the workflow boundary.\n- Populate `description` from `ToolDefinition.description`.\n- Populate `source` from `ToolSource`.\n- For MCP tools, include the server name from `ToolSource::Mcp` and derive `original_name` from the qualified exposed name using the existing MCP naming convention.\n- Populate `category` from the existing tool category mapping for known exposed names; use `other` when no category mapping applies.\n- Emit `agent.tools.available` once for the stage session after session setup and filtering are complete.\n\n- [ ] **Unit 3: Project available and invoked tools**\n\n**Goal:** Make `StageProjection.agent_tools` replay-authoritative.\n\n**Files:**\n- Modify: `lib/crates/fabro-store/src/run_state.rs`\n\n**Work:**\n- On `EventBody::AgentToolsAvailable`, replace the current stage visit's `agent_tools` with the event tools.\n- On `EventBody::AgentToolStarted`, find a matching `agent_tools` entry by exposed `tool_name` and set `invoked = true`.\n- Keep the existing MCP server `invoked` update unchanged.\n- If a legacy run has no availability event, do not synthesize a full list from permissions.\n\n- [ ] **Unit 4: Update OpenAPI and generated clients**\n\n**Goal:** Expose the new projection field and event contract through public API clients without duplicate Rust API types.\n\n**Files:**\n- Modify: `docs/public/api-reference/fabro-api.yaml`\n- Modify: `lib/crates/fabro-api/build.rs`\n- Modify: `lib/crates/fabro-api/src/lib.rs`\n- Modify generated files under `lib/crates/fabro-api/src/generated.rs` via `cargo build -p fabro-api`.\n- Modify generated files under `lib/packages/fabro-api-client/src` via TypeScript client generation.\n\n**Work:**\n- Add the OpenAPI schemas listed in the API Contract section.\n- Add `StageProjection.agent_tools`.\n- Add `fabro-api/build.rs` replacements for the new `fabro-types` types.\n- Regenerate Rust API code.\n- Regenerate TypeScript API client code.\n\n- [ ] **Unit 5: Render tools in the web sidebar**\n\n**Goal:** Replace the permission-derived sidebar display with the actual projected tool list.\n\n**Files:**\n- Modify: `apps/fabro-web/app/components/stage-insights-sidebar.tsx`\n- Modify: `apps/fabro-web/app/components/stage-insights-sidebar.test.tsx`\n\n**Work:**\n- Render `stage.agent_tools` when present.\n- Show each tool's name, description, source/category, and invoked state.\n- Keep permission level as secondary metadata or fallback for legacy stages with no `agent_tools`.\n- Do not infer tool availability from permission level.\n\n## Test Plan\n\n- `fabro-types`\n - JSON round-trip for `agent.tools.available`.\n - Serialization checks for `AgentToolSource` and `AgentToolCategory`.\n - Backward compatibility check that missing `agent_tools` deserializes to an empty list.\n\n- `fabro-workflow`\n - Event name and conversion tests for `agent.tools.available`.\n - Capture test proving native tools such as `apply_patch`, `grep`, and `glob` are emitted from the effective registry path.\n - MCP source mapping test for a qualified MCP tool name.\n\n- `fabro-store`\n - Projection test that `agent.tools.available` populates `StageProjection.agent_tools`.\n - Projection test that `agent.tool.started` marks only the matching tool as invoked.\n - Regression test that MCP server `invoked` status still updates as before.\n\n- `fabro-api`\n - Type identity/parity tests confirming API types reuse `fabro_types::AgentToolSummary`, `AgentToolSource`, `AgentToolCategory`, and `AgentToolsAvailableProps`.\n - StageProjection round-trip test including `agent_tools`.\n\n- `fabro-web`\n - Sidebar test rendering tool names and descriptions from `stage.agent_tools`.\n - Sidebar test showing invoked state.\n - Legacy fallback test for stages without `agent_tools`.\n\n## Run Checks\n\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo nextest run -p fabro-types -p fabro-workflow -p fabro-store -p fabro-api`\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cd apps/fabro-web && bun test && bun run typecheck`\n\n## Assumptions\n\n- The first implementation targets normal API-backed agent sessions, not ACP-native sessions.\n- `description` is safe to expose because it is already model-facing tool metadata, but parameter schemas remain out of scope for StageProjection.\n- `agent_tools` is a complete list only for runs that emit `agent.tools.available`; legacy runs return an empty list and may still show existing permission/MCP metadata.\n- If tool registration becomes mutable later, the event contract can be re-emitted and projection replacement semantics will still work.\n",
"thread.start.current_node": "toolchain",
"failure_class": "",
"outcome": "succeeded",
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
},
"node_outcomes": {
"toolchain": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
},
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
"usage": null
},
"start": {
"status": "succeeded",
"usage": null
}
},
"next_node_id": "preflight_compile",
"git_commit_sha": "8f3c0e3d6d0b064c6704dce2103c5081094383fc",
"node_visits": {
"start": 1,
"toolchain": 1
}
},
"diff": {
"summary": {
"files_changed": 0,
"additions": 0,
"deletions": 0
}
}
},
{
"seq": 39,
"checkpoint": {
"timestamp": "2026-05-24T17:15:21.466359Z",
"current_node": "preflight_compile",
"completed_nodes": [
"start",
"toolchain",
"preflight_compile"
],
"node_retries": {},
"context_values": {
"graph.rankdir": "LR",
"failure_signature": "",
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"failure_class": "",
"internal.retry_count.start": 0,
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
"internal.node_visit_count": 1,
"internal.run_id": "01KSDFKD9JJS7D2ZJ6JQYXV58Z",
"thread.toolchain.current_node": "preflight_compile",
"internal.work_dir": "/home/daytona/workspace/fabro",
"thread.start.current_node": "toolchain",
"current_node": "preflight_compile",
"internal.thread_id": "toolchain",
"internal.retry_count.preflight_compile": 0,
"outcome": "succeeded",
"graph.goal": "---\ntitle: feat: StageProjection agent tools API\ntype: feat\nstatus: active\ndate: 2026-05-24\n---\n\n# feat: StageProjection Agent Tools API\n\n## Overview\n\nExpose the complete effective tool list for each agent-backed stage through `StageProjection`, so UI and API consumers can show actual tools such as `apply_patch`, `grep`, `glob`, `read_file`, MCP tools, skill tools, and subagent tools without inferring them from `permission_level`.\n\nThe API should expose tool summaries with `name`, `description`, `source`, `category`, and `invoked`. It must not expose full JSON parameter schemas in the run projection.\n\n## Problem Frame\n\nThe stage sidebar currently has permission metadata such as \"Full access\", but `permission_level` is only an access mode. It does not tell consumers which tools were actually exposed to the model after provider profile setup, optional tool registration, MCP integration, and tool access policy filtering.\n\nThe authoritative list already exists at request-build time in the agent session registry. The API needs to capture that effective list once per stage session and project it onto the stage.\n\n## Requirements Trace\n\n- R1. Add a StageProjection API field containing the complete effective tools for a stage session.\n- R2. Include `name`, `description`, `source`, `category`, and `invoked` for each tool.\n- R3. Do not infer tools from `permission_level` in backend or frontend code.\n- R4. Do not expose full tool parameter schemas through StageProjection.\n- R5. Preserve existing `permission_level` and `mcp_servers` fields for compatibility.\n- R6. Mark individual tools as invoked when matching `agent.tool.started` events are projected.\n- R7. Keep legacy runs backward compatible by defaulting missing tool lists to empty.\n\n## Scope Boundaries\n\n- Do not remove or rename `StageProjection.permission_level`.\n- Do not remove the existing MCP server projection or `AgentMcpToolSummary`.\n- Do not change completion API tool definitions.\n- Do not add ACP-native tool discovery in this pass unless an ACP path already has an equivalent effective tool list available.\n- Do not render parameter schemas in the web UI.\n\n## Context & Research\n\n### Relevant Code and Patterns\n\n- OpenAPI is the source of truth for HTTP contracts in `docs/public/api-reference/fabro-api.yaml`.\n- Shared API/projection DTOs should live in `fabro-types`, with `fabro-api/build.rs` replacements to avoid duplicate generated Rust types.\n- `StageProjection` lives in `lib/crates/fabro-types/src/run_projection.rs`.\n- Durable run event props live in `lib/crates/fabro-types/src/run_event/agent.rs` and `lib/crates/fabro-types/src/run_event/mod.rs`.\n- Workflow event conversion and event names live in `lib/crates/fabro-workflow/src/event/convert.rs`, `events.rs`, and `names.rs`.\n- Projection replay lives in `lib/crates/fabro-store/src/run_state.rs`.\n- The effective request tool list is built in `lib/crates/fabro-agent/src/session.rs` from `ToolRegistry::definitions_with_source_for_policy`.\n- Tool source metadata already exists as `ToolSource` and `ToolDefinitionWithSource` in `lib/crates/fabro-agent/src/tool_registry.rs`.\n- Tool category mapping already exists in `lib/crates/fabro-agent/src/tool_permissions.rs`.\n- The sidebar display lives in `apps/fabro-web/app/components/stage-insights-sidebar.tsx`.\n\n### Strategy Docs\n\n- Read `docs/internal/events-strategy.md` before adding the new durable event.\n- Read `docs/internal/testing-strategy.md` before adding or reorganizing tests.\n- Follow the OpenAPI type ownership guidance in `AGENTS.md`: reuse `fabro-types` through `fabro-api/build.rs` replacements when the API schema has the same product meaning and serde shape.\n\n## Key Technical Decisions\n\n- Add a new StageProjection field named `agent_tools`, not `tools`, to avoid ambiguity with MCP nested tools and completion tool definitions.\n- Add a dedicated durable event named `agent.tools.available` instead of overloading `agent.session.activated`.\n- Capture the effective tool list after session setup and filtering, using the same path as model request construction.\n- Store descriptions in the summary because they are useful API/UI metadata; omit parameter schemas to keep projection payloads small and avoid leaking full implementation detail.\n- Keep `AgentMcpToolSummary` MCP-only. Add a new general-purpose `AgentToolSummary` instead of stretching the MCP type beyond its meaning.\n- Treat `invoked` as projected state. The availability event should emit tools with `invoked: false`; replay of `agent.tool.started` flips matching tools to true.\n\n## API Contract\n\nAdd these schemas to OpenAPI and map them to `fabro_types` replacements:\n\n- `AgentToolSummary`\n - required: `name`, `description`, `source`, `category`, `invoked`\n - `name`: exposed tool name, e.g. `apply_patch` or `mcp__filesystem__read_file`\n - `description`: model-facing tool description\n - `source`: `AgentToolSource`\n - `category`: `AgentToolCategory`\n - `invoked`: boolean\n- `AgentToolSource`\n - tagged by `kind`\n - `native`\n - `mcp` with `server_name` and `original_name`\n - `skill`\n- `AgentToolCategory`\n - enum: `read`, `write`, `shell`, `subagent`, `other`\n- `AgentToolsAvailableProps`\n - required: `tools`, `visit`\n - `tools`: array of `AgentToolSummary`\n - `visit`: stage visit number\n\nAdd to `StageProjection`:\n\n- `agent_tools`: array of `AgentToolSummary`\n- Default to an empty array when omitted.\n- Skip serializing when empty, matching existing projection optional-list style.\n\nAdd event body:\n\n- Serialized event name: `agent.tools.available`\n- Event body type: `AgentToolsAvailableProps`\n\n## Implementation Units\n\n- [ ] **Unit 1: Add shared tool summary types**\n\n**Goal:** Define the canonical API/projection DTOs in `fabro-types`.\n\n**Files:**\n- Modify: `lib/crates/fabro-types/src/run_event/agent.rs`\n- Modify: `lib/crates/fabro-types/src/run_event/mod.rs`\n- Modify: `lib/crates/fabro-types/src/run_projection.rs`\n- Modify: `lib/crates/fabro-types/src/lib.rs`\n\n**Work:**\n- Add `AgentToolSummary`, `AgentToolSource`, `AgentToolCategory`, and `AgentToolsAvailableProps`.\n- Add `EventBody::AgentToolsAvailable` serialized as `agent.tools.available`.\n- Add `agent_tools: Vec<AgentToolSummary>` to `StageProjection`.\n- Ensure all new fields default cleanly for older persisted events/projections.\n- Export the new public types from `fabro-types`.\n\n- [ ] **Unit 2: Capture effective session tools**\n\n**Goal:** Provide an authoritative one-time source for the list that the model can actually call.\n\n**Files:**\n- Modify: `lib/crates/fabro-agent/src/session.rs`\n- Modify: `lib/crates/fabro-workflow/src/handler/llm/api.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/events.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/names.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/convert.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/stored_fields.rs` only if the new event needs non-standard stored fields.\n\n**Work:**\n- Add `Session::available_tools()` that returns the same effective `ToolDefinitionWithSource` list as `build_request()`.\n- Map `ToolDefinitionWithSource` to `AgentToolSummary` at the workflow boundary.\n- Populate `description` from `ToolDefinition.description`.\n- Populate `source` from `ToolSource`.\n- For MCP tools, include the server name from `ToolSource::Mcp` and derive `original_name` from the qualified exposed name using the existing MCP naming convention.\n- Populate `category` from the existing tool category mapping for known exposed names; use `other` when no category mapping applies.\n- Emit `agent.tools.available` once for the stage session after session setup and filtering are complete.\n\n- [ ] **Unit 3: Project available and invoked tools**\n\n**Goal:** Make `StageProjection.agent_tools` replay-authoritative.\n\n**Files:**\n- Modify: `lib/crates/fabro-store/src/run_state.rs`\n\n**Work:**\n- On `EventBody::AgentToolsAvailable`, replace the current stage visit's `agent_tools` with the event tools.\n- On `EventBody::AgentToolStarted`, find a matching `agent_tools` entry by exposed `tool_name` and set `invoked = true`.\n- Keep the existing MCP server `invoked` update unchanged.\n- If a legacy run has no availability event, do not synthesize a full list from permissions.\n\n- [ ] **Unit 4: Update OpenAPI and generated clients**\n\n**Goal:** Expose the new projection field and event contract through public API clients without duplicate Rust API types.\n\n**Files:**\n- Modify: `docs/public/api-reference/fabro-api.yaml`\n- Modify: `lib/crates/fabro-api/build.rs`\n- Modify: `lib/crates/fabro-api/src/lib.rs`\n- Modify generated files under `lib/crates/fabro-api/src/generated.rs` via `cargo build -p fabro-api`.\n- Modify generated files under `lib/packages/fabro-api-client/src` via TypeScript client generation.\n\n**Work:**\n- Add the OpenAPI schemas listed in the API Contract section.\n- Add `StageProjection.agent_tools`.\n- Add `fabro-api/build.rs` replacements for the new `fabro-types` types.\n- Regenerate Rust API code.\n- Regenerate TypeScript API client code.\n\n- [ ] **Unit 5: Render tools in the web sidebar**\n\n**Goal:** Replace the permission-derived sidebar display with the actual projected tool list.\n\n**Files:**\n- Modify: `apps/fabro-web/app/components/stage-insights-sidebar.tsx`\n- Modify: `apps/fabro-web/app/components/stage-insights-sidebar.test.tsx`\n\n**Work:**\n- Render `stage.agent_tools` when present.\n- Show each tool's name, description, source/category, and invoked state.\n- Keep permission level as secondary metadata or fallback for legacy stages with no `agent_tools`.\n- Do not infer tool availability from permission level.\n\n## Test Plan\n\n- `fabro-types`\n - JSON round-trip for `agent.tools.available`.\n - Serialization checks for `AgentToolSource` and `AgentToolCategory`.\n - Backward compatibility check that missing `agent_tools` deserializes to an empty list.\n\n- `fabro-workflow`\n - Event name and conversion tests for `agent.tools.available`.\n - Capture test proving native tools such as `apply_patch`, `grep`, and `glob` are emitted from the effective registry path.\n - MCP source mapping test for a qualified MCP tool name.\n\n- `fabro-store`\n - Projection test that `agent.tools.available` populates `StageProjection.agent_tools`.\n - Projection test that `agent.tool.started` marks only the matching tool as invoked.\n - Regression test that MCP server `invoked` status still updates as before.\n\n- `fabro-api`\n - Type identity/parity tests confirming API types reuse `fabro_types::AgentToolSummary`, `AgentToolSource`, `AgentToolCategory`, and `AgentToolsAvailableProps`.\n - StageProjection round-trip test including `agent_tools`.\n\n- `fabro-web`\n - Sidebar test rendering tool names and descriptions from `stage.agent_tools`.\n - Sidebar test showing invoked state.\n - Legacy fallback test for stages without `agent_tools`.\n\n## Run Checks\n\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo nextest run -p fabro-types -p fabro-workflow -p fabro-store -p fabro-api`\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cd apps/fabro-web && bun test && bun run typecheck`\n\n## Assumptions\n\n- The first implementation targets normal API-backed agent sessions, not ACP-native sessions.\n- `description` is safe to expose because it is already model-facing tool metadata, but parameter schemas remain out of scope for StageProjection.\n- `agent_tools` is a complete list only for runs that emit `agent.tools.available`; legacy runs return an empty list and may still show existing permission/MCP metadata.\n- If tool registration becomes mutable later, the event contract can be re-emitted and projection replacement semantics will still work.\n",
"internal.fidelity": "compact",
"internal.retry_count.toolchain": 0
},
"node_outcomes": {
"preflight_compile": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
},
"notes": "Script completed: cargo check -q --workspace 2>&1",
"usage": null
},
"toolchain": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
},
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
"usage": null
},
"start": {
"status": "succeeded",
"usage": null
}
},
"next_node_id": "preflight_lint",
"git_commit_sha": "f50c4a28285b0b8a44d9bd34fb8862172e034e7b",
"node_visits": {
"toolchain": 1,
"preflight_compile": 1,
"start": 1
}
},
"diff": {
"summary": {
"files_changed": 0,
"additions": 0,
"deletions": 0
}
}
},
{
"seq": 49,
"checkpoint": {
"timestamp": "2026-05-24T17:17:39.925475Z",
"current_node": "preflight_lint",
"completed_nodes": [
"start",
"toolchain",
"preflight_compile",
"preflight_lint"
],
"node_retries": {},
"context_values": {
"internal.thread_id": "preflight_compile",
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"internal.run_id": "01KSDFKD9JJS7D2ZJ6JQYXV58Z",
"failure_class": "",
"internal.work_dir": "/home/daytona/workspace/fabro",
"thread.start.current_node": "toolchain",
"internal.retry_count.preflight_lint": 0,
"internal.fidelity": "compact",
"current_node": "preflight_lint",
"internal.retry_count.start": 0,
"thread.preflight_compile.current_node": "preflight_lint",
"graph.goal": "---\ntitle: feat: StageProjection agent tools API\ntype: feat\nstatus: active\ndate: 2026-05-24\n---\n\n# feat: StageProjection Agent Tools API\n\n## Overview\n\nExpose the complete effective tool list for each agent-backed stage through `StageProjection`, so UI and API consumers can show actual tools such as `apply_patch`, `grep`, `glob`, `read_file`, MCP tools, skill tools, and subagent tools without inferring them from `permission_level`.\n\nThe API should expose tool summaries with `name`, `description`, `source`, `category`, and `invoked`. It must not expose full JSON parameter schemas in the run projection.\n\n## Problem Frame\n\nThe stage sidebar currently has permission metadata such as \"Full access\", but `permission_level` is only an access mode. It does not tell consumers which tools were actually exposed to the model after provider profile setup, optional tool registration, MCP integration, and tool access policy filtering.\n\nThe authoritative list already exists at request-build time in the agent session registry. The API needs to capture that effective list once per stage session and project it onto the stage.\n\n## Requirements Trace\n\n- R1. Add a StageProjection API field containing the complete effective tools for a stage session.\n- R2. Include `name`, `description`, `source`, `category`, and `invoked` for each tool.\n- R3. Do not infer tools from `permission_level` in backend or frontend code.\n- R4. Do not expose full tool parameter schemas through StageProjection.\n- R5. Preserve existing `permission_level` and `mcp_servers` fields for compatibility.\n- R6. Mark individual tools as invoked when matching `agent.tool.started` events are projected.\n- R7. Keep legacy runs backward compatible by defaulting missing tool lists to empty.\n\n## Scope Boundaries\n\n- Do not remove or rename `StageProjection.permission_level`.\n- Do not remove the existing MCP server projection or `AgentMcpToolSummary`.\n- Do not change completion API tool definitions.\n- Do not add ACP-native tool discovery in this pass unless an ACP path already has an equivalent effective tool list available.\n- Do not render parameter schemas in the web UI.\n\n## Context & Research\n\n### Relevant Code and Patterns\n\n- OpenAPI is the source of truth for HTTP contracts in `docs/public/api-reference/fabro-api.yaml`.\n- Shared API/projection DTOs should live in `fabro-types`, with `fabro-api/build.rs` replacements to avoid duplicate generated Rust types.\n- `StageProjection` lives in `lib/crates/fabro-types/src/run_projection.rs`.\n- Durable run event props live in `lib/crates/fabro-types/src/run_event/agent.rs` and `lib/crates/fabro-types/src/run_event/mod.rs`.\n- Workflow event conversion and event names live in `lib/crates/fabro-workflow/src/event/convert.rs`, `events.rs`, and `names.rs`.\n- Projection replay lives in `lib/crates/fabro-store/src/run_state.rs`.\n- The effective request tool list is built in `lib/crates/fabro-agent/src/session.rs` from `ToolRegistry::definitions_with_source_for_policy`.\n- Tool source metadata already exists as `ToolSource` and `ToolDefinitionWithSource` in `lib/crates/fabro-agent/src/tool_registry.rs`.\n- Tool category mapping already exists in `lib/crates/fabro-agent/src/tool_permissions.rs`.\n- The sidebar display lives in `apps/fabro-web/app/components/stage-insights-sidebar.tsx`.\n\n### Strategy Docs\n\n- Read `docs/internal/events-strategy.md` before adding the new durable event.\n- Read `docs/internal/testing-strategy.md` before adding or reorganizing tests.\n- Follow the OpenAPI type ownership guidance in `AGENTS.md`: reuse `fabro-types` through `fabro-api/build.rs` replacements when the API schema has the same product meaning and serde shape.\n\n## Key Technical Decisions\n\n- Add a new StageProjection field named `agent_tools`, not `tools`, to avoid ambiguity with MCP nested tools and completion tool definitions.\n- Add a dedicated durable event named `agent.tools.available` instead of overloading `agent.session.activated`.\n- Capture the effective tool list after session setup and filtering, using the same path as model request construction.\n- Store descriptions in the summary because they are useful API/UI metadata; omit parameter schemas to keep projection payloads small and avoid leaking full implementation detail.\n- Keep `AgentMcpToolSummary` MCP-only. Add a new general-purpose `AgentToolSummary` instead of stretching the MCP type beyond its meaning.\n- Treat `invoked` as projected state. The availability event should emit tools with `invoked: false`; replay of `agent.tool.started` flips matching tools to true.\n\n## API Contract\n\nAdd these schemas to OpenAPI and map them to `fabro_types` replacements:\n\n- `AgentToolSummary`\n - required: `name`, `description`, `source`, `category`, `invoked`\n - `name`: exposed tool name, e.g. `apply_patch` or `mcp__filesystem__read_file`\n - `description`: model-facing tool description\n - `source`: `AgentToolSource`\n - `category`: `AgentToolCategory`\n - `invoked`: boolean\n- `AgentToolSource`\n - tagged by `kind`\n - `native`\n - `mcp` with `server_name` and `original_name`\n - `skill`\n- `AgentToolCategory`\n - enum: `read`, `write`, `shell`, `subagent`, `other`\n- `AgentToolsAvailableProps`\n - required: `tools`, `visit`\n - `tools`: array of `AgentToolSummary`\n - `visit`: stage visit number\n\nAdd to `StageProjection`:\n\n- `agent_tools`: array of `AgentToolSummary`\n- Default to an empty array when omitted.\n- Skip serializing when empty, matching existing projection optional-list style.\n\nAdd event body:\n\n- Serialized event name: `agent.tools.available`\n- Event body type: `AgentToolsAvailableProps`\n\n## Implementation Units\n\n- [ ] **Unit 1: Add shared tool summary types**\n\n**Goal:** Define the canonical API/projection DTOs in `fabro-types`.\n\n**Files:**\n- Modify: `lib/crates/fabro-types/src/run_event/agent.rs`\n- Modify: `lib/crates/fabro-types/src/run_event/mod.rs`\n- Modify: `lib/crates/fabro-types/src/run_projection.rs`\n- Modify: `lib/crates/fabro-types/src/lib.rs`\n\n**Work:**\n- Add `AgentToolSummary`, `AgentToolSource`, `AgentToolCategory`, and `AgentToolsAvailableProps`.\n- Add `EventBody::AgentToolsAvailable` serialized as `agent.tools.available`.\n- Add `agent_tools: Vec<AgentToolSummary>` to `StageProjection`.\n- Ensure all new fields default cleanly for older persisted events/projections.\n- Export the new public types from `fabro-types`.\n\n- [ ] **Unit 2: Capture effective session tools**\n\n**Goal:** Provide an authoritative one-time source for the list that the model can actually call.\n\n**Files:**\n- Modify: `lib/crates/fabro-agent/src/session.rs`\n- Modify: `lib/crates/fabro-workflow/src/handler/llm/api.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/events.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/names.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/convert.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/stored_fields.rs` only if the new event needs non-standard stored fields.\n\n**Work:**\n- Add `Session::available_tools()` that returns the same effective `ToolDefinitionWithSource` list as `build_request()`.\n- Map `ToolDefinitionWithSource` to `AgentToolSummary` at the workflow boundary.\n- Populate `description` from `ToolDefinition.description`.\n- Populate `source` from `ToolSource`.\n- For MCP tools, include the server name from `ToolSource::Mcp` and derive `original_name` from the qualified exposed name using the existing MCP naming convention.\n- Populate `category` from the existing tool category mapping for known exposed names; use `other` when no category mapping applies.\n- Emit `agent.tools.available` once for the stage session after session setup and filtering are complete.\n\n- [ ] **Unit 3: Project available and invoked tools**\n\n**Goal:** Make `StageProjection.agent_tools` replay-authoritative.\n\n**Files:**\n- Modify: `lib/crates/fabro-store/src/run_state.rs`\n\n**Work:**\n- On `EventBody::AgentToolsAvailable`, replace the current stage visit's `agent_tools` with the event tools.\n- On `EventBody::AgentToolStarted`, find a matching `agent_tools` entry by exposed `tool_name` and set `invoked = true`.\n- Keep the existing MCP server `invoked` update unchanged.\n- If a legacy run has no availability event, do not synthesize a full list from permissions.\n\n- [ ] **Unit 4: Update OpenAPI and generated clients**\n\n**Goal:** Expose the new projection field and event contract through public API clients without duplicate Rust API types.\n\n**Files:**\n- Modify: `docs/public/api-reference/fabro-api.yaml`\n- Modify: `lib/crates/fabro-api/build.rs`\n- Modify: `lib/crates/fabro-api/src/lib.rs`\n- Modify generated files under `lib/crates/fabro-api/src/generated.rs` via `cargo build -p fabro-api`.\n- Modify generated files under `lib/packages/fabro-api-client/src` via TypeScript client generation.\n\n**Work:**\n- Add the OpenAPI schemas listed in the API Contract section.\n- Add `StageProjection.agent_tools`.\n- Add `fabro-api/build.rs` replacements for the new `fabro-types` types.\n- Regenerate Rust API code.\n- Regenerate TypeScript API client code.\n\n- [ ] **Unit 5: Render tools in the web sidebar**\n\n**Goal:** Replace the permission-derived sidebar display with the actual projected tool list.\n\n**Files:**\n- Modify: `apps/fabro-web/app/components/stage-insights-sidebar.tsx`\n- Modify: `apps/fabro-web/app/components/stage-insights-sidebar.test.tsx`\n\n**Work:**\n- Render `stage.agent_tools` when present.\n- Show each tool's name, description, source/category, and invoked state.\n- Keep permission level as secondary metadata or fallback for legacy stages with no `agent_tools`.\n- Do not infer tool availability from permission level.\n\n## Test Plan\n\n- `fabro-types`\n - JSON round-trip for `agent.tools.available`.\n - Serialization checks for `AgentToolSource` and `AgentToolCategory`.\n - Backward compatibility check that missing `agent_tools` deserializes to an empty list.\n\n- `fabro-workflow`\n - Event name and conversion tests for `agent.tools.available`.\n - Capture test proving native tools such as `apply_patch`, `grep`, and `glob` are emitted from the effective registry path.\n - MCP source mapping test for a qualified MCP tool name.\n\n- `fabro-store`\n - Projection test that `agent.tools.available` populates `StageProjection.agent_tools`.\n - Projection test that `agent.tool.started` marks only the matching tool as invoked.\n - Regression test that MCP server `invoked` status still updates as before.\n\n- `fabro-api`\n - Type identity/parity tests confirming API types reuse `fabro_types::AgentToolSummary`, `AgentToolSource`, `AgentToolCategory`, and `AgentToolsAvailableProps`.\n - StageProjection round-trip test including `agent_tools`.\n\n- `fabro-web`\n - Sidebar test rendering tool names and descriptions from `stage.agent_tools`.\n - Sidebar test showing invoked state.\n - Legacy fallback test for stages without `agent_tools`.\n\n## Run Checks\n\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo nextest run -p fabro-types -p fabro-workflow -p fabro-store -p fabro-api`\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cd apps/fabro-web && bun test && bun run typecheck`\n\n## Assumptions\n\n- The first implementation targets normal API-backed agent sessions, not ACP-native sessions.\n- `description` is safe to expose because it is already model-facing tool metadata, but parameter schemas remain out of scope for StageProjection.\n- `agent_tools` is a complete list only for runs that emit `agent.tools.available`; legacy runs return an empty list and may still show existing permission/MCP metadata.\n- If tool registration becomes mutable later, the event contract can be re-emitted and projection replacement semantics will still work.\n",
"graph.rankdir": "LR",
"internal.node_visit_count": 1,
"thread.toolchain.current_node": "preflight_compile",
"failure_signature": "",
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
"internal.retry_count.preflight_compile": 0,
"internal.retry_count.toolchain": 0,
"outcome": "succeeded"
},
"node_outcomes": {
"preflight_compile": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
},
"notes": "Script completed: cargo check -q --workspace 2>&1",
"usage": null
},
"toolchain": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
},
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
"usage": null
},
"preflight_lint": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
},
"notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
"usage": null
},
"start": {
"status": "succeeded",
"usage": null
}
},
"next_node_id": "implement",
"git_commit_sha": "3d3964a286091d3742bff02c99733cab95b24f91",
"node_visits": {
"toolchain": 1,
"preflight_compile": 1,
"preflight_lint": 1,
"start": 1
}
},
"diff": {
"summary": {
"files_changed": 0,
"additions": 0,
"deletions": 0
}
}
},
{
"seq": 976,
"checkpoint": {
"timestamp": "2026-05-24T17:40:37.848859Z",
"current_node": "implement",
"completed_nodes": [
"start",
"toolchain",
"preflight_compile",
"preflight_lint",
"implement"
],
"node_retries": {},
"context_values": {
"graph.goal": "---\ntitle: feat: StageProjection agent tools API\ntype: feat\nstatus: active\ndate: 2026-05-24\n---\n\n# feat: StageProjection Agent Tools API\n\n## Overview\n\nExpose the complete effective tool list for each agent-backed stage through `StageProjection`, so UI and API consumers can show actual tools such as `apply_patch`, `grep`, `glob`, `read_file`, MCP tools, skill tools, and subagent tools without inferring them from `permission_level`.\n\nThe API should expose tool summaries with `name`, `description`, `source`, `category`, and `invoked`. It must not expose full JSON parameter schemas in the run projection.\n\n## Problem Frame\n\nThe stage sidebar currently has permission metadata such as \"Full access\", but `permission_level` is only an access mode. It does not tell consumers which tools were actually exposed to the model after provider profile setup, optional tool registration, MCP integration, and tool access policy filtering.\n\nThe authoritative list already exists at request-build time in the agent session registry. The API needs to capture that effective list once per stage session and project it onto the stage.\n\n## Requirements Trace\n\n- R1. Add a StageProjection API field containing the complete effective tools for a stage session.\n- R2. Include `name`, `description`, `source`, `category`, and `invoked` for each tool.\n- R3. Do not infer tools from `permission_level` in backend or frontend code.\n- R4. Do not expose full tool parameter schemas through StageProjection.\n- R5. Preserve existing `permission_level` and `mcp_servers` fields for compatibility.\n- R6. Mark individual tools as invoked when matching `agent.tool.started` events are projected.\n- R7. Keep legacy runs backward compatible by defaulting missing tool lists to empty.\n\n## Scope Boundaries\n\n- Do not remove or rename `StageProjection.permission_level`.\n- Do not remove the existing MCP server projection or `AgentMcpToolSummary`.\n- Do not change completion API tool definitions.\n- Do not add ACP-native tool discovery in this pass unless an ACP path already has an equivalent effective tool list available.\n- Do not render parameter schemas in the web UI.\n\n## Context & Research\n\n### Relevant Code and Patterns\n\n- OpenAPI is the source of truth for HTTP contracts in `docs/public/api-reference/fabro-api.yaml`.\n- Shared API/projection DTOs should live in `fabro-types`, with `fabro-api/build.rs` replacements to avoid duplicate generated Rust types.\n- `StageProjection` lives in `lib/crates/fabro-types/src/run_projection.rs`.\n- Durable run event props live in `lib/crates/fabro-types/src/run_event/agent.rs` and `lib/crates/fabro-types/src/run_event/mod.rs`.\n- Workflow event conversion and event names live in `lib/crates/fabro-workflow/src/event/convert.rs`, `events.rs`, and `names.rs`.\n- Projection replay lives in `lib/crates/fabro-store/src/run_state.rs`.\n- The effective request tool list is built in `lib/crates/fabro-agent/src/session.rs` from `ToolRegistry::definitions_with_source_for_policy`.\n- Tool source metadata already exists as `ToolSource` and `ToolDefinitionWithSource` in `lib/crates/fabro-agent/src/tool_registry.rs`.\n- Tool category mapping already exists in `lib/crates/fabro-agent/src/tool_permissions.rs`.\n- The sidebar display lives in `apps/fabro-web/app/components/stage-insights-sidebar.tsx`.\n\n### Strategy Docs\n\n- Read `docs/internal/events-strategy.md` before adding the new durable event.\n- Read `docs/internal/testing-strategy.md` before adding or reorganizing tests.\n- Follow the OpenAPI type ownership guidance in `AGENTS.md`: reuse `fabro-types` through `fabro-api/build.rs` replacements when the API schema has the same product meaning and serde shape.\n\n## Key Technical Decisions\n\n- Add a new StageProjection field named `agent_tools`, not `tools`, to avoid ambiguity with MCP nested tools and completion tool definitions.\n- Add a dedicated durable event named `agent.tools.available` instead of overloading `agent.session.activated`.\n- Capture the effective tool list after session setup and filtering, using the same path as model request construction.\n- Store descriptions in the summary because they are useful API/UI metadata; omit parameter schemas to keep projection payloads small and avoid leaking full implementation detail.\n- Keep `AgentMcpToolSummary` MCP-only. Add a new general-purpose `AgentToolSummary` instead of stretching the MCP type beyond its meaning.\n- Treat `invoked` as projected state. The availability event should emit tools with `invoked: false`; replay of `agent.tool.started` flips matching tools to true.\n\n## API Contract\n\nAdd these schemas to OpenAPI and map them to `fabro_types` replacements:\n\n- `AgentToolSummary`\n - required: `name`, `description`, `source`, `category`, `invoked`\n - `name`: exposed tool name, e.g. `apply_patch` or `mcp__filesystem__read_file`\n - `description`: model-facing tool description\n - `source`: `AgentToolSource`\n - `category`: `AgentToolCategory`\n - `invoked`: boolean\n- `AgentToolSource`\n - tagged by `kind`\n - `native`\n - `mcp` with `server_name` and `original_name`\n - `skill`\n- `AgentToolCategory`\n - enum: `read`, `write`, `shell`, `subagent`, `other`\n- `AgentToolsAvailableProps`\n - required: `tools`, `visit`\n - `tools`: array of `AgentToolSummary`\n - `visit`: stage visit number\n\nAdd to `StageProjection`:\n\n- `agent_tools`: array of `AgentToolSummary`\n- Default to an empty array when omitted.\n- Skip serializing when empty, matching existing projection optional-list style.\n\nAdd event body:\n\n- Serialized event name: `agent.tools.available`\n- Event body type: `AgentToolsAvailableProps`\n\n## Implementation Units\n\n- [ ] **Unit 1: Add shared tool summary types**\n\n**Goal:** Define the canonical API/projection DTOs in `fabro-types`.\n\n**Files:**\n- Modify: `lib/crates/fabro-types/src/run_event/agent.rs`\n- Modify: `lib/crates/fabro-types/src/run_event/mod.rs`\n- Modify: `lib/crates/fabro-types/src/run_projection.rs`\n- Modify: `lib/crates/fabro-types/src/lib.rs`\n\n**Work:**\n- Add `AgentToolSummary`, `AgentToolSource`, `AgentToolCategory`, and `AgentToolsAvailableProps`.\n- Add `EventBody::AgentToolsAvailable` serialized as `agent.tools.available`.\n- Add `agent_tools: Vec<AgentToolSummary>` to `StageProjection`.\n- Ensure all new fields default cleanly for older persisted events/projections.\n- Export the new public types from `fabro-types`.\n\n- [ ] **Unit 2: Capture effective session tools**\n\n**Goal:** Provide an authoritative one-time source for the list that the model can actually call.\n\n**Files:**\n- Modify: `lib/crates/fabro-agent/src/session.rs`\n- Modify: `lib/crates/fabro-workflow/src/handler/llm/api.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/events.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/names.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/convert.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/stored_fields.rs` only if the new event needs non-standard stored fields.\n\n**Work:**\n- Add `Session::available_tools()` that returns the same effective `ToolDefinitionWithSource` list as `build_request()`.\n- Map `ToolDefinitionWithSource` to `AgentToolSummary` at the workflow boundary.\n- Populate `description` from `ToolDefinition.description`.\n- Populate `source` from `ToolSource`.\n- For MCP tools, include the server name from `ToolSource::Mcp` and derive `original_name` from the qualified exposed name using the existing MCP naming convention.\n- Populate `category` from the existing tool category mapping for known exposed names; use `other` when no category mapping applies.\n- Emit `agent.tools.available` once for the stage session after session setup and filtering are complete.\n\n- [ ] **Unit 3: Project available and invoked tools**\n\n**Goal:** Make `StageProjection.agent_tools` replay-authoritative.\n\n**Files:**\n- Modify: `lib/crates/fabro-store/src/run_state.rs`\n\n**Work:**\n- On `EventBody::AgentToolsAvailable`, replace the current stage visit's `agent_tools` with the event tools.\n- On `EventBody::AgentToolStarted`, find a matching `agent_tools` entry by exposed `tool_name` and set `invoked = true`.\n- Keep the existing MCP server `invoked` update unchanged.\n- If a legacy run has no availability event, do not synthesize a full list from permissions.\n\n- [ ] **Unit 4: Update OpenAPI and generated clients**\n\n**Goal:** Expose the new projection field and event contract through public API clients without duplicate Rust API types.\n\n**Files:**\n- Modify: `docs/public/api-reference/fabro-api.yaml`\n- Modify: `lib/crates/fabro-api/build.rs`\n- Modify: `lib/crates/fabro-api/src/lib.rs`\n- Modify generated files under `lib/crates/fabro-api/src/generated.rs` via `cargo build -p fabro-api`.\n- Modify generated files under `lib/packages/fabro-api-client/src` via TypeScript client generation.\n\n**Work:**\n- Add the OpenAPI schemas listed in the API Contract section.\n- Add `StageProjection.agent_tools`.\n- Add `fabro-api/build.rs` replacements for the new `fabro-types` types.\n- Regenerate Rust API code.\n- Regenerate TypeScript API client code.\n\n- [ ] **Unit 5: Render tools in the web sidebar**\n\n**Goal:** Replace the permission-derived sidebar display with the actual projected tool list.\n\n**Files:**\n- Modify: `apps/fabro-web/app/components/stage-insights-sidebar.tsx`\n- Modify: `apps/fabro-web/app/components/stage-insights-sidebar.test.tsx`\n\n**Work:**\n- Render `stage.agent_tools` when present.\n- Show each tool's name, description, source/category, and invoked state.\n- Keep permission level as secondary metadata or fallback for legacy stages with no `agent_tools`.\n- Do not infer tool availability from permission level.\n\n## Test Plan\n\n- `fabro-types`\n - JSON round-trip for `agent.tools.available`.\n - Serialization checks for `AgentToolSource` and `AgentToolCategory`.\n - Backward compatibility check that missing `agent_tools` deserializes to an empty list.\n\n- `fabro-workflow`\n - Event name and conversion tests for `agent.tools.available`.\n - Capture test proving native tools such as `apply_patch`, `grep`, and `glob` are emitted from the effective registry path.\n - MCP source mapping test for a qualified MCP tool name.\n\n- `fabro-store`\n - Projection test that `agent.tools.available` populates `StageProjection.agent_tools`.\n - Projection test that `agent.tool.started` marks only the matching tool as invoked.\n - Regression test that MCP server `invoked` status still updates as before.\n\n- `fabro-api`\n - Type identity/parity tests confirming API types reuse `fabro_types::AgentToolSummary`, `AgentToolSource`, `AgentToolCategory`, and `AgentToolsAvailableProps`.\n - StageProjection round-trip test including `agent_tools`.\n\n- `fabro-web`\n - Sidebar test rendering tool names and descriptions from `stage.agent_tools`.\n - Sidebar test showing invoked state.\n - Legacy fallback test for stages without `agent_tools`.\n\n## Run Checks\n\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo nextest run -p fabro-types -p fabro-workflow -p fabro-store -p fabro-api`\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cd apps/fabro-web && bun test && bun run typecheck`\n\n## Assumptions\n\n- The first implementation targets normal API-backed agent sessions, not ACP-native sessions.\n- `description` is safe to expose because it is already model-facing tool metadata, but parameter schemas remain out of scope for StageProjection.\n- `agent_tools` is a complete list only for runs that emit `agent.tools.available`; legacy runs return an empty list and may still show existing permission/MCP metadata.\n- If tool registration becomes mutable later, the event contract can be re-emitted and projection replacement semantics will still work.\n",
"internal.thread_id": "preflight_lint",
"internal.retry_count.start": 0,
"failure_signature": "",
"internal.retry_count.toolchain": 0,
"internal.work_dir": "/home/daytona/workspace/fabro",
"last_stage": "implement",
"internal.fidelity": "compact",
"failure_class": "",
"thread.start.current_node": "toolchain",
"current_node": "implement",
"graph.rankdir": "LR",
"internal.retry_count.preflight_compile": 0,
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
"internal.retry_count.implement": 0,
"internal.node_visit_count": 1,
"internal.retry_count.preflight_lint": 0,
"internal.run_id": "01KSDFKD9JJS7D2ZJ6JQYXV58Z",
"last_response": "The TS generator is pinned now, but this sandbox lacks Java, which OpenAPI Generator needs. Ill install a headless JRE and rerun generation.",
"thread.preflight_compile.current_node": "preflight_lint",
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"thread.preflight_lint.current_node": "implement",
"response.implement": "The TS generator is pinned now, but this sandbox lacks Java, which OpenAPI Generator needs. Ill install a headless JRE and rerun generation.",
"thread.toolchain.current_node": "preflight_compile",
"outcome": "succeeded"
},
"node_outcomes": {
"implement": {
"status": "succeeded",
"context_updates": {
"last_stage": "implement",
"last_response": "The TS generator is pinned now, but this sandbox lacks Java, which OpenAPI Generator needs. Ill install a headless JRE and rerun generation.",
"response.implement": "The TS generator is pinned now, but this sandbox lacks Java, which OpenAPI Generator needs. Ill install a headless JRE and rerun generation."
},
"notes": "Stage completed: implement",
"usage": {
"input": {
"usage": {
"model": {
"provider": "openai",
"model_id": "gpt-5.5"
},
"tokens": {
"input_tokens": 6711598,
"output_tokens": 22676,
"reasoning_tokens": 11522,
"cache_read_tokens": 16107008,
"cache_write_tokens": 0
}
},
"facts": {
"algorithm": "openai"
}
},
"total_usd_micros": 42637434
}
},
"toolchain": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
},
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
"usage": null
},
"preflight_compile": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
},
"notes": "Script completed: cargo check -q --workspace 2>&1",
"usage": null
},
"start": {
"status": "succeeded",
"usage": null
},
"preflight_lint": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
},
"notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
"usage": null
}
},
"next_node_id": "simplify_opus",
"git_commit_sha": "e93905f7589a03544e39e62df728ab414200e93c",
"node_visits": {
"preflight_compile": 1,
"preflight_lint": 1,
"start": 1,
"implement": 1,
"toolchain": 1
}
},
"diff": {
"patch": "diff --git a/apps/fabro-web/app/components/stage-insights-sidebar.test.tsx b/apps/fabro-web/app/components/stage-insights-sidebar.test.tsx\nindex c505a9ffd..713f1b4bc 100644\n--- a/apps/fabro-web/app/components/stage-insights-sidebar.test.tsx\n+++ b/apps/fabro-web/app/components/stage-insights-sidebar.test.tsx\n@@ -3,6 +3,7 @@ import TestRenderer, { act } from \"react-test-renderer\";\n import { MemoryRouter } from \"react-router\";\n \n import {\n+ AgentToolCategory,\n AgentSkillActivationSource,\n PermissionLevel,\n StageContextWindowCategory,\n@@ -66,7 +67,7 @@ function makeContextWindow(overrides: Partial<StageContextWindow> = {}): StageCo\n let restoreWindow: (() => void) | null = null;\n beforeAll(() => {\n const store = new Map<string, string>();\n- for (const key of [\"todos\", \"context\", \"skills\", \"mcps\"]) {\n+ for (const key of [\"todos\", \"context\", \"tools\", \"skills\", \"mcps\"]) {\n store.set(`fabro:stage-insights-section:${key}`, \"1\");\n }\n const stub = {\n@@ -157,6 +158,50 @@ describe(\"StageInsightsSidebar\", () => {\n expect(dom).toContain(\"Full access\");\n });\n \n+ test(\"renders projected agent tool names, descriptions, categories, and invoked state\", () => {\n+ const dom = render(\n+ makeStage({\n+ agent_tools: [\n+ {\n+ name: \"apply_patch\",\n+ description: \"Apply a unified diff patch\",\n+ source: { kind: \"native\" },\n+ category: AgentToolCategory.WRITE,\n+ invoked: true,\n+ },\n+ {\n+ name: \"grep\",\n+ description: \"Search file contents\",\n+ source: { kind: \"native\" },\n+ category: AgentToolCategory.READ,\n+ invoked: false,\n+ },\n+ ],\n+ permission_level: PermissionLevel.FULL,\n+ }),\n+ null,\n+ );\n+\n+ expect(dom).toContain(\"1/2\");\n+ expect(dom).toContain(\"apply_patch\");\n+ expect(dom).toContain(\"Apply a unified diff patch\");\n+ expect(dom).toContain(\"write\");\n+ expect(dom).toContain(\"used\");\n+ expect(dom).toContain(\"grep\");\n+ expect(dom).toContain(\"Search file contents\");\n+ expect(dom).toContain(\"read\");\n+ expect(dom).toContain(\"available\");\n+ // Permission remains secondary compatibility metadata, not the source of\n+ // the tool list.\n+ expect(dom).toContain(\"Full access\");\n+ });\n+\n+ test(\"legacy stages without agent tools keep permission fallback only\", () => {\n+ const dom = render(makeStage({ permission_level: PermissionLevel.READ_WRITE }), null);\n+ expect(dom).toContain(\"Read/write\");\n+ expect(dom).not.toContain(\"apply_patch\");\n+ });\n+\n test(\"renders mcp server used/total count, marks invoked servers as 'used'\", () => {\n const dom = render(\n makeStage({\ndiff --git a/apps/fabro-web/app/components/stage-insights-sidebar.tsx b/apps/fabro-web/app/components/stage-insights-sidebar.tsx\nindex 71ffa5094..135f1dd72 100644\n--- a/apps/fabro-web/app/components/stage-insights-sidebar.tsx\n+++ b/apps/fabro-web/app/components/stage-insights-sidebar.tsx\n@@ -19,6 +19,7 @@ import {\n PuzzlePieceIcon,\n ServerStackIcon,\n Squares2X2Icon,\n+ WrenchScrewdriverIcon,\n } from \"@heroicons/react/24/outline\";\n import {\n AgentSkillActivationSource,\n@@ -30,6 +31,7 @@ import {\n import type {\n ActivatedSkill,\n AgentSkillSummary,\n+ AgentToolSummary,\n McpServerProjection,\n StageContextWindow,\n StageContextWindowBreakdownItem,\n@@ -42,11 +44,12 @@ import { formatTokenCount } from \"../lib/format\";\n const COLLAPSED_STORAGE_KEY = \"fabro:stage-insights-sidebar-collapsed\";\n const SECTION_STORAGE_PREFIX = \"fabro:stage-insights-section:\";\n \n-type SectionKey = \"todos\" | \"context\" | \"skills\" | \"mcps\";\n+type SectionKey = \"todos\" | \"context\" | \"tools\" | \"skills\" | \"mcps\";\n \n const SECTIONS_DEFAULT_OPEN: Record<SectionKey, boolean> = {\n todos: true,\n context: false,\n+ tools: false,\n skills: false,\n mcps: false,\n };\n@@ -70,11 +73,13 @@ export function StageInsightsSidebar({ stage, contextWindow }: StageInsightsSide\n \n const todos = stage?.todos ?? null;\n const skills = stage?.skills ?? { activated: [], available: [] };\n+ const agentTools = stage?.agent_tools ?? [];\n const mcpServers = stage?.mcp_servers ?? [];\n const permission = stage?.permission_level ?? null;\n \n const todoStats = countTodoStats(todos);\n const activatedSkillNames = new Set(skills.activated.map((s) => s.name));\n+ const invokedToolCount = agentTools.filter((tool) => tool.invoked).length;\n \n return (\n <aside\n@@ -119,6 +124,18 @@ export function StageInsightsSidebar({ stage, contextWindow }: StageInsightsSide\n \n <ContextWindowSection collapsed={collapsed} snapshot={contextWindow ?? null} />\n \n+ <CollapsibleSection\n+ sectionKey=\"tools\"\n+ title=\"Tools\"\n+ icon={WrenchScrewdriverIcon}\n+ collapsed={collapsed}\n+ count={`${invokedToolCount}/${agentTools.length}`}\n+ empty={agentTools.length === 0}\n+ hideCountWhenCollapsed\n+ >\n+ <AgentToolsSection tools={agentTools} />\n+ </CollapsibleSection>\n+\n <CollapsibleSection\n sectionKey=\"skills\"\n title=\"Skills\"\n@@ -496,6 +513,61 @@ function SkillSourceIcon({ source }: { source: ActivatedSkill[\"source\"] }) {\n return <Icon className=\"size-3.5 shrink-0 text-fg-muted\" />;\n }\n \n+// ---------- Tools ----------\n+\n+function AgentToolsSection({ tools }: { tools: AgentToolSummary[] }) {\n+ if (tools.length === 0) return <p className=\"text-xs text-fg-muted\">No tools reported.</p>;\n+ return (\n+ <ul role=\"list\" className=\"space-y-2\">\n+ {tools.map((tool) => {\n+ const nameClass = tool.invoked\n+ ? \"min-w-0 flex-1 truncate text-xs text-fg-2\"\n+ : \"min-w-0 flex-1 truncate text-xs text-fg-muted\";\n+ return (\n+ <li key={tool.name} className=\"space-y-0.5\">\n+ <div className=\"flex items-center gap-1.5\">\n+ {tool.invoked ? (\n+ <CheckCircleIcon className=\"size-3.5 shrink-0 text-mint\" aria-label=\"Invoked\" />\n+ ) : (\n+ <ToolAvailableIcon className=\"size-3.5 shrink-0 text-fg-muted\" />\n+ )}\n+ <span className={nameClass}>{tool.name}</span>\n+ <span className=\"font-mono text-[10px] tabular-nums text-fg-muted\">\n+ {tool.invoked ? \"used\" : \"available\"}\n+ </span>\n+ </div>\n+ <p className=\"text-[11px] leading-snug text-fg-muted\">{tool.description}</p>\n+ <div className=\"flex flex-wrap gap-1\">\n+ <span className=\"rounded bg-overlay px-1 py-0.5 text-[10px] uppercase tracking-wider text-fg-muted\">\n+ {toolSourceLabel(tool.source)}\n+ </span>\n+ <span className=\"rounded bg-overlay px-1 py-0.5 text-[10px] uppercase tracking-wider text-fg-muted\">\n+ {tool.category}\n+ </span>\n+ </div>\n+ </li>\n+ );\n+ })}\n+ </ul>\n+ );\n+}\n+\n+function toolSourceLabel(source: AgentToolSummary[\"source\"]): string {\n+ switch (source.kind) {\n+ case \"mcp\":\n+ return `mcp:${source.server_name}`;\n+ case \"skill\":\n+ return \"skill\";\n+ case \"native\":\n+ default:\n+ return \"native\";\n+ }\n+}\n+\n+function ToolAvailableIcon({ className }: { className?: string }) {\n+ return <span className={`inline-block rounded-full border border-current ${className ?? \"\"}`} aria-hidden=\"true\" />;\n+}\n+\n // ---------- MCPs ----------\n \n function McpSection({ servers }: { servers: McpServerProjection[] }) {\ndiff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml\nindex 58a1e02e9..cc5eaa80b 100644\n--- a/docs/public/api-reference/fabro-api.yaml\n+++ b/docs/public/api-reference/fabro-api.yaml\n@@ -7971,6 +7971,22 @@ components:\n type: integer\n minimum: 1\n \n+ AgentToolsAvailableProps:\n+ description: Properties for the `agent.tools.available` event.\n+ type: object\n+ required:\n+ - tools\n+ - visit\n+ properties:\n+ tools:\n+ type: array\n+ description: Effective model-callable tools exposed to the stage session.\n+ items:\n+ $ref: \"#/components/schemas/AgentToolSummary\"\n+ visit:\n+ type: integer\n+ minimum: 1\n+\n RunSupersededByProps:\n description: Properties for the `run.superseded_by` audit event emitted on a rewound source run after archive succeeds.\n type: object\n@@ -8550,6 +8566,13 @@ components:\n - $ref: \"#/components/schemas/PermissionLevel\"\n - type: \"null\"\n description: Agent tool permission level applied to this stage session.\n+ agent_tools:\n+ type: array\n+ description: >\n+ Effective model-callable tools exposed to this agent stage session.\n+ Tool parameter schemas are intentionally omitted from this projection.\n+ items:\n+ $ref: \"#/components/schemas/AgentToolSummary\"\n mcp_servers:\n type: array\n description: MCP servers observed by this stage.\n@@ -8689,6 +8712,83 @@ components:\n type: string\n enum: [slash, tool]\n \n+ AgentToolSummary:\n+ description: Summary of one effective model-callable tool exposed to an agent stage.\n+ type: object\n+ required:\n+ - name\n+ - description\n+ - source\n+ - category\n+ - invoked\n+ properties:\n+ name:\n+ type: string\n+ description: Exposed model-facing tool name, for example `apply_patch` or `mcp__filesystem__read_file`.\n+ description:\n+ type: string\n+ description: Model-facing tool description.\n+ source:\n+ $ref: \"#/components/schemas/AgentToolSource\"\n+ category:\n+ $ref: \"#/components/schemas/AgentToolCategory\"\n+ invoked:\n+ type: boolean\n+ description: True once this tool has been invoked during the stage.\n+\n+ AgentToolSource:\n+ description: Origin of an effective agent tool.\n+ oneOf:\n+ - $ref: \"#/components/schemas/AgentToolSourceNative\"\n+ - $ref: \"#/components/schemas/AgentToolSourceMcp\"\n+ - $ref: \"#/components/schemas/AgentToolSourceSkill\"\n+ discriminator:\n+ propertyName: kind\n+ mapping:\n+ native: \"#/components/schemas/AgentToolSourceNative\"\n+ mcp: \"#/components/schemas/AgentToolSourceMcp\"\n+ skill: \"#/components/schemas/AgentToolSourceSkill\"\n+\n+ AgentToolSourceNative:\n+ type: object\n+ required:\n+ - kind\n+ properties:\n+ kind:\n+ type: string\n+ enum: [native]\n+\n+ AgentToolSourceMcp:\n+ type: object\n+ required:\n+ - kind\n+ - server_name\n+ - original_name\n+ properties:\n+ kind:\n+ type: string\n+ enum: [mcp]\n+ server_name:\n+ type: string\n+ description: MCP server name that provided the tool.\n+ original_name:\n+ type: string\n+ description: Tool name before MCP qualification.\n+\n+ AgentToolSourceSkill:\n+ type: object\n+ required:\n+ - kind\n+ properties:\n+ kind:\n+ type: string\n+ enum: [skill]\n+\n+ AgentToolCategory:\n+ description: Coarse tool category for display and grouping.\n+ type: string\n+ enum: [read, write, shell, subagent, other]\n+\n McpServerProjection:\n description: Projected state for one MCP server observed by an agent stage.\n type: object\ndiff --git a/lib/crates/fabro-agent/src/session.rs b/lib/crates/fabro-agent/src/session.rs\nindex 62ca5f048..8a8fcf189 100644\n--- a/lib/crates/fabro-agent/src/session.rs\n+++ b/lib/crates/fabro-agent/src/session.rs\n@@ -501,6 +501,11 @@ impl Session {\n self.config.permission_level\n }\n \n+ #[must_use]\n+ pub fn available_tools(&self) -> Vec<ToolDefinitionWithSource> {\n+ self.effective_tools()\n+ }\n+\n /// Initialize session by discovering project docs and capturing environment\n /// context. Call before `process_input`.\n ///\n@@ -1966,13 +1971,7 @@ impl Session {\n }\n messages.extend(self.history.convert_to_messages());\n \n- let tools_with_source = self\n- .provider_profile\n- .tool_registry()\n- .definitions_with_source_for_policy(\n- self.config.tool_access_policy.as_deref(),\n- self.config.tool_exposure_mode,\n- );\n+ let tools_with_source = self.effective_tools();\n let tools: Vec<_> = tools_with_source\n .iter()\n .map(|tool| tool.definition.clone())\n@@ -2009,13 +2008,11 @@ impl Session {\n }\n \n fn inject_task_reminder_if_needed(&mut self) {\n- let tools = self\n- .provider_profile\n- .tool_registry()\n- .definitions_for_policy(\n- self.config.tool_access_policy.as_deref(),\n- self.config.tool_exposure_mode,\n- );\n+ let tools: Vec<_> = self\n+ .effective_tools()\n+ .into_iter()\n+ .map(|tool| tool.definition)\n+ .collect();\n let tool_names: Vec<&str> = tools.iter().map(|tool| tool.name.as_str()).collect();\n if let Some(reminder) = task_reminder::maybe_reminder(&self.history, &tool_names) {\n self.history.push(Message::System {\n@@ -2024,6 +2021,15 @@ impl Session {\n });\n }\n }\n+\n+ fn effective_tools(&self) -> Vec<ToolDefinitionWithSource> {\n+ self.provider_profile\n+ .tool_registry()\n+ .definitions_with_source_for_policy(\n+ self.config.tool_access_policy.as_deref(),\n+ self.config.tool_exposure_mode,\n+ )\n+ }\n }\n \n const fn is_auth_error(err: &LlmError) -> bool {\n@@ -3274,6 +3280,38 @@ mod tests {\n assert_eq!(tools[0].name, \"read_file\");\n }\n \n+ #[tokio::test]\n+ async fn available_tools_uses_same_effective_registry_filter_as_requests() {\n+ let provider = Arc::new(CapturingLlmProvider::new());\n+ let client = make_client(provider as Arc<dyn ProviderAdapter>).await;\n+ let mut registry = ToolRegistry::new();\n+ registry.register(make_named_noop_tool(\"read_file\"));\n+ registry.register(make_named_noop_tool(\"apply_patch\"));\n+ registry.register(make_named_noop_tool(\"shell\"));\n+ let profile = Arc::new(TestProfile::with_tools(registry));\n+ let env = Arc::new(MockSandbox::default());\n+ let config = SessionOptions {\n+ tool_access_policy: Some(Arc::new(NamedToolAccessPolicy::new(vec![\n+ (\"read_file\", ToolAccess::Allowed),\n+ (\"apply_patch\", ToolAccess::RequiresApproval),\n+ (\"shell\", ToolAccess::Denied),\n+ ]))),\n+ tool_exposure_mode: ToolExposureMode::IncludeRequiresApproval,\n+ ..SessionOptions::default()\n+ };\n+ let session = Session::new(client, profile, env, config, None);\n+\n+ let tools = session.available_tools();\n+ let mut tool_names: Vec<&str> = tools\n+ .iter()\n+ .map(|tool| tool.definition.name.as_str())\n+ .collect();\n+ tool_names.sort_unstable();\n+\n+ assert_eq!(tool_names, vec![\"apply_patch\", \"read_file\"]);\n+ assert!(tools.iter().all(|tool| tool.source == ToolSource::Native));\n+ }\n+\n #[tokio::test]\n async fn request_exposes_approval_required_tools_when_mode_allows_them() {\n let provider = Arc::new(CapturingLlmProvider::new());\ndiff --git a/lib/crates/fabro-agent/src/tool_permissions.rs b/lib/crates/fabro-agent/src/tool_permissions.rs\nindex 3a62931f1..b22dba945 100644\n--- a/lib/crates/fabro-agent/src/tool_permissions.rs\n+++ b/lib/crates/fabro-agent/src/tool_permissions.rs\n@@ -1,11 +1,16 @@\n use fabro_types::PermissionLevel;\n \n pub fn tool_category(name: &str) -> &'static str {\n+ known_tool_category(name).unwrap_or(\"shell\")\n+}\n+\n+pub fn known_tool_category(name: &str) -> Option<&'static str> {\n match name {\n- \"read_file\" | \"read_many_files\" | \"grep\" | \"glob\" | \"list_dir\" => \"read\",\n- \"write_file\" | \"edit_file\" | \"apply_patch\" => \"write\",\n- \"spawn_agent\" | \"send_input\" | \"wait\" | \"close_agent\" => \"subagent\",\n- _ => \"shell\",\n+ \"read_file\" | \"read_many_files\" | \"grep\" | \"glob\" | \"list_dir\" => Some(\"read\"),\n+ \"write_file\" | \"edit_file\" | \"apply_patch\" => Some(\"write\"),\n+ \"shell\" => Some(\"shell\"),\n+ \"spawn_agent\" | \"send_input\" | \"wait\" | \"close_agent\" => Some(\"subagent\"),\n+ _ => None,\n }\n }\n \ndiff --git a/lib/crates/fabro-api/build.rs b/lib/crates/fabro-api/build.rs\nindex 4649ef19e..2a38da779 100644\n--- a/lib/crates/fabro-api/build.rs\n+++ b/lib/crates/fabro-api/build.rs\n@@ -366,6 +366,14 @@ fn main() {\n \"fabro_types::AgentSkillActivationSource\",\n &[],\n ),\n+ (\"AgentToolSummary\", \"fabro_types::AgentToolSummary\", &[]),\n+ (\"AgentToolSource\", \"fabro_types::AgentToolSource\", &[]),\n+ (\"AgentToolCategory\", \"fabro_types::AgentToolCategory\", &[]),\n+ (\n+ \"AgentToolsAvailableProps\",\n+ \"fabro_types::AgentToolsAvailableProps\",\n+ &[],\n+ ),\n (\n \"McpServerProjection\",\n \"fabro_types::McpServerProjection\",\ndiff --git a/lib/crates/fabro-api/src/lib.rs b/lib/crates/fabro-api/src/lib.rs\nindex a148d9aac..d7ab6f416 100644\n--- a/lib/crates/fabro-api/src/lib.rs\n+++ b/lib/crates/fabro-api/src/lib.rs\n@@ -34,8 +34,9 @@ pub mod types {\n };\n pub use fabro_types::{\n ActivatedSkill, AgentMcpToolSummary, AgentSkillActivationSource, AgentSkillSummary,\n- AskFabro, AuthMethod, BilledTokenCounts, CommandTermination, Conclusion, DiffStats,\n- DiffSummary, DirtyStatus, EventEnvelope, ExecOutputTail, FailureCategory, FailureDetail,\n+ AgentToolCategory, AgentToolSource, AgentToolSummary, AgentToolsAvailableProps, AskFabro,\n+ AuthMethod, BilledTokenCounts, CommandTermination, Conclusion, DiffStats, DiffSummary,\n+ DirtyStatus, EventEnvelope, ExecOutputTail, FailureCategory, FailureDetail,\n FailureSignature, GitContext, IdpIdentity, InterviewOption, InterviewQuestionRecord,\n McpServerProjection, McpServerStatus, PairId, PairMessageId, PairMessageRecord,\n PairMessageRequest, PairRecord, PairStartRequest, PairStatus, PairTarget,\ndiff --git a/lib/crates/fabro-api/tests/stage_projection_round_trip.rs b/lib/crates/fabro-api/tests/stage_projection_round_trip.rs\nindex 8c110b5b4..2f26ad612 100644\n--- a/lib/crates/fabro-api/tests/stage_projection_round_trip.rs\n+++ b/lib/crates/fabro-api/tests/stage_projection_round_trip.rs\n@@ -3,9 +3,12 @@ use std::any::{TypeId, type_name};\n use fabro_api::types::{\n ActivatedSkill as ApiActivatedSkill, AgentMcpToolSummary as ApiAgentMcpToolSummary,\n AgentSkillActivationSource as ApiAgentSkillActivationSource,\n- AgentSkillSummary as ApiAgentSkillSummary, McpServerProjection as ApiMcpServerProjection,\n- McpServerStatus as ApiMcpServerStatus, PermissionLevel as ApiPermissionLevel,\n- SkillsProjection as ApiSkillsProjection, StageContextWindow as ApiStageContextWindow,\n+ AgentSkillSummary as ApiAgentSkillSummary, AgentToolCategory as ApiAgentToolCategory,\n+ AgentToolSource as ApiAgentToolSource, AgentToolSummary as ApiAgentToolSummary,\n+ AgentToolsAvailableProps as ApiAgentToolsAvailableProps,\n+ McpServerProjection as ApiMcpServerProjection, McpServerStatus as ApiMcpServerStatus,\n+ PermissionLevel as ApiPermissionLevel, SkillsProjection as ApiSkillsProjection,\n+ StageContextWindow as ApiStageContextWindow,\n StageContextWindowBreakdownItem as ApiStageContextWindowBreakdownItem,\n StageContextWindowCategory as ApiStageContextWindowCategory,\n StageContextWindowCountMethod as ApiStageContextWindowCountMethod,\n@@ -18,6 +21,7 @@ use fabro_api::types::{\n };\n use fabro_types::{\n ActivatedSkill, AgentMcpToolSummary, AgentSkillActivationSource, AgentSkillSummary,\n+ AgentToolCategory, AgentToolSource, AgentToolSummary, AgentToolsAvailableProps,\n McpServerProjection, McpServerStatus, PermissionLevel, SkillsProjection, StageContextWindow,\n StageContextWindowBreakdownItem, StageContextWindowCategory, StageContextWindowCountMethod,\n StageContextWindowProjection, StageContextWindowStaleness, StageContextWindowUnavailableReason,\n@@ -40,6 +44,10 @@ fn stage_projection_reuses_nested_agent_state_types() {\n assert_same_type::<ApiActivatedSkill, ActivatedSkill>();\n assert_same_type::<ApiAgentSkillSummary, AgentSkillSummary>();\n assert_same_type::<ApiAgentSkillActivationSource, AgentSkillActivationSource>();\n+ assert_same_type::<ApiAgentToolSummary, AgentToolSummary>();\n+ assert_same_type::<ApiAgentToolSource, AgentToolSource>();\n+ assert_same_type::<ApiAgentToolCategory, AgentToolCategory>();\n+ assert_same_type::<ApiAgentToolsAvailableProps, AgentToolsAvailableProps>();\n assert_same_type::<ApiMcpServerProjection, McpServerProjection>();\n assert_same_type::<ApiMcpServerStatus, McpServerStatus>();\n assert_same_type::<ApiAgentMcpToolSummary, AgentMcpToolSummary>();\n@@ -135,6 +143,26 @@ fn stage_projection_round_trips_representative_json() {\n ]\n },\n \"permission_level\": \"read-only\",\n+ \"agent_tools\": [\n+ {\n+ \"name\": \"apply_patch\",\n+ \"description\": \"Apply a unified diff patch\",\n+ \"source\": { \"kind\": \"native\" },\n+ \"category\": \"write\",\n+ \"invoked\": true\n+ },\n+ {\n+ \"name\": \"mcp__filesystem__read_file\",\n+ \"description\": \"Read a file through MCP\",\n+ \"source\": {\n+ \"kind\": \"mcp\",\n+ \"server_name\": \"filesystem\",\n+ \"original_name\": \"read_file\"\n+ },\n+ \"category\": \"other\",\n+ \"invoked\": false\n+ }\n+ ],\n \"mcp_servers\": [\n {\n \"server_name\": \"filesystem\",\n@@ -355,6 +383,47 @@ fn nested_agent_state_types_match_openapi_json_shape() {\n assert_eq!(mcp_server.tool_count, 1);\n }\n \n+#[test]\n+fn agent_tool_summary_matches_openapi_json_shape_without_parameter_schema() {\n+ let tool = AgentToolSummary {\n+ name: \"mcp__filesystem__read_file\".to_string(),\n+ description: \"Read a file through MCP\".to_string(),\n+ source: AgentToolSource::Mcp {\n+ server_name: \"filesystem\".to_string(),\n+ original_name: \"read_file\".to_string(),\n+ },\n+ category: AgentToolCategory::Other,\n+ invoked: false,\n+ };\n+\n+ let tool_json = serde_json::to_value(&tool).unwrap();\n+ assert_eq!(\n+ tool_json,\n+ json!({\n+ \"name\": \"mcp__filesystem__read_file\",\n+ \"description\": \"Read a file through MCP\",\n+ \"source\": {\n+ \"kind\": \"mcp\",\n+ \"server_name\": \"filesystem\",\n+ \"original_name\": \"read_file\"\n+ },\n+ \"category\": \"other\",\n+ \"invoked\": false\n+ })\n+ );\n+ assert!(tool_json.as_object().unwrap().get(\"parameters\").is_none());\n+ let api_tool: ApiAgentToolSummary = serde_json::from_value(tool_json).unwrap();\n+ assert_eq!(api_tool, tool);\n+\n+ let props = AgentToolsAvailableProps {\n+ tools: vec![tool],\n+ visit: 2,\n+ };\n+ let props_json = serde_json::to_value(&props).unwrap();\n+ let api_props: ApiAgentToolsAvailableProps = serde_json::from_value(props_json).unwrap();\n+ assert_eq!(api_props, props);\n+}\n+\n fn assert_same_type<T: 'static, U: 'static>() {\n assert_eq!(\n TypeId::of::<T>(),\ndiff --git a/lib/crates/fabro-store/src/run_state.rs b/lib/crates/fabro-store/src/run_state.rs\nindex d5854291e..7bd2e8a3e 100644\n--- a/lib/crates/fabro-store/src/run_state.rs\n+++ b/lib/crates/fabro-store/src/run_state.rs\n@@ -411,6 +411,13 @@ impl RunProjectionReducer for RunProjection {\n stage.provider_used = Some(StageModelUsage::from_agent_session_activated(props));\n stage.permission_level = props.permission_level;\n }\n+ EventBody::AgentToolsAvailable(props) => {\n+ let Some(stage) = stage_at_stored_or_visit(self, stored, props.visit, event.seq)\n+ else {\n+ return Ok(());\n+ };\n+ stage.agent_tools.clone_from(&props.tools);\n+ }\n // `AgentAcpStarted` is the start-of-process signal for an external\n // ACP agent. `provider_used` is intentionally sourced from the\n // subsequent `AgentSessionActivated` event, which carries the\n@@ -605,6 +612,13 @@ impl RunProjectionReducer for RunProjection {\n else {\n return Ok(());\n };\n+ if let Some(tool) = stage\n+ .agent_tools\n+ .iter_mut()\n+ .find(|tool| tool.name == props.tool_name)\n+ {\n+ tool.invoked = true;\n+ }\n if let Some(server) = mcp_server_from_tool_name(&props.tool_name) {\n if let Some(projection) = stage\n .mcp_servers\n@@ -1232,9 +1246,11 @@ mod tests {\n AgentSessionEndedProps, AgentSessionStartedProps, AgentSkillActivatedProps,\n AgentSkillActivationSource, AgentSkillSummary, AgentSkillsDiscoveredProps,\n AgentSubClosedProps, AgentSubCompletedProps, AgentSubFailedProps, AgentSubSpawnedProps,\n- AgentToolStartedProps, CheckpointCompletedProps, InterviewCompletedProps, InterviewOption,\n- InterviewStartedProps, RunCompletedProps, RunControlEffectProps, StageCompletedProps,\n- StageFailedProps, StagePromptProps, StageRetryingProps, StageStartedProps,\n+ AgentToolCategory, AgentToolSource, AgentToolStartedProps, AgentToolSummary,\n+ AgentToolsAvailableProps, CheckpointCompletedProps, InterviewCompletedProps,\n+ InterviewOption, InterviewStartedProps, RunCompletedProps, RunControlEffectProps,\n+ StageCompletedProps, StageFailedProps, StagePromptProps, StageRetryingProps,\n+ StageStartedProps,\n };\n use fabro_types::{\n AgentBackend, BilledModelUsage, BilledTokenCounts, BlockedReason, Checkpoint,\n@@ -4599,6 +4615,117 @@ mod tests {\n assert_eq!(legacy_stage.permission_level, None);\n }\n \n+ fn agent_tool(name: &str, category: AgentToolCategory, invoked: bool) -> AgentToolSummary {\n+ AgentToolSummary {\n+ name: name.to_string(),\n+ description: format!(\"{name} description\"),\n+ source: AgentToolSource::Native,\n+ category,\n+ invoked,\n+ }\n+ }\n+\n+ #[test]\n+ fn agent_tools_available_replaces_stage_agent_tools() {\n+ let mut state = initialized_projection();\n+ let stage_id = stage_id();\n+\n+ state\n+ .apply_event(&test_stage_event(\n+ 1,\n+ EventBody::AgentToolsAvailable(AgentToolsAvailableProps {\n+ tools: vec![\n+ agent_tool(\"read_file\", AgentToolCategory::Read, false),\n+ agent_tool(\"apply_patch\", AgentToolCategory::Write, false),\n+ ],\n+ visit: 1,\n+ }),\n+ stage_id.clone(),\n+ ))\n+ .unwrap();\n+ state\n+ .apply_event(&test_stage_event(\n+ 2,\n+ EventBody::AgentToolsAvailable(AgentToolsAvailableProps {\n+ tools: vec![agent_tool(\"grep\", AgentToolCategory::Read, false)],\n+ visit: 1,\n+ }),\n+ stage_id.clone(),\n+ ))\n+ .unwrap();\n+\n+ let stage = state.stage(&stage_id).unwrap();\n+ assert_eq!(stage.agent_tools, vec![agent_tool(\n+ \"grep\",\n+ AgentToolCategory::Read,\n+ false\n+ )]);\n+ }\n+\n+ #[test]\n+ fn agent_tool_started_marks_only_matching_available_tool_invoked() {\n+ let mut state = initialized_projection();\n+ let stage_id = stage_id();\n+\n+ state\n+ .apply_event(&test_stage_event(\n+ 1,\n+ EventBody::AgentToolsAvailable(AgentToolsAvailableProps {\n+ tools: vec![\n+ agent_tool(\"read_file\", AgentToolCategory::Read, false),\n+ agent_tool(\"apply_patch\", AgentToolCategory::Write, false),\n+ ],\n+ visit: 1,\n+ }),\n+ stage_id.clone(),\n+ ))\n+ .unwrap();\n+ state\n+ .apply_event(&test_stage_event(\n+ 2,\n+ EventBody::AgentToolStarted(AgentToolStartedProps {\n+ tool_name: \"apply_patch\".to_string(),\n+ tool_call_id: \"call_patch\".to_string(),\n+ arguments: serde_json::json!({}),\n+ visit: 1,\n+ tool_call: None,\n+ turn_id: None,\n+ parent_message_id: None,\n+ }),\n+ stage_id.clone(),\n+ ))\n+ .unwrap();\n+\n+ let stage = state.stage(&stage_id).unwrap();\n+ assert!(!stage.agent_tools[0].invoked);\n+ assert!(stage.agent_tools[1].invoked);\n+ }\n+\n+ #[test]\n+ fn legacy_tool_started_without_available_tools_does_not_synthesize_tool_list() {\n+ let mut state = initialized_projection();\n+ let stage_id = stage_id();\n+\n+ state\n+ .apply_event(&test_stage_event(\n+ 1,\n+ EventBody::AgentToolStarted(AgentToolStartedProps {\n+ tool_name: \"apply_patch\".to_string(),\n+ tool_call_id: \"call_patch\".to_string(),\n+ arguments: serde_json::json!({}),\n+ visit: 1,\n+ tool_call: None,\n+ turn_id: None,\n+ parent_message_id: None,\n+ }),\n+ stage_id.clone(),\n+ ))\n+ .unwrap();\n+\n+ let stage = state.stage(&stage_id).unwrap();\n+ assert!(stage.agent_tools.is_empty());\n+ }\n+\n #[test]\n fn mcp_server_events_update_stage_projection() {\n let mut state = initialized_projection();\ndiff --git a/lib/crates/fabro-types/src/lib.rs b/lib/crates/fabro-types/src/lib.rs\nindex 622e8729f..d2bbdfbcd 100644\n--- a/lib/crates/fabro-types/src/lib.rs\n+++ b/lib/crates/fabro-types/src/lib.rs\n@@ -96,9 +96,10 @@ pub use run::{\n pub use run_blob_id::RunBlobId;\n pub use run_event::{\n AgentMcpToolSummary, AgentMemoryFileProps, AgentSkillActivationSource, AgentSkillSummary,\n- EventBody, ExecOutputTail, InterviewOption, MetadataSnapshotFailureKind, MetadataSnapshotPhase,\n- RunEvent, RunNoticeCode, RunNoticeLevel, RunPairEndedReason, RunPairFailedReason,\n- RunRunnableSource, SessionCapability, TodoCreatedProps, TodoDeletedProps, TodoUpdatedProps,\n+ AgentToolCategory, AgentToolSource, AgentToolSummary, AgentToolsAvailableProps, EventBody,\n+ ExecOutputTail, InterviewOption, MetadataSnapshotFailureKind, MetadataSnapshotPhase, RunEvent,\n+ RunNoticeCode, RunNoticeLevel, RunPairEndedReason, RunPairFailedReason, RunRunnableSource,\n+ SessionCapability, TodoCreatedProps, TodoDeletedProps, TodoUpdatedProps,\n };\n pub use run_failure::RunFailure;\n pub use run_id::{RunId, fixtures};\ndiff --git a/lib/crates/fabro-types/src/run_event/agent.rs b/lib/crates/fabro-types/src/run_event/agent.rs\nindex d172b2c9e..cbd994690 100644\n--- a/lib/crates/fabro-types/src/run_event/agent.rs\n+++ b/lib/crates/fabro-types/src/run_event/agent.rs\n@@ -1,6 +1,7 @@\n use fabro_model::{ReasoningEffort, Speed};\n use serde::{Deserialize, Serialize};\n use serde_json::Value;\n+use strum::{Display, EnumString, IntoStaticStr};\n \n use super::BilledTokenCounts;\n use crate::transcript::{ToolCall, ToolResult, TranscriptMessage};\n@@ -53,6 +54,56 @@ pub struct AgentSessionDeactivatedProps {\n pub visit: u32,\n }\n \n+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n+pub struct AgentToolsAvailableProps {\n+ #[serde(default)]\n+ pub tools: Vec<AgentToolSummary>,\n+ pub visit: u32,\n+}\n+\n+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n+pub struct AgentToolSummary {\n+ pub name: String,\n+ pub description: String,\n+ pub source: AgentToolSource,\n+ pub category: AgentToolCategory,\n+ pub invoked: bool,\n+}\n+\n+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n+#[serde(tag = \"kind\", rename_all = \"snake_case\")]\n+pub enum AgentToolSource {\n+ Native,\n+ Mcp {\n+ server_name: String,\n+ original_name: String,\n+ },\n+ Skill,\n+}\n+\n+#[derive(\n+ Debug,\n+ Clone,\n+ Copy,\n+ PartialEq,\n+ Eq,\n+ Hash,\n+ Serialize,\n+ Deserialize,\n+ Display,\n+ EnumString,\n+ IntoStaticStr,\n+)]\n+#[serde(rename_all = \"snake_case\")]\n+#[strum(serialize_all = \"snake_case\")]\n+pub enum AgentToolCategory {\n+ Read,\n+ Write,\n+ Shell,\n+ Subagent,\n+ Other,\n+}\n+\n #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\n pub struct AgentProcessingEndProps {\n pub visit: u32,\ndiff --git a/lib/crates/fabro-types/src/run_event/mod.rs b/lib/crates/fabro-types/src/run_event/mod.rs\nindex 84a58ef16..4b35947f2 100644\n--- a/lib/crates/fabro-types/src/run_event/mod.rs\n+++ b/lib/crates/fabro-types/src/run_event/mod.rs\n@@ -198,6 +198,8 @@ pub enum EventBody {\n AgentSessionStarted(AgentSessionStartedProps),\n #[serde(rename = \"agent.session.activated\")]\n AgentSessionActivated(AgentSessionActivatedProps),\n+ #[serde(rename = \"agent.tools.available\")]\n+ AgentToolsAvailable(AgentToolsAvailableProps),\n #[serde(rename = \"agent.session.deactivated\")]\n AgentSessionDeactivated(AgentSessionDeactivatedProps),\n #[serde(rename = \"agent.session.ended\")]\n@@ -500,6 +502,7 @@ impl EventBody {\n Self::PromptCompleted(_) => \"prompt.completed\",\n Self::AgentSessionStarted(_) => \"agent.session.started\",\n Self::AgentSessionActivated(_) => \"agent.session.activated\",\n+ Self::AgentToolsAvailable(_) => \"agent.tools.available\",\n Self::AgentSessionDeactivated(_) => \"agent.session.deactivated\",\n Self::AgentSessionEnded(_) => \"agent.session.ended\",\n Self::AgentProcessingEnd(_) => \"agent.processing.end\",\n@@ -682,6 +685,7 @@ fn is_known_event_name(event: &str) -> bool {\n | \"prompt.completed\"\n | \"agent.session.started\"\n | \"agent.session.activated\"\n+ | \"agent.tools.available\"\n | \"agent.session.deactivated\"\n | \"agent.session.ended\"\n | \"agent.processing.end\"\n@@ -2261,4 +2265,76 @@ mod tests {\n \"empty tools should be omitted for legacy parity\"\n );\n }\n+\n+ #[test]\n+ fn agent_tools_available_round_trips_without_parameter_schemas() {\n+ let body = EventBody::AgentToolsAvailable(AgentToolsAvailableProps {\n+ tools: vec![\n+ AgentToolSummary {\n+ name: \"apply_patch\".to_string(),\n+ description: \"Apply a unified diff patch\".to_string(),\n+ source: AgentToolSource::Native,\n+ category: AgentToolCategory::Write,\n+ invoked: false,\n+ },\n+ AgentToolSummary {\n+ name: \"mcp__filesystem__read_file\".to_string(),\n+ description: \"Read a file through the filesystem MCP server\".to_string(),\n+ source: AgentToolSource::Mcp {\n+ server_name: \"filesystem\".to_string(),\n+ original_name: \"read_file\".to_string(),\n+ },\n+ category: AgentToolCategory::Other,\n+ invoked: false,\n+ },\n+ ],\n+ visit: 1,\n+ });\n+\n+ let value = serde_json::to_value(&body).unwrap();\n+ assert_eq!(value[\"event\"], \"agent.tools.available\");\n+ assert_eq!(value[\"properties\"][\"visit\"], 1);\n+ assert_eq!(value[\"properties\"][\"tools\"][0][\"name\"], \"apply_patch\");\n+ assert_eq!(value[\"properties\"][\"tools\"][0][\"source\"][\"kind\"], \"native\");\n+ assert_eq!(value[\"properties\"][\"tools\"][0][\"category\"], \"write\");\n+ assert!(\n+ value[\"properties\"][\"tools\"][0]\n+ .as_object()\n+ .unwrap()\n+ .get(\"parameters\")\n+ .is_none(),\n+ \"StageProjection tool summaries must not expose full parameter schemas\"\n+ );\n+\n+ let parsed: EventBody = serde_json::from_value(value).unwrap();\n+ assert_eq!(parsed, body);\n+ }\n+\n+ #[test]\n+ fn agent_tool_source_and_category_use_public_json_shape() {\n+ assert_eq!(\n+ serde_json::to_value(AgentToolCategory::Read).unwrap(),\n+ json!(\"read\")\n+ );\n+ assert_eq!(\n+ serde_json::to_value(AgentToolCategory::Subagent).unwrap(),\n+ json!(\"subagent\")\n+ );\n+ assert_eq!(\n+ serde_json::to_value(AgentToolSource::Skill).unwrap(),\n+ json!({ \"kind\": \"skill\" })\n+ );\n+ assert_eq!(\n+ serde_json::to_value(AgentToolSource::Mcp {\n+ server_name: \"github\".to_string(),\n+ original_name: \"create_issue\".to_string(),\n+ })\n+ .unwrap(),\n+ json!({\n+ \"kind\": \"mcp\",\n+ \"server_name\": \"github\",\n+ \"original_name\": \"create_issue\"\n+ })\n+ );\n+ }\n }\ndiff --git a/lib/crates/fabro-types/src/run_projection.rs b/lib/crates/fabro-types/src/run_projection.rs\nindex fca145920..3bba6d43e 100644\n--- a/lib/crates/fabro-types/src/run_projection.rs\n+++ b/lib/crates/fabro-types/src/run_projection.rs\n@@ -9,10 +9,10 @@ use strum::{Display, EnumString, IntoStaticStr};\n use crate::run_event::{AgentSessionActivatedProps, StagePromptProps};\n use crate::{\n AgentBackend, AgentMcpToolSummary, AgentSkillActivationSource, AgentSkillSummary,\n- BilledTokenCounts, Checkpoint, Conclusion, InterviewQuestionRecord, InvalidTransition,\n- ModelRef, PermissionLevel, PullRequestLink, RunApproval, RunControlAction, RunDiff, RunId,\n- RunSandbox, RunSpec, RunStatus, RunTiming, StageCompletion, StageHandler, StageId, StageState,\n- StageTiming, StartRecord, TodoListProjection,\n+ AgentToolSummary, BilledTokenCounts, Checkpoint, Conclusion, InterviewQuestionRecord,\n+ InvalidTransition, ModelRef, PermissionLevel, PullRequestLink, RunApproval, RunControlAction,\n+ RunDiff, RunId, RunSandbox, RunSpec, RunStatus, RunTiming, StageCompletion, StageHandler,\n+ StageId, StageState, StageTiming, StartRecord, TodoListProjection,\n };\n \n #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\n@@ -360,6 +360,8 @@ pub struct StageProjection {\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub permission_level: Option<PermissionLevel>,\n #[serde(default, skip_serializing_if = \"Vec::is_empty\")]\n+ pub agent_tools: Vec<AgentToolSummary>,\n+ #[serde(default, skip_serializing_if = \"Vec::is_empty\")]\n pub mcp_servers: Vec<McpServerProjection>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub context_window: Option<StageContextWindowProjection>,\n@@ -440,6 +442,7 @@ impl StageProjection {\n subagents: Vec::new(),\n skills: SkillsProjection::default(),\n permission_level: None,\n+ agent_tools: Vec::new(),\n mcp_servers: Vec::new(),\n context_window: None,\n provider_used: None,\n@@ -744,9 +747,10 @@ mod iter_stages_tests {\n use std::num::NonZeroU32;\n \n use chrono::Utc;\n+ use serde_json::json;\n \n use super::RunProjection;\n- use crate::{Graph, RunId, RunSpec, WorkflowSettings};\n+ use crate::{Graph, RunId, RunSpec, StageProjection, WorkflowSettings};\n \n fn seq(n: u32) -> NonZeroU32 {\n NonZeroU32::new(n).unwrap()\n@@ -817,6 +821,23 @@ mod iter_stages_tests {\n assert_eq!(order, vec![\"a\", \"b\", \"c\"]);\n }\n \n+ #[test]\n+ fn stage_projection_defaults_missing_agent_tools_to_empty_and_omits_empty_list() {\n+ let value = json!({\n+ \"first_event_seq\": 1,\n+ \"state\": \"running\"\n+ });\n+\n+ let stage: StageProjection = serde_json::from_value(value).unwrap();\n+ assert!(stage.agent_tools.is_empty());\n+\n+ let serialized = serde_json::to_value(stage).unwrap();\n+ assert!(\n+ serialized.as_object().unwrap().get(\"agent_tools\").is_none(),\n+ \"empty agent_tools should be omitted from StageProjection JSON\"\n+ );\n+ }\n+\n #[test]\n fn iter_stages_tie_breaks_same_first_event_seq_by_stage_id() {\n for _ in 0..128 {\ndiff --git a/lib/crates/fabro-workflow/src/event/convert.rs b/lib/crates/fabro-workflow/src/event/convert.rs\nindex ff098e6f0..de4998a5f 100644\n--- a/lib/crates/fabro-workflow/src/event/convert.rs\n+++ b/lib/crates/fabro-workflow/src/event/convert.rs\n@@ -1174,6 +1174,12 @@ fn event_body_from_event(event: &Event) -> EventBody {\n capabilities: capabilities.clone(),\n visit: *visit,\n }),\n+ Event::AgentToolsAvailable { tools, visit, .. } => {\n+ EventBody::AgentToolsAvailable(fabro_types::AgentToolsAvailableProps {\n+ tools: tools.clone(),\n+ visit: *visit,\n+ })\n+ }\n Event::AgentSessionDeactivated { visit, .. } => {\n EventBody::AgentSessionDeactivated(fabro_types::AgentSessionDeactivatedProps {\n visit: *visit,\n@@ -1594,6 +1600,31 @@ mod tests {\n assert_eq!(properties[\"visit\"], 2);\n }\n \n+ #[test]\n+ fn run_event_agent_tools_available_moves_session_and_stage_metadata_to_header() {\n+ let stored = to_run_event(&fixtures::RUN_4, &Event::AgentToolsAvailable {\n+ node_id: \"code\".to_string(),\n+ visit: 2,\n+ session_id: \"ses_root\".to_string(),\n+ tools: vec![::fabro_types::AgentToolSummary {\n+ name: \"apply_patch\".to_string(),\n+ description: \"Apply a unified diff patch\".to_string(),\n+ source: ::fabro_types::AgentToolSource::Native,\n+ category: ::fabro_types::AgentToolCategory::Write,\n+ invoked: false,\n+ }],\n+ });\n+\n+ assert_eq!(stored.event_name(), \"agent.tools.available\");\n+ assert_eq!(stored.node_id.as_deref(), Some(\"code\"));\n+ assert_eq!(stored.stage_id, Some(StageId::new(\"code\", 2)));\n+ assert_eq!(stored.session_id.as_deref(), Some(\"ses_root\"));\n+ let properties = stored.properties().unwrap();\n+ assert_eq!(properties[\"visit\"], 2);\n+ assert_eq!(properties[\"tools\"][0][\"name\"], \"apply_patch\");\n+ assert_eq!(properties[\"tools\"][0][\"category\"], \"write\");\n+ }\n+\n #[test]\n fn run_event_sandbox_event_keeps_properties_nested() {\n let stored = to_run_event(&fixtures::RUN_5, &Event::Sandbox {\ndiff --git a/lib/crates/fabro-workflow/src/event/events.rs b/lib/crates/fabro-workflow/src/event/events.rs\nindex e91686d22..a38b184bb 100644\n--- a/lib/crates/fabro-workflow/src/event/events.rs\n+++ b/lib/crates/fabro-workflow/src/event/events.rs\n@@ -619,6 +619,14 @@ pub enum Event {\n permission_level: Option<PermissionLevel>,\n capabilities: Vec<fabro_types::SessionCapability>,\n },\n+ /// Effective model-callable tools for a stage session after profile setup,\n+ /// optional registrations, MCP integration, and access-policy filtering.\n+ AgentToolsAvailable {\n+ node_id: String,\n+ visit: u32,\n+ session_id: String,\n+ tools: Vec<fabro_types::AgentToolSummary>,\n+ },\n /// A stage's steerable live session binding ended.\n AgentSessionDeactivated {\n node_id: String,\n@@ -1466,6 +1474,20 @@ impl Event {\n } => {\n debug!(node_id, visit, session_id, \"Agent session activated\");\n }\n+ Self::AgentToolsAvailable {\n+ node_id,\n+ visit,\n+ session_id,\n+ tools,\n+ } => {\n+ debug!(\n+ node_id,\n+ visit,\n+ session_id,\n+ tool_count = tools.len(),\n+ \"Agent tools available\"\n+ );\n+ }\n Self::AgentSessionDeactivated {\n node_id,\n visit,\ndiff --git a/lib/crates/fabro-workflow/src/event/names.rs b/lib/crates/fabro-workflow/src/event/names.rs\nindex 958f8fdeb..ae645dcf9 100644\n--- a/lib/crates/fabro-workflow/src/event/names.rs\n+++ b/lib/crates/fabro-workflow/src/event/names.rs\n@@ -140,6 +140,7 @@ pub fn event_name(event: &Event) -> &'static str {\n Event::CommandCompleted { .. } => \"command.completed\",\n Event::AgentSessionStarted { .. } => \"agent.session.started\",\n Event::AgentSessionActivated { .. } => \"agent.session.activated\",\n+ Event::AgentToolsAvailable { .. } => \"agent.tools.available\",\n Event::AgentSessionDeactivated { .. } => \"agent.session.deactivated\",\n Event::AgentSessionEnded { .. } => \"agent.session.ended\",\n Event::AgentInterruptInjected { .. } => \"agent.interrupt.injected\",\n@@ -202,6 +203,15 @@ mod tests {\n }),\n \"agent.sub.spawned\"\n );\n+ assert_eq!(\n+ event_name(&Event::AgentToolsAvailable {\n+ node_id: \"code\".to_string(),\n+ visit: 1,\n+ session_id: \"session-1\".to_string(),\n+ tools: Vec::new(),\n+ }),\n+ \"agent.tools.available\"\n+ );\n }\n \n #[test]\ndiff --git a/lib/crates/fabro-workflow/src/event/stored_fields.rs b/lib/crates/fabro-workflow/src/event/stored_fields.rs\nindex f993035de..94fba25ad 100644\n--- a/lib/crates/fabro-workflow/src/event/stored_fields.rs\n+++ b/lib/crates/fabro-workflow/src/event/stored_fields.rs\n@@ -161,6 +161,12 @@ fn stored_event_fields_for_variant(event: &Event) -> StoredEventFields {\n session_id,\n ..\n }\n+ | Event::AgentToolsAvailable {\n+ node_id,\n+ visit,\n+ session_id,\n+ ..\n+ }\n | Event::AgentSessionDeactivated {\n node_id,\n visit,\ndiff --git a/lib/crates/fabro-workflow/src/handler/llm/api.rs b/lib/crates/fabro-workflow/src/handler/llm/api.rs\nindex 42dc0e1f5..135b5e3b7 100644\n--- a/lib/crates/fabro-workflow/src/handler/llm/api.rs\n+++ b/lib/crates/fabro-workflow/src/handler/llm/api.rs\n@@ -3,11 +3,13 @@ use std::sync::{Arc, Mutex};\n \n use async_trait::async_trait;\n use fabro_agent::subagent::{SessionFactory, SubAgentManager};\n-use fabro_agent::tool_registry::{RegisteredTool, ToolContext, ToolRegistry, ToolSource};\n+use fabro_agent::tool_registry::{\n+ RegisteredTool, ToolContext, ToolDefinitionWithSource, ToolRegistry, ToolSource,\n+};\n use fabro_agent::{\n AgentEvent, AgentProfile, AnthropicProfile, CompletionCoordinator, GeminiProfile,\n Message as AgentMessage, OpenAiProfile, Sandbox, Session, SessionOptions, StaticEnvProvider,\n- ToolEnvProvider, register_question_tools,\n+ ToolEnvProvider, register_question_tools, tool_permissions,\n };\n use fabro_auth::{CredentialSource, EnvCredentialSource};\n use fabro_graphviz::graph::{AttrValue, Node};\n@@ -21,7 +23,10 @@ use fabro_mcp::config::McpServerSettings;\n use fabro_model::catalog::LlmCatalogSettings;\n use fabro_model::{AgentProfileKind, Catalog, FallbackTarget, ModelRef, ProviderId};\n use fabro_types::settings::run::RunModelControls;\n-use fabro_types::{PermissionLevel, RunId, SessionCapability, StageId};\n+use fabro_types::{\n+ AgentToolCategory, AgentToolSource, AgentToolSummary, PermissionLevel, RunId,\n+ SessionCapability, StageId,\n+};\n use serde::de::DeserializeOwned;\n use tokio::sync::Mutex as TokioMutex;\n use tokio::task::JoinHandle;\n@@ -496,6 +501,61 @@ fn last_assistant_response(session: &Session) -> String {\n .unwrap_or_default()\n }\n \n+fn agent_tool_summaries_from_definitions(\n+ tools: &[ToolDefinitionWithSource],\n+) -> Vec<AgentToolSummary> {\n+ let mut summaries: Vec<_> = tools\n+ .iter()\n+ .map(|tool| AgentToolSummary {\n+ name: tool.definition.name.clone(),\n+ description: tool.definition.description.clone(),\n+ source: agent_tool_source(&tool.definition.name, &tool.source),\n+ category: agent_tool_category(&tool.definition.name),\n+ invoked: false,\n+ })\n+ .collect();\n+ summaries.sort_by(|left, right| left.name.cmp(&right.name));\n+ summaries\n+}\n+\n+fn agent_tool_source(name: &str, source: &ToolSource) -> AgentToolSource {\n+ match source {\n+ ToolSource::Native => AgentToolSource::Native,\n+ ToolSource::Mcp { server_name } => AgentToolSource::Mcp {\n+ server_name: server_name.clone(),\n+ original_name: fabro_mcp::connection_manager::parse_qualified_name(name)\n+ .map(|(_, original_name)| original_name)\n+ .unwrap_or_else(|| name.to_string()),\n+ },\n+ ToolSource::Skill => AgentToolSource::Skill,\n+ }\n+}\n+\n+fn agent_tool_category(name: &str) -> AgentToolCategory {\n+ match tool_permissions::known_tool_category(name) {\n+ Some(\"read\") => AgentToolCategory::Read,\n+ Some(\"write\") => AgentToolCategory::Write,\n+ Some(\"shell\") => AgentToolCategory::Shell,\n+ Some(\"subagent\") => AgentToolCategory::Subagent,\n+ Some(_) | None => AgentToolCategory::Other,\n+ }\n+}\n+\n+fn emit_agent_tools_available(\n+ session: &Session,\n+ node_id: &str,\n+ stage_id: &StageId,\n+ emitter: &Arc<Emitter>,\n+) {\n+ let tools = agent_tool_summaries_from_definitions(&session.available_tools());\n+ emitter.emit(&Event::AgentToolsAvailable {\n+ node_id: node_id.to_string(),\n+ visit: stage_id.visit(),\n+ session_id: session.id().to_string(),\n+ tools,\n+ });\n+}\n+\n /// Spawn a task that subscribes to session events and:\n /// 1. Tracks file changes (write_file/edit_file tool calls) into shared state.\n /// 2. Forwards non-streaming agent events to the pipeline emitter.\n@@ -1197,6 +1257,7 @@ impl CodergenBackend for AgentApiBackend {\n return Err(err);\n }\n }\n+ emit_agent_tools_available(&session, &node.id, &stage_id, emitter);\n session\n .process_input_with_runtime(prompt, agent_tool_runtime.clone())\n .await\n@@ -1323,6 +1384,7 @@ impl CodergenBackend for AgentApiBackend {\n return Err(err);\n }\n }\n+ emit_agent_tools_available(&session, &node.id, &stage_id, emitter);\n match session\n .process_input_with_runtime(prompt, agent_tool_runtime.clone())\n .await\n@@ -1500,6 +1562,7 @@ mod tests {\n \n use chrono::TimeZone;\n use fabro_agent::subagent::SessionFactory;\n+ use fabro_agent::tool_registry::ToolDefinitionWithSource;\n use fabro_agent::{AgentProfile, LocalSandbox, ToolRegistry};\n use fabro_api::types;\n use fabro_auth::{EnvCredentialSource, VaultCredentialSource};\n@@ -1566,6 +1629,68 @@ mod tests {\n }\n }\n \n+ fn test_tool_with_source(name: &str, source: ToolSource) -> ToolDefinitionWithSource {\n+ ToolDefinitionWithSource {\n+ definition: LlmToolDefinition {\n+ name: name.to_string(),\n+ description: format!(\"{name} description\"),\n+ parameters: serde_json::json!({\n+ \"type\": \"object\",\n+ \"properties\": {\n+ \"path\": { \"type\": \"string\" }\n+ }\n+ }),\n+ },\n+ source,\n+ }\n+ }\n+\n+ #[test]\n+ fn agent_tool_summaries_map_known_native_categories_without_schemas() {\n+ let summaries = agent_tool_summaries_from_definitions(&[\n+ test_tool_with_source(\"apply_patch\", ToolSource::Native),\n+ test_tool_with_source(\"grep\", ToolSource::Native),\n+ test_tool_with_source(\"glob\", ToolSource::Native),\n+ test_tool_with_source(\"spawn_agent\", ToolSource::Native),\n+ test_tool_with_source(\"unknown_native\", ToolSource::Native),\n+ ]);\n+\n+ assert_eq!(summaries[0].name, \"apply_patch\");\n+ assert_eq!(summaries[0].description, \"apply_patch description\");\n+ assert_eq!(summaries[0].source, fabro_types::AgentToolSource::Native);\n+ assert_eq!(summaries[0].category, fabro_types::AgentToolCategory::Write);\n+ assert!(!summaries[0].invoked);\n+ assert_eq!(summaries[1].category, fabro_types::AgentToolCategory::Read);\n+ assert_eq!(summaries[2].category, fabro_types::AgentToolCategory::Read);\n+ assert_eq!(\n+ summaries[3].category,\n+ fabro_types::AgentToolCategory::Subagent\n+ );\n+ assert_eq!(summaries[4].category, fabro_types::AgentToolCategory::Other);\n+\n+ let json = serde_json::to_value(&summaries[0]).unwrap();\n+ assert!(\n+ json.as_object().unwrap().get(\"parameters\").is_none(),\n+ \"agent tool summaries should not include tool parameter schemas\"\n+ );\n+ }\n+\n+ #[test]\n+ fn agent_tool_summaries_map_mcp_source_and_original_name_from_qualified_name() {\n+ let summaries = agent_tool_summaries_from_definitions(&[test_tool_with_source(\n+ \"mcp__filesystem__read_file\",\n+ ToolSource::Mcp {\n+ server_name: \"filesystem\".to_string(),\n+ },\n+ )]);\n+\n+ assert_eq!(summaries[0].source, fabro_types::AgentToolSource::Mcp {\n+ server_name: \"filesystem\".to_string(),\n+ original_name: \"read_file\".to_string(),\n+ });\n+ assert_eq!(summaries[0].category, fabro_types::AgentToolCategory::Other);\n+ }\n+\n struct ShutdownTestProvider;\n \n #[async_trait]\ndiff --git a/lib/crates/fabro-workflow/src/operations/fork.rs b/lib/crates/fabro-workflow/src/operations/fork.rs\nindex b4ed04d30..513975d71 100644\n--- a/lib/crates/fabro-workflow/src/operations/fork.rs\n+++ b/lib/crates/fabro-workflow/src/operations/fork.rs\n@@ -230,6 +230,7 @@ fn replay_event_for_fork_projection(body: &EventBody) -> bool {\n | EventBody::InterviewTimeout(_)\n | EventBody::InterviewInterrupted(_)\n | EventBody::AgentSessionActivated(_)\n+ | EventBody::AgentToolsAvailable(_)\n | EventBody::AgentAcpStarted(_)\n | EventBody::AgentAcpCancelled(_)\n | EventBody::AgentAcpTimedOut(_)\n@@ -310,6 +311,12 @@ mod tests {\n visit: 1,\n })\n ));\n+ assert!(replay_event_for_fork_projection(\n+ &EventBody::AgentToolsAvailable(fabro_types::run_event::AgentToolsAvailableProps {\n+ tools: Vec::new(),\n+ visit: 1,\n+ })\n+ ));\n assert!(!replay_event_for_fork_projection(\n &EventBody::AgentSessionStarted(fabro_types::run_event::AgentSessionStartedProps {\n provider: Some(\"openai\".to_string()),\ndiff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES\nindex f8d333e14..c07a09e4c 100644\n--- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES\n+++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES\n@@ -27,6 +27,13 @@ models/agent-permissions.ts\n models/agent-session-activated-props.ts\n models/agent-skill-activation-source.ts\n models/agent-skill-summary.ts\n+models/agent-tool-category.ts\n+models/agent-tool-source-mcp.ts\n+models/agent-tool-source-native.ts\n+models/agent-tool-source-skill.ts\n+models/agent-tool-source.ts\n+models/agent-tool-summary.ts\n+models/agent-tools-available-props.ts\n models/aggregate-billing-totals.ts\n models/aggregate-billing.ts\n models/api-question.ts\ndiff --git a/lib/packages/fabro-api-client/src/models/agent-tool-category.ts b/lib/packages/fabro-api-client/src/models/agent-tool-category.ts\nnew file mode 100644\nindex 000000000..8e1eda4b0\n--- /dev/null\n+++ b/lib/packages/fabro-api-client/src/models/agent-tool-category.ts\n@@ -0,0 +1,29 @@\n+/* tslint:disable */\n+/* eslint-disable */\n+/**\n+ * Fabro Run API\n+ * HTTP API for managing Fabro workflow run executions.\n+ *\n+ * The version of the OpenAPI document: 0.1.0\n+ *\n+ *\n+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n+ * https://openapi-generator.tech\n+ * Do not edit the class manually.\n+ */\n+\n+\n+\n+/**\n+ * Coarse tool category for display and grouping.\n+ */\n+\n+export const AgentToolCategory = {\n+ READ: 'read',\n+ WRITE: 'write',\n+ SHELL: 'shell',\n+ SUBAGENT: 'subagent',\n+ OTHER: 'other'\n+} as const;\n+\n+export type AgentToolCategory = typeof AgentToolCategory[keyof typeof AgentToolCategory];\ndiff --git a/lib/packages/fabro-api-client/src/models/agent-tool-source-mcp.ts b/lib/packages/fabro-api-client/src/models/agent-tool-source-mcp.ts\nnew file mode 100644\nindex 000000000..3f8089a92\n--- /dev/null\n+++ b/lib/packages/fabro-api-client/src/models/agent-tool-source-mcp.ts\n@@ -0,0 +1,33 @@\n+/* tslint:disable */\n+/* eslint-disable */\n+/**\n+ * Fabro Run API\n+ * HTTP API for managing Fabro workflow run executions.\n+ *\n+ * The version of the OpenAPI document: 0.1.0\n+ *\n+ *\n+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n+ * https://openapi-generator.tech\n+ * Do not edit the class manually.\n+ */\n+\n+\n+\n+export interface AgentToolSourceMcp {\n+ 'kind': AgentToolSourceMcpKindEnum;\n+ /**\n+ * MCP server name that provided the tool.\n+ */\n+ 'server_name': string;\n+ /**\n+ * Tool name before MCP qualification.\n+ */\n+ 'original_name': string;\n+}\n+\n+export const AgentToolSourceMcpKindEnum = {\n+ MCP: 'mcp'\n+} as const;\n+\n+export type AgentToolSourceMcpKindEnum = typeof AgentToolSourceMcpKindEnum[keyof typeof AgentToolSourceMcpKindEnum];\ndiff --git a/lib/packages/fabro-api-client/src/models/agent-tool-source-native.ts b/lib/packages/fabro-api-client/src/models/agent-tool-source-native.ts\nnew file mode 100644\nindex 000000000..4ea94bead\n--- /dev/null\n+++ b/lib/packages/fabro-api-client/src/models/agent-tool-source-native.ts\n@@ -0,0 +1,25 @@\n+/* tslint:disable */\n+/* eslint-disable */\n+/**\n+ * Fabro Run API\n+ * HTTP API for managing Fabro workflow run executions.\n+ *\n+ * The version of the OpenAPI document: 0.1.0\n+ *\n+ *\n+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n+ * https://openapi-generator.tech\n+ * Do not edit the class manually.\n+ */\n+\n+\n+\n+export interface AgentToolSourceNative {\n+ 'kind': AgentToolSourceNativeKindEnum;\n+}\n+\n+export const AgentToolSourceNativeKindEnum = {\n+ NATIVE: 'native'\n+} as const;\n+\n+export type AgentToolSourceNativeKindEnum = typeof AgentToolSourceNativeKindEnum[keyof typeof AgentToolSourceNativeKindEnum];\ndiff --git a/lib/packages/fabro-api-client/src/models/agent-tool-source-skill.ts b/lib/packages/fabro-api-client/src/models/agent-tool-source-skill.ts\nnew file mode 100644\nindex 000000000..e150e8591\n--- /dev/null\n+++ b/lib/packages/fabro-api-client/src/models/agent-tool-source-skill.ts\n@@ -0,0 +1,25 @@\n+/* tslint:disable */\n+/* eslint-disable */\n+/**\n+ * Fabro Run API\n+ * HTTP API for managing Fabro workflow run executions.\n+ *\n+ * The version of the OpenAPI document: 0.1.0\n+ *\n+ *\n+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n+ * https://openapi-generator.tech\n+ * Do not edit the class manually.\n+ */\n+\n+\n+\n+export interface AgentToolSourceSkill {\n+ 'kind': AgentToolSourceSkillKindEnum;\n+}\n+\n+export const AgentToolSourceSkillKindEnum = {\n+ SKILL: 'skill'\n+} as const;\n+\n+export type AgentToolSourceSkillKindEnum = typeof AgentToolSourceSkillKindEnum[keyof typeof AgentToolSourceSkillKindEnum];\ndiff --git a/lib/packages/fabro-api-client/src/models/agent-tool-source.ts b/lib/packages/fabro-api-client/src/models/agent-tool-source.ts\nnew file mode 100644\nindex 000000000..391f72fd5\n--- /dev/null\n+++ b/lib/packages/fabro-api-client/src/models/agent-tool-source.ts\n@@ -0,0 +1,30 @@\n+/* tslint:disable */\n+/* eslint-disable */\n+/**\n+ * Fabro Run API\n+ * HTTP API for managing Fabro workflow run executions.\n+ *\n+ * The version of the OpenAPI document: 0.1.0\n+ *\n+ *\n+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n+ * https://openapi-generator.tech\n+ * Do not edit the class manually.\n+ */\n+\n+\n+// May contain unused imports in some cases\n+// @ts-ignore\n+import type { AgentToolSourceMcp } from './agent-tool-source-mcp';\n+// May contain unused imports in some cases\n+// @ts-ignore\n+import type { AgentToolSourceNative } from './agent-tool-source-native';\n+// May contain unused imports in some cases\n+// @ts-ignore\n+import type { AgentToolSourceSkill } from './agent-tool-source-skill';\n+\n+/**\n+ * @type AgentToolSource\n+ * Origin of an effective agent tool.\n+ */\n+export type AgentToolSource = { kind: 'mcp' } & AgentToolSourceMcp | { kind: 'native' } & AgentToolSourceNative | { kind: 'skill' } & AgentToolSourceSkill;\ndiff --git a/lib/packages/fabro-api-client/src/models/agent-tool-summary.ts b/lib/packages/fabro-api-client/src/models/agent-tool-summary.ts\nnew file mode 100644\nindex 000000000..b52e220c0\n--- /dev/null\n+++ b/lib/packages/fabro-api-client/src/models/agent-tool-summary.ts\n@@ -0,0 +1,41 @@\n+/* tslint:disable */\n+/* eslint-disable */\n+/**\n+ * Fabro Run API\n+ * HTTP API for managing Fabro workflow run executions.\n+ *\n+ * The version of the OpenAPI document: 0.1.0\n+ *\n+ *\n+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n+ * https://openapi-generator.tech\n+ * Do not edit the class manually.\n+ */\n+\n+\n+// May contain unused imports in some cases\n+// @ts-ignore\n+import type { AgentToolCategory } from './agent-tool-category';\n+// May contain unused imports in some cases\n+// @ts-ignore\n+import type { AgentToolSource } from './agent-tool-source';\n+\n+/**\n+ * Summary of one effective model-callable tool exposed to an agent stage.\n+ */\n+export interface AgentToolSummary {\n+ /**\n+ * Exposed model-facing tool name, for example `apply_patch` or `mcp__filesystem__read_file`.\n+ */\n+ 'name': string;\n+ /**\n+ * Model-facing tool description.\n+ */\n+ 'description': string;\n+ 'source': AgentToolSource;\n+ 'category': AgentToolCategory;\n+ /**\n+ * True once this tool has been invoked during the stage.\n+ */\n+ 'invoked': boolean;\n+}\ndiff --git a/lib/packages/fabro-api-client/src/models/agent-tools-available-props.ts b/lib/packages/fabro-api-client/src/models/agent-tools-available-props.ts\nnew file mode 100644\nindex 000000000..1231a8aa8\n--- /dev/null\n+++ b/lib/packages/fabro-api-client/src/models/agent-tools-available-props.ts\n@@ -0,0 +1,29 @@\n+/* tslint:disable */\n+/* eslint-disable */\n+/**\n+ * Fabro Run API\n+ * HTTP API for managing Fabro workflow run executions.\n+ *\n+ * The version of the OpenAPI document: 0.1.0\n+ *\n+ *\n+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n+ * https://openapi-generator.tech\n+ * Do not edit the class manually.\n+ */\n+\n+\n+// May contain unused imports in some cases\n+// @ts-ignore\n+import type { AgentToolSummary } from './agent-tool-summary';\n+\n+/**\n+ * Properties for the `agent.tools.available` event.\n+ */\n+export interface AgentToolsAvailableProps {\n+ /**\n+ * Effective model-callable tools exposed to the stage session.\n+ */\n+ 'tools': Array<AgentToolSummary>;\n+ 'visit': number;\n+}\ndiff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts\nindex 78d106d70..1a153e0e7 100644\n--- a/lib/packages/fabro-api-client/src/models/index.ts\n+++ b/lib/packages/fabro-api-client/src/models/index.ts\n@@ -4,6 +4,13 @@ export * from './agent-permissions';\n export * from './agent-session-activated-props';\n export * from './agent-skill-activation-source';\n export * from './agent-skill-summary';\n+export * from './agent-tool-category';\n+export * from './agent-tool-source';\n+export * from './agent-tool-source-mcp';\n+export * from './agent-tool-source-native';\n+export * from './agent-tool-source-skill';\n+export * from './agent-tool-summary';\n+export * from './agent-tools-available-props';\n export * from './aggregate-billing';\n export * from './aggregate-billing-totals';\n export * from './api-question';\ndiff --git a/lib/packages/fabro-api-client/src/models/stage-projection.ts b/lib/packages/fabro-api-client/src/models/stage-projection.ts\nindex 24df4baed..b49e864ca 100644\n--- a/lib/packages/fabro-api-client/src/models/stage-projection.ts\n+++ b/lib/packages/fabro-api-client/src/models/stage-projection.ts\n@@ -13,6 +13,9 @@\n */\n \n \n+// May contain unused imports in some cases\n+// @ts-ignore\n+import type { AgentToolSummary } from './agent-tool-summary';\n // May contain unused imports in some cases\n // @ts-ignore\n import type { BilledTokenCounts } from './billed-token-counts';\n@@ -96,6 +99,10 @@ export interface StageProjection {\n */\n 'skills'?: SkillsProjection;\n 'permission_level'?: PermissionLevel | null;\n+ /**\n+ * Effective model-callable tools exposed to this agent stage session. Tool parameter schemas are intentionally omitted from this projection.\n+ */\n+ 'agent_tools'?: Array<AgentToolSummary>;\n /**\n * MCP servers observed by this stage.\n */\n",
"summary": {
"files_changed": 29,
"additions": 1087,
"deletions": 39
}
}
},
{
"seq": 2285,
"checkpoint": {
"timestamp": "2026-05-24T17:57:23.050179Z",
"current_node": "simplify_opus",
"completed_nodes": [
"start",
"toolchain",
"preflight_compile",
"preflight_lint",
"implement",
"simplify_opus"
],
"node_retries": {},
"context_values": {
"failure_signature": "",
"graph.goal": "---\ntitle: feat: StageProjection agent tools API\ntype: feat\nstatus: active\ndate: 2026-05-24\n---\n\n# feat: StageProjection Agent Tools API\n\n## Overview\n\nExpose the complete effective tool list for each agent-backed stage through `StageProjection`, so UI and API consumers can show actual tools such as `apply_patch`, `grep`, `glob`, `read_file`, MCP tools, skill tools, and subagent tools without inferring them from `permission_level`.\n\nThe API should expose tool summaries with `name`, `description`, `source`, `category`, and `invoked`. It must not expose full JSON parameter schemas in the run projection.\n\n## Problem Frame\n\nThe stage sidebar currently has permission metadata such as \"Full access\", but `permission_level` is only an access mode. It does not tell consumers which tools were actually exposed to the model after provider profile setup, optional tool registration, MCP integration, and tool access policy filtering.\n\nThe authoritative list already exists at request-build time in the agent session registry. The API needs to capture that effective list once per stage session and project it onto the stage.\n\n## Requirements Trace\n\n- R1. Add a StageProjection API field containing the complete effective tools for a stage session.\n- R2. Include `name`, `description`, `source`, `category`, and `invoked` for each tool.\n- R3. Do not infer tools from `permission_level` in backend or frontend code.\n- R4. Do not expose full tool parameter schemas through StageProjection.\n- R5. Preserve existing `permission_level` and `mcp_servers` fields for compatibility.\n- R6. Mark individual tools as invoked when matching `agent.tool.started` events are projected.\n- R7. Keep legacy runs backward compatible by defaulting missing tool lists to empty.\n\n## Scope Boundaries\n\n- Do not remove or rename `StageProjection.permission_level`.\n- Do not remove the existing MCP server projection or `AgentMcpToolSummary`.\n- Do not change completion API tool definitions.\n- Do not add ACP-native tool discovery in this pass unless an ACP path already has an equivalent effective tool list available.\n- Do not render parameter schemas in the web UI.\n\n## Context & Research\n\n### Relevant Code and Patterns\n\n- OpenAPI is the source of truth for HTTP contracts in `docs/public/api-reference/fabro-api.yaml`.\n- Shared API/projection DTOs should live in `fabro-types`, with `fabro-api/build.rs` replacements to avoid duplicate generated Rust types.\n- `StageProjection` lives in `lib/crates/fabro-types/src/run_projection.rs`.\n- Durable run event props live in `lib/crates/fabro-types/src/run_event/agent.rs` and `lib/crates/fabro-types/src/run_event/mod.rs`.\n- Workflow event conversion and event names live in `lib/crates/fabro-workflow/src/event/convert.rs`, `events.rs`, and `names.rs`.\n- Projection replay lives in `lib/crates/fabro-store/src/run_state.rs`.\n- The effective request tool list is built in `lib/crates/fabro-agent/src/session.rs` from `ToolRegistry::definitions_with_source_for_policy`.\n- Tool source metadata already exists as `ToolSource` and `ToolDefinitionWithSource` in `lib/crates/fabro-agent/src/tool_registry.rs`.\n- Tool category mapping already exists in `lib/crates/fabro-agent/src/tool_permissions.rs`.\n- The sidebar display lives in `apps/fabro-web/app/components/stage-insights-sidebar.tsx`.\n\n### Strategy Docs\n\n- Read `docs/internal/events-strategy.md` before adding the new durable event.\n- Read `docs/internal/testing-strategy.md` before adding or reorganizing tests.\n- Follow the OpenAPI type ownership guidance in `AGENTS.md`: reuse `fabro-types` through `fabro-api/build.rs` replacements when the API schema has the same product meaning and serde shape.\n\n## Key Technical Decisions\n\n- Add a new StageProjection field named `agent_tools`, not `tools`, to avoid ambiguity with MCP nested tools and completion tool definitions.\n- Add a dedicated durable event named `agent.tools.available` instead of overloading `agent.session.activated`.\n- Capture the effective tool list after session setup and filtering, using the same path as model request construction.\n- Store descriptions in the summary because they are useful API/UI metadata; omit parameter schemas to keep projection payloads small and avoid leaking full implementation detail.\n- Keep `AgentMcpToolSummary` MCP-only. Add a new general-purpose `AgentToolSummary` instead of stretching the MCP type beyond its meaning.\n- Treat `invoked` as projected state. The availability event should emit tools with `invoked: false`; replay of `agent.tool.started` flips matching tools to true.\n\n## API Contract\n\nAdd these schemas to OpenAPI and map them to `fabro_types` replacements:\n\n- `AgentToolSummary`\n - required: `name`, `description`, `source`, `category`, `invoked`\n - `name`: exposed tool name, e.g. `apply_patch` or `mcp__filesystem__read_file`\n - `description`: model-facing tool description\n - `source`: `AgentToolSource`\n - `category`: `AgentToolCategory`\n - `invoked`: boolean\n- `AgentToolSource`\n - tagged by `kind`\n - `native`\n - `mcp` with `server_name` and `original_name`\n - `skill`\n- `AgentToolCategory`\n - enum: `read`, `write`, `shell`, `subagent`, `other`\n- `AgentToolsAvailableProps`\n - required: `tools`, `visit`\n - `tools`: array of `AgentToolSummary`\n - `visit`: stage visit number\n\nAdd to `StageProjection`:\n\n- `agent_tools`: array of `AgentToolSummary`\n- Default to an empty array when omitted.\n- Skip serializing when empty, matching existing projection optional-list style.\n\nAdd event body:\n\n- Serialized event name: `agent.tools.available`\n- Event body type: `AgentToolsAvailableProps`\n\n## Implementation Units\n\n- [ ] **Unit 1: Add shared tool summary types**\n\n**Goal:** Define the canonical API/projection DTOs in `fabro-types`.\n\n**Files:**\n- Modify: `lib/crates/fabro-types/src/run_event/agent.rs`\n- Modify: `lib/crates/fabro-types/src/run_event/mod.rs`\n- Modify: `lib/crates/fabro-types/src/run_projection.rs`\n- Modify: `lib/crates/fabro-types/src/lib.rs`\n\n**Work:**\n- Add `AgentToolSummary`, `AgentToolSource`, `AgentToolCategory`, and `AgentToolsAvailableProps`.\n- Add `EventBody::AgentToolsAvailable` serialized as `agent.tools.available`.\n- Add `agent_tools: Vec<AgentToolSummary>` to `StageProjection`.\n- Ensure all new fields default cleanly for older persisted events/projections.\n- Export the new public types from `fabro-types`.\n\n- [ ] **Unit 2: Capture effective session tools**\n\n**Goal:** Provide an authoritative one-time source for the list that the model can actually call.\n\n**Files:**\n- Modify: `lib/crates/fabro-agent/src/session.rs`\n- Modify: `lib/crates/fabro-workflow/src/handler/llm/api.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/events.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/names.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/convert.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/stored_fields.rs` only if the new event needs non-standard stored fields.\n\n**Work:**\n- Add `Session::available_tools()` that returns the same effective `ToolDefinitionWithSource` list as `build_request()`.\n- Map `ToolDefinitionWithSource` to `AgentToolSummary` at the workflow boundary.\n- Populate `description` from `ToolDefinition.description`.\n- Populate `source` from `ToolSource`.\n- For MCP tools, include the server name from `ToolSource::Mcp` and derive `original_name` from the qualified exposed name using the existing MCP naming convention.\n- Populate `category` from the existing tool category mapping for known exposed names; use `other` when no category mapping applies.\n- Emit `agent.tools.available` once for the stage session after session setup and filtering are complete.\n\n- [ ] **Unit 3: Project available and invoked tools**\n\n**Goal:** Make `StageProjection.agent_tools` replay-authoritative.\n\n**Files:**\n- Modify: `lib/crates/fabro-store/src/run_state.rs`\n\n**Work:**\n- On `EventBody::AgentToolsAvailable`, replace the current stage visit's `agent_tools` with the event tools.\n- On `EventBody::AgentToolStarted`, find a matching `agent_tools` entry by exposed `tool_name` and set `invoked = true`.\n- Keep the existing MCP server `invoked` update unchanged.\n- If a legacy run has no availability event, do not synthesize a full list from permissions.\n\n- [ ] **Unit 4: Update OpenAPI and generated clients**\n\n**Goal:** Expose the new projection field and event contract through public API clients without duplicate Rust API types.\n\n**Files:**\n- Modify: `docs/public/api-reference/fabro-api.yaml`\n- Modify: `lib/crates/fabro-api/build.rs`\n- Modify: `lib/crates/fabro-api/src/lib.rs`\n- Modify generated files under `lib/crates/fabro-api/src/generated.rs` via `cargo build -p fabro-api`.\n- Modify generated files under `lib/packages/fabro-api-client/src` via TypeScript client generation.\n\n**Work:**\n- Add the OpenAPI schemas listed in the API Contract section.\n- Add `StageProjection.agent_tools`.\n- Add `fabro-api/build.rs` replacements for the new `fabro-types` types.\n- Regenerate Rust API code.\n- Regenerate TypeScript API client code.\n\n- [ ] **Unit 5: Render tools in the web sidebar**\n\n**Goal:** Replace the permission-derived sidebar display with the actual projected tool list.\n\n**Files:**\n- Modify: `apps/fabro-web/app/components/stage-insights-sidebar.tsx`\n- Modify: `apps/fabro-web/app/components/stage-insights-sidebar.test.tsx`\n\n**Work:**\n- Render `stage.agent_tools` when present.\n- Show each tool's name, description, source/category, and invoked state.\n- Keep permission level as secondary metadata or fallback for legacy stages with no `agent_tools`.\n- Do not infer tool availability from permission level.\n\n## Test Plan\n\n- `fabro-types`\n - JSON round-trip for `agent.tools.available`.\n - Serialization checks for `AgentToolSource` and `AgentToolCategory`.\n - Backward compatibility check that missing `agent_tools` deserializes to an empty list.\n\n- `fabro-workflow`\n - Event name and conversion tests for `agent.tools.available`.\n - Capture test proving native tools such as `apply_patch`, `grep`, and `glob` are emitted from the effective registry path.\n - MCP source mapping test for a qualified MCP tool name.\n\n- `fabro-store`\n - Projection test that `agent.tools.available` populates `StageProjection.agent_tools`.\n - Projection test that `agent.tool.started` marks only the matching tool as invoked.\n - Regression test that MCP server `invoked` status still updates as before.\n\n- `fabro-api`\n - Type identity/parity tests confirming API types reuse `fabro_types::AgentToolSummary`, `AgentToolSource`, `AgentToolCategory`, and `AgentToolsAvailableProps`.\n - StageProjection round-trip test including `agent_tools`.\n\n- `fabro-web`\n - Sidebar test rendering tool names and descriptions from `stage.agent_tools`.\n - Sidebar test showing invoked state.\n - Legacy fallback test for stages without `agent_tools`.\n\n## Run Checks\n\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo nextest run -p fabro-types -p fabro-workflow -p fabro-store -p fabro-api`\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cd apps/fabro-web && bun test && bun run typecheck`\n\n## Assumptions\n\n- The first implementation targets normal API-backed agent sessions, not ACP-native sessions.\n- `description` is safe to expose because it is already model-facing tool metadata, but parameter schemas remain out of scope for StageProjection.\n- `agent_tools` is a complete list only for runs that emit `agent.tools.available`; legacy runs return an empty list and may still show existing permission/MCP metadata.\n- If tool registration becomes mutable later, the event contract can be re-emitted and projection replacement semantics will still work.\n",
"graph.rankdir": "LR",
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"internal.retry_count.preflight_compile": 0,
"current_node": "simplify_opus",
"internal.fidelity": "compact",
"internal.retry_count.simplify_opus": 0,
"outcome": "succeeded",
"response.implement": "The TS generator is pinned now, but this sandbox lacks Java, which OpenAPI Generator needs. Ill install a headless JRE and rerun generation.",
"thread.implement.current_node": "simplify_opus",
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
"internal.retry_count.preflight_lint": 0,
"internal.retry_count.start": 0,
"internal.retry_count.toolchain": 0,
"internal.work_dir": "/home/daytona/workspace/fabro",
"last_stage": "simplify_opus",
"thread.preflight_lint.current_node": "implement",
"thread.start.current_node": "toolchain",
"thread.toolchain.current_node": "preflight_compile",
"response.simplify_opus": "## Summary\n\nAggregated three review agents' findings and applied the consensus high-value fixes:\n\n**Quality / reuse fixes:**\n- `tool_permissions::known_tool_category` now returns `Option<AgentToolCategory>` directly. Deleted the brittle `Some(\"read\")` → `Read` string match in `handler/llm/api.rs`. `is_auto_approved` and `tool_category` are now enum-typed end-to-end; cli.rs and its tests updated accordingly. Adding a new category is now a compile-time exhaustiveness error instead of a silent `Other`.\n- `ToolSource::Mcp` now carries `original_name` populated from `info.original_tool_name` in `make_mcp_tools`. Workflow code no longer re-parses qualified MCP names (eliminates the silent fallback bug where a malformed prefix would store the qualified name as \"original\").\n- Moved `agent_tool_source` + `agent_tool_summaries_from_definitions` out of `handler/llm/api.rs`. New `ToolDefinitionWithSource::to_agent_tool_summary()` + `Session::agent_tool_summaries()` colocate the conversion next to the source type. Tests moved to `tool_registry.rs`.\n- Collapsed `Session::available_tools` and the duplicate private `effective_tools` into one public `effective_tools()` method.\n- Merged `ToolAvailableIcon` and `TodoPendingIcon` into a single `EmptyCircleIcon` component in the sidebar.\n\n**Efficiency fix:**\n- Gated `emit_agent_tools_available` on `!is_reused` in the primary path. Reused steerable sessions had been re-emitting the full tool list (with descriptions cloned into the event payload) on every prompt even though registry, access policy, and exposure mode are immutable for the session's lifetime. Failover path keeps its emit since it always builds a fresh session.\n\n**Skipped (cost > benefit):** `mcp_server_from_tool_name` dedup in `fabro-store` would require pulling `fabro-mcp` (with `rmcp`) into the store crate or relocating the parse helper into `fabro-types`. The duplicate is 5 lines using the same delimiter convention — not worth the dependency surface.\n\n**Verification:** `cargo check --workspace --all-targets`, `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`, `cargo +nightly-2026-04-14 fmt --check --all`, `cargo nextest run -p fabro-types -p fabro-workflow -p fabro-store -p fabro-api -p fabro-agent` (2250+464 tests), `bun test stage-insights-sidebar` (10/10), and `bun run typecheck` all pass. The 14 unrelated `run-actions.test.ts` failures pre-exist on the parent commit.",
"last_response": "## Summary\n\nAggregated three review agents' findings and applied the consensus high-value fixes:\n\n**Quality / reuse fixes:**\n- `tool_permissions::known_tool_category` now returns `Option<AgentToolCate",
"failure_class": "",
"internal.retry_count.implement": 0,
"internal.run_id": "01KSDFKD9JJS7D2ZJ6JQYXV58Z",
"internal.node_visit_count": 1,
"internal.thread_id": "implement",
"thread.preflight_compile.current_node": "preflight_lint"
},
"node_outcomes": {
"preflight_compile": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
},
"notes": "Script completed: cargo check -q --workspace 2>&1",
"usage": null
},
"preflight_lint": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
},
"notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
"usage": null
},
"simplify_opus": {
"status": "succeeded",
"context_updates": {
"response.simplify_opus": "## Summary\n\nAggregated three review agents' findings and applied the consensus high-value fixes:\n\n**Quality / reuse fixes:**\n- `tool_permissions::known_tool_category` now returns `Option<AgentToolCategory>` directly. Deleted the brittle `Some(\"read\")` → `Read` string match in `handler/llm/api.rs`. `is_auto_approved` and `tool_category` are now enum-typed end-to-end; cli.rs and its tests updated accordingly. Adding a new category is now a compile-time exhaustiveness error instead of a silent `Other`.\n- `ToolSource::Mcp` now carries `original_name` populated from `info.original_tool_name` in `make_mcp_tools`. Workflow code no longer re-parses qualified MCP names (eliminates the silent fallback bug where a malformed prefix would store the qualified name as \"original\").\n- Moved `agent_tool_source` + `agent_tool_summaries_from_definitions` out of `handler/llm/api.rs`. New `ToolDefinitionWithSource::to_agent_tool_summary()` + `Session::agent_tool_summaries()` colocate the conversion next to the source type. Tests moved to `tool_registry.rs`.\n- Collapsed `Session::available_tools` and the duplicate private `effective_tools` into one public `effective_tools()` method.\n- Merged `ToolAvailableIcon` and `TodoPendingIcon` into a single `EmptyCircleIcon` component in the sidebar.\n\n**Efficiency fix:**\n- Gated `emit_agent_tools_available` on `!is_reused` in the primary path. Reused steerable sessions had been re-emitting the full tool list (with descriptions cloned into the event payload) on every prompt even though registry, access policy, and exposure mode are immutable for the session's lifetime. Failover path keeps its emit since it always builds a fresh session.\n\n**Skipped (cost > benefit):** `mcp_server_from_tool_name` dedup in `fabro-store` would require pulling `fabro-mcp` (with `rmcp`) into the store crate or relocating the parse helper into `fabro-types`. The duplicate is 5 lines using the same delimiter convention — not worth the dependency surface.\n\n**Verification:** `cargo check --workspace --all-targets`, `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`, `cargo +nightly-2026-04-14 fmt --check --all`, `cargo nextest run -p fabro-types -p fabro-workflow -p fabro-store -p fabro-api -p fabro-agent` (2250+464 tests), `bun test stage-insights-sidebar` (10/10), and `bun run typecheck` all pass. The 14 unrelated `run-actions.test.ts` failures pre-exist on the parent commit.",
"last_stage": "simplify_opus",
"last_response": "## Summary\n\nAggregated three review agents' findings and applied the consensus high-value fixes:\n\n**Quality / reuse fixes:**\n- `tool_permissions::known_tool_category` now returns `Option<AgentToolCate"
},
"notes": "Stage completed: simplify_opus",
"usage": {
"input": {
"usage": {
"model": {
"provider": "anthropic",
"model_id": "claude-opus-4-7"
},
"tokens": {
"input_tokens": 113693,
"output_tokens": 34970,
"reasoning_tokens": 0,
"cache_read_tokens": 9248300,
"cache_write_tokens": 728711
}
},
"facts": {
"algorithm": "anthropic",
"cache_write_5m_tokens": 728711,
"cache_write_1h_tokens": 0
}
},
"total_usd_micros": 10621308
},
"files_touched": [
"/home/daytona/workspace/fabro/apps/fabro-web/app/components/stage-insights-sidebar.tsx",
"/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/cli.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/context_window.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/mcp_integration.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/session.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/tool_permissions.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/tool_registry.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/handler/llm/api.rs"
]
},
"start": {
"status": "succeeded",
"usage": null
},
"toolchain": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
},
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
"usage": null
},
"implement": {
"status": "succeeded",
"context_updates": {
"last_stage": "implement",
"last_response": "The TS generator is pinned now, but this sandbox lacks Java, which OpenAPI Generator needs. Ill install a headless JRE and rerun generation.",
"response.implement": "The TS generator is pinned now, but this sandbox lacks Java, which OpenAPI Generator needs. Ill install a headless JRE and rerun generation."
},
"notes": "Stage completed: implement",
"usage": {
"input": {
"usage": {
"model": {
"provider": "openai",
"model_id": "gpt-5.5"
},
"tokens": {
"input_tokens": 6711598,
"output_tokens": 22676,
"reasoning_tokens": 11522,
"cache_read_tokens": 16107008,
"cache_write_tokens": 0
}
},
"facts": {
"algorithm": "openai"
}
},
"total_usd_micros": 42637434
}
}
},
"next_node_id": "simplify_gpt",
"git_commit_sha": "dcf957fbeeebb961509a88854e12dd241029a52e",
"node_visits": {
"implement": 1,
"start": 1,
"simplify_opus": 1,
"preflight_compile": 1,
"preflight_lint": 1,
"toolchain": 1
}
},
"diff": {
"patch": "diff --git a/apps/fabro-web/app/components/stage-insights-sidebar.tsx b/apps/fabro-web/app/components/stage-insights-sidebar.tsx\nindex 135f1dd72..1b534c181 100644\n--- a/apps/fabro-web/app/components/stage-insights-sidebar.tsx\n+++ b/apps/fabro-web/app/components/stage-insights-sidebar.tsx\n@@ -283,13 +283,18 @@ function todoStatusVisual(status: TodoStatus): { Icon: IconType; color: string;\n return { Icon: XCircleIcon, color: \"text-fg-muted\", srLabel: \"Deleted\" };\n case TodoStatus.PENDING:\n default:\n- return { Icon: TodoPendingIcon, color: \"text-fg-muted\", srLabel: \"Pending\" };\n+ return { Icon: EmptyCircleIcon, color: \"text-fg-muted\", srLabel: \"Pending\" };\n }\n }\n \n-/** Empty circle for pending todos (matches Tailwind sizing). */\n-function TodoPendingIcon({ className }: { className?: string }) {\n- return <span className={`inline-block rounded-full border border-current ${className ?? \"\"}`} />;\n+/** Empty circle for pending/available states (matches Tailwind sizing). */\n+function EmptyCircleIcon({ className }: { className?: string }) {\n+ return (\n+ <span\n+ className={`inline-block rounded-full border border-current ${className ?? \"\"}`}\n+ aria-hidden=\"true\"\n+ />\n+ );\n }\n \n // ---------- Context window ----------\n@@ -529,7 +534,7 @@ function AgentToolsSection({ tools }: { tools: AgentToolSummary[] }) {\n {tool.invoked ? (\n <CheckCircleIcon className=\"size-3.5 shrink-0 text-mint\" aria-label=\"Invoked\" />\n ) : (\n- <ToolAvailableIcon className=\"size-3.5 shrink-0 text-fg-muted\" />\n+ <EmptyCircleIcon className=\"size-3.5 shrink-0 text-fg-muted\" />\n )}\n <span className={nameClass}>{tool.name}</span>\n <span className=\"font-mono text-[10px] tabular-nums text-fg-muted\">\n@@ -564,10 +569,6 @@ function toolSourceLabel(source: AgentToolSummary[\"source\"]): string {\n }\n }\n \n-function ToolAvailableIcon({ className }: { className?: string }) {\n- return <span className={`inline-block rounded-full border border-current ${className ?? \"\"}`} aria-hidden=\"true\" />;\n-}\n-\n // ---------- MCPs ----------\n \n function McpSection({ servers }: { servers: McpServerProjection[] }) {\ndiff --git a/lib/crates/fabro-agent/src/cli.rs b/lib/crates/fabro-agent/src/cli.rs\nindex b736fdb3f..e3e874803 100644\n--- a/lib/crates/fabro-agent/src/cli.rs\n+++ b/lib/crates/fabro-agent/src/cli.rs\n@@ -93,7 +93,7 @@ pub enum OutputFormat {\n Json,\n }\n \n-pub use fabro_types::PermissionLevel;\n+pub use fabro_types::{AgentToolCategory, PermissionLevel};\n \n impl AgentArgs {\n /// Fill `None` fields from settings.toml values, then hardcoded defaults.\n@@ -155,6 +155,8 @@ fn build_tool_approval(\n \"Allow {} ({category})? [y]es / [n]o / [a]lways: \",\n styles.bold.apply_to(tool_name),\n );\n+ // `AgentToolCategory` derives strum::Display so it renders as the\n+ // canonical snake_case label (e.g. \"read\", \"write\").\n std::io::stderr().flush().ok();\n \n let mut input = String::new();\n@@ -166,7 +168,7 @@ fn build_tool_approval(\n \"y\" | \"yes\" => Ok(()),\n \"a\" | \"always\" => {\n let mut lvl = level.lock().expect(\"permission lock poisoned\");\n- *lvl = if category == \"write\" {\n+ *lvl = if category == AgentToolCategory::Write {\n PermissionLevel::ReadWrite\n } else {\n PermissionLevel::Full\n@@ -816,62 +818,98 @@ mod tests {\n \n #[test]\n fn tool_category_read_tools() {\n- assert_eq!(tool_category(\"read_file\"), \"read\");\n- assert_eq!(tool_category(\"read_many_files\"), \"read\");\n- assert_eq!(tool_category(\"grep\"), \"read\");\n- assert_eq!(tool_category(\"glob\"), \"read\");\n- assert_eq!(tool_category(\"list_dir\"), \"read\");\n+ assert_eq!(tool_category(\"read_file\"), AgentToolCategory::Read);\n+ assert_eq!(tool_category(\"read_many_files\"), AgentToolCategory::Read);\n+ assert_eq!(tool_category(\"grep\"), AgentToolCategory::Read);\n+ assert_eq!(tool_category(\"glob\"), AgentToolCategory::Read);\n+ assert_eq!(tool_category(\"list_dir\"), AgentToolCategory::Read);\n }\n \n #[test]\n fn tool_category_write_tools() {\n- assert_eq!(tool_category(\"write_file\"), \"write\");\n- assert_eq!(tool_category(\"edit_file\"), \"write\");\n- assert_eq!(tool_category(\"apply_patch\"), \"write\");\n+ assert_eq!(tool_category(\"write_file\"), AgentToolCategory::Write);\n+ assert_eq!(tool_category(\"edit_file\"), AgentToolCategory::Write);\n+ assert_eq!(tool_category(\"apply_patch\"), AgentToolCategory::Write);\n }\n \n #[test]\n fn tool_category_shell() {\n- assert_eq!(tool_category(\"shell\"), \"shell\");\n+ assert_eq!(tool_category(\"shell\"), AgentToolCategory::Shell);\n }\n \n #[test]\n fn tool_category_subagent_tools() {\n- assert_eq!(tool_category(\"spawn_agent\"), \"subagent\");\n- assert_eq!(tool_category(\"send_input\"), \"subagent\");\n- assert_eq!(tool_category(\"wait\"), \"subagent\");\n- assert_eq!(tool_category(\"close_agent\"), \"subagent\");\n+ assert_eq!(tool_category(\"spawn_agent\"), AgentToolCategory::Subagent);\n+ assert_eq!(tool_category(\"send_input\"), AgentToolCategory::Subagent);\n+ assert_eq!(tool_category(\"wait\"), AgentToolCategory::Subagent);\n+ assert_eq!(tool_category(\"close_agent\"), AgentToolCategory::Subagent);\n }\n \n #[test]\n fn tool_category_unknown_defaults_to_shell() {\n- assert_eq!(tool_category(\"some_random_tool\"), \"shell\");\n+ assert_eq!(tool_category(\"some_random_tool\"), AgentToolCategory::Shell);\n }\n \n // is_auto_approved tests\n \n #[test]\n fn is_auto_approved_read_only() {\n- assert!(is_auto_approved(PermissionLevel::ReadOnly, \"read\"));\n- assert!(is_auto_approved(PermissionLevel::ReadOnly, \"subagent\"));\n- assert!(!is_auto_approved(PermissionLevel::ReadOnly, \"write\"));\n- assert!(!is_auto_approved(PermissionLevel::ReadOnly, \"shell\"));\n+ assert!(is_auto_approved(\n+ PermissionLevel::ReadOnly,\n+ AgentToolCategory::Read\n+ ));\n+ assert!(is_auto_approved(\n+ PermissionLevel::ReadOnly,\n+ AgentToolCategory::Subagent\n+ ));\n+ assert!(!is_auto_approved(\n+ PermissionLevel::ReadOnly,\n+ AgentToolCategory::Write\n+ ));\n+ assert!(!is_auto_approved(\n+ PermissionLevel::ReadOnly,\n+ AgentToolCategory::Shell\n+ ));\n }\n \n #[test]\n fn is_auto_approved_read_write() {\n- assert!(is_auto_approved(PermissionLevel::ReadWrite, \"read\"));\n- assert!(is_auto_approved(PermissionLevel::ReadWrite, \"subagent\"));\n- assert!(is_auto_approved(PermissionLevel::ReadWrite, \"write\"));\n- assert!(!is_auto_approved(PermissionLevel::ReadWrite, \"shell\"));\n+ assert!(is_auto_approved(\n+ PermissionLevel::ReadWrite,\n+ AgentToolCategory::Read\n+ ));\n+ assert!(is_auto_approved(\n+ PermissionLevel::ReadWrite,\n+ AgentToolCategory::Subagent\n+ ));\n+ assert!(is_auto_approved(\n+ PermissionLevel::ReadWrite,\n+ AgentToolCategory::Write\n+ ));\n+ assert!(!is_auto_approved(\n+ PermissionLevel::ReadWrite,\n+ AgentToolCategory::Shell\n+ ));\n }\n \n #[test]\n fn is_auto_approved_full() {\n- assert!(is_auto_approved(PermissionLevel::Full, \"read\"));\n- assert!(is_auto_approved(PermissionLevel::Full, \"subagent\"));\n- assert!(is_auto_approved(PermissionLevel::Full, \"write\"));\n- assert!(is_auto_approved(PermissionLevel::Full, \"shell\"));\n+ assert!(is_auto_approved(\n+ PermissionLevel::Full,\n+ AgentToolCategory::Read\n+ ));\n+ assert!(is_auto_approved(\n+ PermissionLevel::Full,\n+ AgentToolCategory::Subagent\n+ ));\n+ assert!(is_auto_approved(\n+ PermissionLevel::Full,\n+ AgentToolCategory::Write\n+ ));\n+ assert!(is_auto_approved(\n+ PermissionLevel::Full,\n+ AgentToolCategory::Shell\n+ ));\n }\n \n // build_tool_approval non-interactive tests\ndiff --git a/lib/crates/fabro-agent/src/context_window.rs b/lib/crates/fabro-agent/src/context_window.rs\nindex da8da9d72..88dcc616b 100644\n--- a/lib/crates/fabro-agent/src/context_window.rs\n+++ b/lib/crates/fabro-agent/src/context_window.rs\n@@ -382,7 +382,8 @@ mod tests {\n let tools = vec![\n tool(\"read_file\", ToolSource::Native),\n tool(\"mcp__server__search\", ToolSource::Mcp {\n- server_name: \"server\".to_string(),\n+ server_name: \"server\".to_string(),\n+ original_name: \"search\".to_string(),\n }),\n tool(\"use_skill\", ToolSource::Skill),\n ];\ndiff --git a/lib/crates/fabro-agent/src/mcp_integration.rs b/lib/crates/fabro-agent/src/mcp_integration.rs\nindex e839ef4e3..65787f448 100644\n--- a/lib/crates/fabro-agent/src/mcp_integration.rs\n+++ b/lib/crates/fabro-agent/src/mcp_integration.rs\n@@ -15,6 +15,7 @@ pub fn make_mcp_tools(manager: &Arc<McpConnectionManager>) -> Vec<RegisteredTool\n let mgr = Arc::clone(manager);\n let name = qualified_name.clone();\n let server_name = info.server_name.clone();\n+ let original_name = info.original_tool_name.clone();\n let tool_timeout = std::time::Duration::from_mins(2);\n \n RegisteredTool {\n@@ -35,7 +36,10 @@ pub fn make_mcp_tools(manager: &Arc<McpConnectionManager>) -> Vec<RegisteredTool\n call_result_to_string(&result)\n })\n }),\n- source: ToolSource::Mcp { server_name },\n+ source: ToolSource::Mcp {\n+ server_name,\n+ original_name,\n+ },\n }\n })\n .collect()\ndiff --git a/lib/crates/fabro-agent/src/session.rs b/lib/crates/fabro-agent/src/session.rs\nindex 8a8fcf189..f6921361b 100644\n--- a/lib/crates/fabro-agent/src/session.rs\n+++ b/lib/crates/fabro-agent/src/session.rs\n@@ -18,8 +18,8 @@ use fabro_mcp::config::{McpServerSettings, McpTransport};\n use fabro_mcp::connection_manager::McpConnectionManager;\n use fabro_model::{AgentProfileKind, Catalog, ModelRef, Speed};\n use fabro_types::{\n- PermissionLevel, Principal, SessionMessage, SessionRecord, StageContextWindowProjection,\n- SteeringMessage,\n+ AgentToolSummary, PermissionLevel, Principal, SessionMessage, SessionRecord,\n+ StageContextWindowProjection, SteeringMessage,\n };\n use futures::StreamExt;\n use tokio::sync::{Mutex as AsyncMutex, Notify, broadcast};\n@@ -501,9 +501,32 @@ impl Session {\n self.config.permission_level\n }\n \n+ /// Effective tool list the model is exposed to after provider-profile\n+ /// setup, optional registrations, MCP integration, and access-policy\n+ /// filtering. This is the same path used to build outbound requests.\n #[must_use]\n- pub fn available_tools(&self) -> Vec<ToolDefinitionWithSource> {\n- self.effective_tools()\n+ pub fn effective_tools(&self) -> Vec<ToolDefinitionWithSource> {\n+ self.provider_profile\n+ .tool_registry()\n+ .definitions_with_source_for_policy(\n+ self.config.tool_access_policy.as_deref(),\n+ self.config.tool_exposure_mode,\n+ )\n+ }\n+\n+ /// Public projection of `effective_tools()` for\n+ /// `StageProjection.agent_tools` and the `agent.tools.available` event.\n+ /// Sorted by name for deterministic snapshots; the underlying registry\n+ /// stores tools in a `HashMap`.\n+ #[must_use]\n+ pub fn agent_tool_summaries(&self) -> Vec<AgentToolSummary> {\n+ let mut summaries: Vec<_> = self\n+ .effective_tools()\n+ .iter()\n+ .map(ToolDefinitionWithSource::to_agent_tool_summary)\n+ .collect();\n+ summaries.sort_by(|left, right| left.name.cmp(&right.name));\n+ summaries\n }\n \n /// Initialize session by discovering project docs and capturing environment\n@@ -2021,15 +2044,6 @@ impl Session {\n });\n }\n }\n-\n- fn effective_tools(&self) -> Vec<ToolDefinitionWithSource> {\n- self.provider_profile\n- .tool_registry()\n- .definitions_with_source_for_policy(\n- self.config.tool_access_policy.as_deref(),\n- self.config.tool_exposure_mode,\n- )\n- }\n }\n \n const fn is_auth_error(err: &LlmError) -> bool {\n@@ -3281,7 +3295,7 @@ mod tests {\n }\n \n #[tokio::test]\n- async fn available_tools_uses_same_effective_registry_filter_as_requests() {\n+ async fn effective_tools_match_request_tool_filtering() {\n let provider = Arc::new(CapturingLlmProvider::new());\n let client = make_client(provider as Arc<dyn ProviderAdapter>).await;\n let mut registry = ToolRegistry::new();\n@@ -3301,7 +3315,7 @@ mod tests {\n };\n let session = Session::new(client, profile, env, config, None);\n \n- let tools = session.available_tools();\n+ let tools = session.effective_tools();\n let mut tool_names: Vec<&str> = tools\n .iter()\n .map(|tool| tool.definition.name.as_str())\ndiff --git a/lib/crates/fabro-agent/src/tool_permissions.rs b/lib/crates/fabro-agent/src/tool_permissions.rs\nindex b22dba945..a56d17e08 100644\n--- a/lib/crates/fabro-agent/src/tool_permissions.rs\n+++ b/lib/crates/fabro-agent/src/tool_permissions.rs\n@@ -1,25 +1,35 @@\n-use fabro_types::PermissionLevel;\n+use fabro_types::{AgentToolCategory, PermissionLevel};\n \n-pub fn tool_category(name: &str) -> &'static str {\n- known_tool_category(name).unwrap_or(\"shell\")\n-}\n-\n-pub fn known_tool_category(name: &str) -> Option<&'static str> {\n+/// Coarse access category for an exposed tool. Returns `None` for unknown\n+/// names so callers can decide whether to default (legacy CLI permission\n+/// gate) or surface a distinct \"other\" category (projection metadata).\n+pub fn known_tool_category(name: &str) -> Option<AgentToolCategory> {\n match name {\n- \"read_file\" | \"read_many_files\" | \"grep\" | \"glob\" | \"list_dir\" => Some(\"read\"),\n- \"write_file\" | \"edit_file\" | \"apply_patch\" => Some(\"write\"),\n- \"shell\" => Some(\"shell\"),\n- \"spawn_agent\" | \"send_input\" | \"wait\" | \"close_agent\" => Some(\"subagent\"),\n+ \"read_file\" | \"read_many_files\" | \"grep\" | \"glob\" | \"list_dir\" => {\n+ Some(AgentToolCategory::Read)\n+ }\n+ \"write_file\" | \"edit_file\" | \"apply_patch\" => Some(AgentToolCategory::Write),\n+ \"shell\" => Some(AgentToolCategory::Shell),\n+ \"spawn_agent\" | \"send_input\" | \"wait\" | \"close_agent\" => Some(AgentToolCategory::Subagent),\n _ => None,\n }\n }\n \n-pub fn is_auto_approved(level: PermissionLevel, category: &str) -> bool {\n+/// CLI permission gate category. Unknown tools fall back to `Shell` so they\n+/// require explicit user approval at any permission level below `Full`.\n+pub fn tool_category(name: &str) -> AgentToolCategory {\n+ known_tool_category(name).unwrap_or(AgentToolCategory::Shell)\n+}\n+\n+pub fn is_auto_approved(level: PermissionLevel, category: AgentToolCategory) -> bool {\n matches!(\n (level, category),\n- (_, \"read\" | \"subagent\")\n- | (PermissionLevel::ReadWrite | PermissionLevel::Full, \"write\")\n- | (PermissionLevel::Full, \"shell\")\n+ (_, AgentToolCategory::Read | AgentToolCategory::Subagent)\n+ | (\n+ PermissionLevel::ReadWrite | PermissionLevel::Full,\n+ AgentToolCategory::Write,\n+ )\n+ | (PermissionLevel::Full, AgentToolCategory::Shell)\n )\n }\n \ndiff --git a/lib/crates/fabro-agent/src/tool_registry.rs b/lib/crates/fabro-agent/src/tool_registry.rs\nindex 3e43d7e34..74af24354 100644\n--- a/lib/crates/fabro-agent/src/tool_registry.rs\n+++ b/lib/crates/fabro-agent/src/tool_registry.rs\n@@ -4,11 +4,13 @@ use std::pin::Pin;\n use std::sync::Arc;\n \n use fabro_llm::types::ToolDefinition;\n+use fabro_types::{AgentToolCategory, AgentToolSource, AgentToolSummary};\n use tokio_util::sync::CancellationToken;\n \n use crate::config::{ToolAccessPolicy, ToolExposureMode};\n use crate::sandbox::Sandbox;\n use crate::session::ToolEnvProvider;\n+use crate::tool_permissions;\n use crate::types::AgentEvent;\n \n /// Narrow handle a tool uses to publish typed agent events (e.g. todo\n@@ -72,8 +74,13 @@ pub struct RegisteredTool {\n pub enum ToolSource {\n #[default]\n Native,\n+ /// `original_name` is the raw upstream MCP tool name (before the\n+ /// `mcp__<server>__` qualification applied by `fabro_mcp`). It is\n+ /// supplied by the MCP integration that registers the tool, so consumers\n+ /// never need to re-parse the qualified name.\n Mcp {\n- server_name: String,\n+ server_name: String,\n+ original_name: String,\n },\n Skill,\n }\n@@ -84,6 +91,39 @@ pub struct ToolDefinitionWithSource {\n pub source: ToolSource,\n }\n \n+impl ToolDefinitionWithSource {\n+ /// Project this tool into the public `AgentToolSummary` used by\n+ /// `StageProjection.agent_tools` and the `agent.tools.available` event.\n+ /// Drops the parameter schema; `invoked` defaults to `false` and is set\n+ /// by the projection reducer when matching `agent.tool.started` events\n+ /// replay.\n+ #[must_use]\n+ pub fn to_agent_tool_summary(&self) -> AgentToolSummary {\n+ AgentToolSummary {\n+ name: self.definition.name.clone(),\n+ description: self.definition.description.clone(),\n+ source: agent_tool_source(&self.source),\n+ category: tool_permissions::known_tool_category(&self.definition.name)\n+ .unwrap_or(AgentToolCategory::Other),\n+ invoked: false,\n+ }\n+ }\n+}\n+\n+fn agent_tool_source(source: &ToolSource) -> AgentToolSource {\n+ match source {\n+ ToolSource::Native => AgentToolSource::Native,\n+ ToolSource::Mcp {\n+ server_name,\n+ original_name,\n+ } => AgentToolSource::Mcp {\n+ server_name: server_name.clone(),\n+ original_name: original_name.clone(),\n+ },\n+ ToolSource::Skill => AgentToolSource::Skill,\n+ }\n+}\n+\n pub struct ToolRegistry {\n tools: HashMap<String, RegisteredTool>,\n }\n@@ -385,4 +425,59 @@ mod tests {\n assert!(registry.names().is_empty());\n assert!(registry.definitions().is_empty());\n }\n+\n+ fn tool_with_source(name: &str, source: ToolSource) -> ToolDefinitionWithSource {\n+ ToolDefinitionWithSource {\n+ definition: ToolDefinition {\n+ name: name.to_string(),\n+ description: format!(\"{name} description\"),\n+ parameters: serde_json::json!({\n+ \"type\": \"object\",\n+ \"properties\": { \"path\": { \"type\": \"string\" } }\n+ }),\n+ },\n+ source,\n+ }\n+ }\n+\n+ #[test]\n+ fn to_agent_tool_summary_maps_known_native_categories_and_drops_parameters() {\n+ let cases = [\n+ (\"apply_patch\", AgentToolCategory::Write),\n+ (\"grep\", AgentToolCategory::Read),\n+ (\"glob\", AgentToolCategory::Read),\n+ (\"spawn_agent\", AgentToolCategory::Subagent),\n+ (\"shell\", AgentToolCategory::Shell),\n+ (\"unknown_native\", AgentToolCategory::Other),\n+ ];\n+ for (name, expected) in cases {\n+ let summary = tool_with_source(name, ToolSource::Native).to_agent_tool_summary();\n+ assert_eq!(summary.name, name);\n+ assert_eq!(summary.description, format!(\"{name} description\"));\n+ assert_eq!(summary.source, AgentToolSource::Native);\n+ assert_eq!(summary.category, expected);\n+ assert!(!summary.invoked);\n+\n+ let json = serde_json::to_value(&summary).unwrap();\n+ assert!(\n+ json.as_object().unwrap().get(\"parameters\").is_none(),\n+ \"agent tool summaries must not include parameter schemas\"\n+ );\n+ }\n+ }\n+\n+ #[test]\n+ fn to_agent_tool_summary_carries_mcp_original_name_from_source() {\n+ let summary = tool_with_source(\"mcp__filesystem__read_file\", ToolSource::Mcp {\n+ server_name: \"filesystem\".to_string(),\n+ original_name: \"read_file\".to_string(),\n+ })\n+ .to_agent_tool_summary();\n+\n+ assert_eq!(summary.source, AgentToolSource::Mcp {\n+ server_name: \"filesystem\".to_string(),\n+ original_name: \"read_file\".to_string(),\n+ });\n+ assert_eq!(summary.category, AgentToolCategory::Other);\n+ }\n }\ndiff --git a/lib/crates/fabro-workflow/src/handler/llm/api.rs b/lib/crates/fabro-workflow/src/handler/llm/api.rs\nindex 135b5e3b7..8fcdf32c2 100644\n--- a/lib/crates/fabro-workflow/src/handler/llm/api.rs\n+++ b/lib/crates/fabro-workflow/src/handler/llm/api.rs\n@@ -3,13 +3,11 @@ use std::sync::{Arc, Mutex};\n \n use async_trait::async_trait;\n use fabro_agent::subagent::{SessionFactory, SubAgentManager};\n-use fabro_agent::tool_registry::{\n- RegisteredTool, ToolContext, ToolDefinitionWithSource, ToolRegistry, ToolSource,\n-};\n+use fabro_agent::tool_registry::{RegisteredTool, ToolContext, ToolRegistry, ToolSource};\n use fabro_agent::{\n AgentEvent, AgentProfile, AnthropicProfile, CompletionCoordinator, GeminiProfile,\n Message as AgentMessage, OpenAiProfile, Sandbox, Session, SessionOptions, StaticEnvProvider,\n- ToolEnvProvider, register_question_tools, tool_permissions,\n+ ToolEnvProvider, register_question_tools,\n };\n use fabro_auth::{CredentialSource, EnvCredentialSource};\n use fabro_graphviz::graph::{AttrValue, Node};\n@@ -23,10 +21,7 @@ use fabro_mcp::config::McpServerSettings;\n use fabro_model::catalog::LlmCatalogSettings;\n use fabro_model::{AgentProfileKind, Catalog, FallbackTarget, ModelRef, ProviderId};\n use fabro_types::settings::run::RunModelControls;\n-use fabro_types::{\n- AgentToolCategory, AgentToolSource, AgentToolSummary, PermissionLevel, RunId,\n- SessionCapability, StageId,\n-};\n+use fabro_types::{PermissionLevel, RunId, SessionCapability, StageId};\n use serde::de::DeserializeOwned;\n use tokio::sync::Mutex as TokioMutex;\n use tokio::task::JoinHandle;\n@@ -501,58 +496,17 @@ fn last_assistant_response(session: &Session) -> String {\n .unwrap_or_default()\n }\n \n-fn agent_tool_summaries_from_definitions(\n- tools: &[ToolDefinitionWithSource],\n-) -> Vec<AgentToolSummary> {\n- let mut summaries: Vec<_> = tools\n- .iter()\n- .map(|tool| AgentToolSummary {\n- name: tool.definition.name.clone(),\n- description: tool.definition.description.clone(),\n- source: agent_tool_source(&tool.definition.name, &tool.source),\n- category: agent_tool_category(&tool.definition.name),\n- invoked: false,\n- })\n- .collect();\n- summaries.sort_by(|left, right| left.name.cmp(&right.name));\n- summaries\n-}\n-\n-fn agent_tool_source(name: &str, source: &ToolSource) -> AgentToolSource {\n- match source {\n- ToolSource::Native => AgentToolSource::Native,\n- ToolSource::Mcp { server_name } => AgentToolSource::Mcp {\n- server_name: server_name.clone(),\n- original_name: fabro_mcp::connection_manager::parse_qualified_name(name)\n- .map(|(_, original_name)| original_name)\n- .unwrap_or_else(|| name.to_string()),\n- },\n- ToolSource::Skill => AgentToolSource::Skill,\n- }\n-}\n-\n-fn agent_tool_category(name: &str) -> AgentToolCategory {\n- match tool_permissions::known_tool_category(name) {\n- Some(\"read\") => AgentToolCategory::Read,\n- Some(\"write\") => AgentToolCategory::Write,\n- Some(\"shell\") => AgentToolCategory::Shell,\n- Some(\"subagent\") => AgentToolCategory::Subagent,\n- Some(_) | None => AgentToolCategory::Other,\n- }\n-}\n-\n fn emit_agent_tools_available(\n session: &Session,\n node_id: &str,\n stage_id: &StageId,\n emitter: &Arc<Emitter>,\n ) {\n- let tools = agent_tool_summaries_from_definitions(&session.available_tools());\n emitter.emit(&Event::AgentToolsAvailable {\n- node_id: node_id.to_string(),\n- visit: stage_id.visit(),\n+ node_id: node_id.to_string(),\n+ visit: stage_id.visit(),\n session_id: session.id().to_string(),\n- tools,\n+ tools: session.agent_tool_summaries(),\n });\n }\n \n@@ -1257,7 +1211,13 @@ impl CodergenBackend for AgentApiBackend {\n return Err(err);\n }\n }\n- emit_agent_tools_available(&session, &node.id, &stage_id, emitter);\n+ // Reused steerable sessions already emitted their effective\n+ // tool list on first activation; the registry, access policy,\n+ // and exposure mode are immutable for the session's lifetime,\n+ // so re-emitting on every subsequent prompt is wasted work.\n+ if !is_reused {\n+ emit_agent_tools_available(&session, &node.id, &stage_id, emitter);\n+ }\n session\n .process_input_with_runtime(prompt, agent_tool_runtime.clone())\n .await\n@@ -1562,7 +1522,6 @@ mod tests {\n \n use chrono::TimeZone;\n use fabro_agent::subagent::SessionFactory;\n- use fabro_agent::tool_registry::ToolDefinitionWithSource;\n use fabro_agent::{AgentProfile, LocalSandbox, ToolRegistry};\n use fabro_api::types;\n use fabro_auth::{EnvCredentialSource, VaultCredentialSource};\n@@ -1629,68 +1588,6 @@ mod tests {\n }\n }\n \n- fn test_tool_with_source(name: &str, source: ToolSource) -> ToolDefinitionWithSource {\n- ToolDefinitionWithSource {\n- definition: LlmToolDefinition {\n- name: name.to_string(),\n- description: format!(\"{name} description\"),\n- parameters: serde_json::json!({\n- \"type\": \"object\",\n- \"properties\": {\n- \"path\": { \"type\": \"string\" }\n- }\n- }),\n- },\n- source,\n- }\n- }\n-\n- #[test]\n- fn agent_tool_summaries_map_known_native_categories_without_schemas() {\n- let summaries = agent_tool_summaries_from_definitions(&[\n- test_tool_with_source(\"apply_patch\", ToolSource::Native),\n- test_tool_with_source(\"grep\", ToolSource::Native),\n- test_tool_with_source(\"glob\", ToolSource::Native),\n- test_tool_with_source(\"spawn_agent\", ToolSource::Native),\n- test_tool_with_source(\"unknown_native\", ToolSource::Native),\n- ]);\n-\n- assert_eq!(summaries[0].name, \"apply_patch\");\n- assert_eq!(summaries[0].description, \"apply_patch description\");\n- assert_eq!(summaries[0].source, fabro_types::AgentToolSource::Native);\n- assert_eq!(summaries[0].category, fabro_types::AgentToolCategory::Write);\n- assert!(!summaries[0].invoked);\n- assert_eq!(summaries[1].category, fabro_types::AgentToolCategory::Read);\n- assert_eq!(summaries[2].category, fabro_types::AgentToolCategory::Read);\n- assert_eq!(\n- summaries[3].category,\n- fabro_types::AgentToolCategory::Subagent\n- );\n- assert_eq!(summaries[4].category, fabro_types::AgentToolCategory::Other);\n-\n- let json = serde_json::to_value(&summaries[0]).unwrap();\n- assert!(\n- json.as_object().unwrap().get(\"parameters\").is_none(),\n- \"agent tool summaries should not include tool parameter schemas\"\n- );\n- }\n-\n- #[test]\n- fn agent_tool_summaries_map_mcp_source_and_original_name_from_qualified_name() {\n- let summaries = agent_tool_summaries_from_definitions(&[test_tool_with_source(\n- \"mcp__filesystem__read_file\",\n- ToolSource::Mcp {\n- server_name: \"filesystem\".to_string(),\n- },\n- )]);\n-\n- assert_eq!(summaries[0].source, fabro_types::AgentToolSource::Mcp {\n- server_name: \"filesystem\".to_string(),\n- original_name: \"read_file\".to_string(),\n- });\n- assert_eq!(summaries[0].category, fabro_types::AgentToolCategory::Other);\n- }\n-\n struct ShutdownTestProvider;\n \n #[async_trait]\n",
"summary": {
"files_changed": 33,
"additions": 1187,
"deletions": 79
}
}
},
{
"seq": 2697,
"checkpoint": {
"timestamp": "2026-05-24T18:01:10.173175Z",
"current_node": "simplify_gpt",
"completed_nodes": [
"start",
"toolchain",
"preflight_compile",
"preflight_lint",
"implement",
"simplify_opus",
"simplify_gpt"
],
"node_retries": {},
"context_values": {
"internal.run_id": "01KSDFKD9JJS7D2ZJ6JQYXV58Z",
"thread.preflight_compile.current_node": "preflight_lint",
"internal.retry_count.simplify_opus": 0,
"internal.fidelity": "compact",
"failure_signature": "",
"internal.retry_count.start": 0,
"current_node": "simplify_gpt",
"last_response": "Reviewed the feature branch diff against `origin/main...HEAD` and ran the requested parallel review pass. I didnt find any actionable reuse/quality/efficiency issues that warranted code changes, so",
"last_stage": "simplify_gpt",
"response.implement": "The TS generator is pinned now, but this sandbox lacks Java, which OpenAPI Generator needs. Ill install a headless JRE and rerun generation.",
"thread.preflight_lint.current_node": "implement",
"thread.start.current_node": "toolchain",
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
"internal.retry_count.preflight_compile": 0,
"graph.goal": "---\ntitle: feat: StageProjection agent tools API\ntype: feat\nstatus: active\ndate: 2026-05-24\n---\n\n# feat: StageProjection Agent Tools API\n\n## Overview\n\nExpose the complete effective tool list for each agent-backed stage through `StageProjection`, so UI and API consumers can show actual tools such as `apply_patch`, `grep`, `glob`, `read_file`, MCP tools, skill tools, and subagent tools without inferring them from `permission_level`.\n\nThe API should expose tool summaries with `name`, `description`, `source`, `category`, and `invoked`. It must not expose full JSON parameter schemas in the run projection.\n\n## Problem Frame\n\nThe stage sidebar currently has permission metadata such as \"Full access\", but `permission_level` is only an access mode. It does not tell consumers which tools were actually exposed to the model after provider profile setup, optional tool registration, MCP integration, and tool access policy filtering.\n\nThe authoritative list already exists at request-build time in the agent session registry. The API needs to capture that effective list once per stage session and project it onto the stage.\n\n## Requirements Trace\n\n- R1. Add a StageProjection API field containing the complete effective tools for a stage session.\n- R2. Include `name`, `description`, `source`, `category`, and `invoked` for each tool.\n- R3. Do not infer tools from `permission_level` in backend or frontend code.\n- R4. Do not expose full tool parameter schemas through StageProjection.\n- R5. Preserve existing `permission_level` and `mcp_servers` fields for compatibility.\n- R6. Mark individual tools as invoked when matching `agent.tool.started` events are projected.\n- R7. Keep legacy runs backward compatible by defaulting missing tool lists to empty.\n\n## Scope Boundaries\n\n- Do not remove or rename `StageProjection.permission_level`.\n- Do not remove the existing MCP server projection or `AgentMcpToolSummary`.\n- Do not change completion API tool definitions.\n- Do not add ACP-native tool discovery in this pass unless an ACP path already has an equivalent effective tool list available.\n- Do not render parameter schemas in the web UI.\n\n## Context & Research\n\n### Relevant Code and Patterns\n\n- OpenAPI is the source of truth for HTTP contracts in `docs/public/api-reference/fabro-api.yaml`.\n- Shared API/projection DTOs should live in `fabro-types`, with `fabro-api/build.rs` replacements to avoid duplicate generated Rust types.\n- `StageProjection` lives in `lib/crates/fabro-types/src/run_projection.rs`.\n- Durable run event props live in `lib/crates/fabro-types/src/run_event/agent.rs` and `lib/crates/fabro-types/src/run_event/mod.rs`.\n- Workflow event conversion and event names live in `lib/crates/fabro-workflow/src/event/convert.rs`, `events.rs`, and `names.rs`.\n- Projection replay lives in `lib/crates/fabro-store/src/run_state.rs`.\n- The effective request tool list is built in `lib/crates/fabro-agent/src/session.rs` from `ToolRegistry::definitions_with_source_for_policy`.\n- Tool source metadata already exists as `ToolSource` and `ToolDefinitionWithSource` in `lib/crates/fabro-agent/src/tool_registry.rs`.\n- Tool category mapping already exists in `lib/crates/fabro-agent/src/tool_permissions.rs`.\n- The sidebar display lives in `apps/fabro-web/app/components/stage-insights-sidebar.tsx`.\n\n### Strategy Docs\n\n- Read `docs/internal/events-strategy.md` before adding the new durable event.\n- Read `docs/internal/testing-strategy.md` before adding or reorganizing tests.\n- Follow the OpenAPI type ownership guidance in `AGENTS.md`: reuse `fabro-types` through `fabro-api/build.rs` replacements when the API schema has the same product meaning and serde shape.\n\n## Key Technical Decisions\n\n- Add a new StageProjection field named `agent_tools`, not `tools`, to avoid ambiguity with MCP nested tools and completion tool definitions.\n- Add a dedicated durable event named `agent.tools.available` instead of overloading `agent.session.activated`.\n- Capture the effective tool list after session setup and filtering, using the same path as model request construction.\n- Store descriptions in the summary because they are useful API/UI metadata; omit parameter schemas to keep projection payloads small and avoid leaking full implementation detail.\n- Keep `AgentMcpToolSummary` MCP-only. Add a new general-purpose `AgentToolSummary` instead of stretching the MCP type beyond its meaning.\n- Treat `invoked` as projected state. The availability event should emit tools with `invoked: false`; replay of `agent.tool.started` flips matching tools to true.\n\n## API Contract\n\nAdd these schemas to OpenAPI and map them to `fabro_types` replacements:\n\n- `AgentToolSummary`\n - required: `name`, `description`, `source`, `category`, `invoked`\n - `name`: exposed tool name, e.g. `apply_patch` or `mcp__filesystem__read_file`\n - `description`: model-facing tool description\n - `source`: `AgentToolSource`\n - `category`: `AgentToolCategory`\n - `invoked`: boolean\n- `AgentToolSource`\n - tagged by `kind`\n - `native`\n - `mcp` with `server_name` and `original_name`\n - `skill`\n- `AgentToolCategory`\n - enum: `read`, `write`, `shell`, `subagent`, `other`\n- `AgentToolsAvailableProps`\n - required: `tools`, `visit`\n - `tools`: array of `AgentToolSummary`\n - `visit`: stage visit number\n\nAdd to `StageProjection`:\n\n- `agent_tools`: array of `AgentToolSummary`\n- Default to an empty array when omitted.\n- Skip serializing when empty, matching existing projection optional-list style.\n\nAdd event body:\n\n- Serialized event name: `agent.tools.available`\n- Event body type: `AgentToolsAvailableProps`\n\n## Implementation Units\n\n- [ ] **Unit 1: Add shared tool summary types**\n\n**Goal:** Define the canonical API/projection DTOs in `fabro-types`.\n\n**Files:**\n- Modify: `lib/crates/fabro-types/src/run_event/agent.rs`\n- Modify: `lib/crates/fabro-types/src/run_event/mod.rs`\n- Modify: `lib/crates/fabro-types/src/run_projection.rs`\n- Modify: `lib/crates/fabro-types/src/lib.rs`\n\n**Work:**\n- Add `AgentToolSummary`, `AgentToolSource`, `AgentToolCategory`, and `AgentToolsAvailableProps`.\n- Add `EventBody::AgentToolsAvailable` serialized as `agent.tools.available`.\n- Add `agent_tools: Vec<AgentToolSummary>` to `StageProjection`.\n- Ensure all new fields default cleanly for older persisted events/projections.\n- Export the new public types from `fabro-types`.\n\n- [ ] **Unit 2: Capture effective session tools**\n\n**Goal:** Provide an authoritative one-time source for the list that the model can actually call.\n\n**Files:**\n- Modify: `lib/crates/fabro-agent/src/session.rs`\n- Modify: `lib/crates/fabro-workflow/src/handler/llm/api.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/events.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/names.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/convert.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/stored_fields.rs` only if the new event needs non-standard stored fields.\n\n**Work:**\n- Add `Session::available_tools()` that returns the same effective `ToolDefinitionWithSource` list as `build_request()`.\n- Map `ToolDefinitionWithSource` to `AgentToolSummary` at the workflow boundary.\n- Populate `description` from `ToolDefinition.description`.\n- Populate `source` from `ToolSource`.\n- For MCP tools, include the server name from `ToolSource::Mcp` and derive `original_name` from the qualified exposed name using the existing MCP naming convention.\n- Populate `category` from the existing tool category mapping for known exposed names; use `other` when no category mapping applies.\n- Emit `agent.tools.available` once for the stage session after session setup and filtering are complete.\n\n- [ ] **Unit 3: Project available and invoked tools**\n\n**Goal:** Make `StageProjection.agent_tools` replay-authoritative.\n\n**Files:**\n- Modify: `lib/crates/fabro-store/src/run_state.rs`\n\n**Work:**\n- On `EventBody::AgentToolsAvailable`, replace the current stage visit's `agent_tools` with the event tools.\n- On `EventBody::AgentToolStarted`, find a matching `agent_tools` entry by exposed `tool_name` and set `invoked = true`.\n- Keep the existing MCP server `invoked` update unchanged.\n- If a legacy run has no availability event, do not synthesize a full list from permissions.\n\n- [ ] **Unit 4: Update OpenAPI and generated clients**\n\n**Goal:** Expose the new projection field and event contract through public API clients without duplicate Rust API types.\n\n**Files:**\n- Modify: `docs/public/api-reference/fabro-api.yaml`\n- Modify: `lib/crates/fabro-api/build.rs`\n- Modify: `lib/crates/fabro-api/src/lib.rs`\n- Modify generated files under `lib/crates/fabro-api/src/generated.rs` via `cargo build -p fabro-api`.\n- Modify generated files under `lib/packages/fabro-api-client/src` via TypeScript client generation.\n\n**Work:**\n- Add the OpenAPI schemas listed in the API Contract section.\n- Add `StageProjection.agent_tools`.\n- Add `fabro-api/build.rs` replacements for the new `fabro-types` types.\n- Regenerate Rust API code.\n- Regenerate TypeScript API client code.\n\n- [ ] **Unit 5: Render tools in the web sidebar**\n\n**Goal:** Replace the permission-derived sidebar display with the actual projected tool list.\n\n**Files:**\n- Modify: `apps/fabro-web/app/components/stage-insights-sidebar.tsx`\n- Modify: `apps/fabro-web/app/components/stage-insights-sidebar.test.tsx`\n\n**Work:**\n- Render `stage.agent_tools` when present.\n- Show each tool's name, description, source/category, and invoked state.\n- Keep permission level as secondary metadata or fallback for legacy stages with no `agent_tools`.\n- Do not infer tool availability from permission level.\n\n## Test Plan\n\n- `fabro-types`\n - JSON round-trip for `agent.tools.available`.\n - Serialization checks for `AgentToolSource` and `AgentToolCategory`.\n - Backward compatibility check that missing `agent_tools` deserializes to an empty list.\n\n- `fabro-workflow`\n - Event name and conversion tests for `agent.tools.available`.\n - Capture test proving native tools such as `apply_patch`, `grep`, and `glob` are emitted from the effective registry path.\n - MCP source mapping test for a qualified MCP tool name.\n\n- `fabro-store`\n - Projection test that `agent.tools.available` populates `StageProjection.agent_tools`.\n - Projection test that `agent.tool.started` marks only the matching tool as invoked.\n - Regression test that MCP server `invoked` status still updates as before.\n\n- `fabro-api`\n - Type identity/parity tests confirming API types reuse `fabro_types::AgentToolSummary`, `AgentToolSource`, `AgentToolCategory`, and `AgentToolsAvailableProps`.\n - StageProjection round-trip test including `agent_tools`.\n\n- `fabro-web`\n - Sidebar test rendering tool names and descriptions from `stage.agent_tools`.\n - Sidebar test showing invoked state.\n - Legacy fallback test for stages without `agent_tools`.\n\n## Run Checks\n\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo nextest run -p fabro-types -p fabro-workflow -p fabro-store -p fabro-api`\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cd apps/fabro-web && bun test && bun run typecheck`\n\n## Assumptions\n\n- The first implementation targets normal API-backed agent sessions, not ACP-native sessions.\n- `description` is safe to expose because it is already model-facing tool metadata, but parameter schemas remain out of scope for StageProjection.\n- `agent_tools` is a complete list only for runs that emit `agent.tools.available`; legacy runs return an empty list and may still show existing permission/MCP metadata.\n- If tool registration becomes mutable later, the event contract can be re-emitted and projection replacement semantics will still work.\n",
"response.simplify_opus": "## Summary\n\nAggregated three review agents' findings and applied the consensus high-value fixes:\n\n**Quality / reuse fixes:**\n- `tool_permissions::known_tool_category` now returns `Option<AgentToolCategory>` directly. Deleted the brittle `Some(\"read\")` → `Read` string match in `handler/llm/api.rs`. `is_auto_approved` and `tool_category` are now enum-typed end-to-end; cli.rs and its tests updated accordingly. Adding a new category is now a compile-time exhaustiveness error instead of a silent `Other`.\n- `ToolSource::Mcp` now carries `original_name` populated from `info.original_tool_name` in `make_mcp_tools`. Workflow code no longer re-parses qualified MCP names (eliminates the silent fallback bug where a malformed prefix would store the qualified name as \"original\").\n- Moved `agent_tool_source` + `agent_tool_summaries_from_definitions` out of `handler/llm/api.rs`. New `ToolDefinitionWithSource::to_agent_tool_summary()` + `Session::agent_tool_summaries()` colocate the conversion next to the source type. Tests moved to `tool_registry.rs`.\n- Collapsed `Session::available_tools` and the duplicate private `effective_tools` into one public `effective_tools()` method.\n- Merged `ToolAvailableIcon` and `TodoPendingIcon` into a single `EmptyCircleIcon` component in the sidebar.\n\n**Efficiency fix:**\n- Gated `emit_agent_tools_available` on `!is_reused` in the primary path. Reused steerable sessions had been re-emitting the full tool list (with descriptions cloned into the event payload) on every prompt even though registry, access policy, and exposure mode are immutable for the session's lifetime. Failover path keeps its emit since it always builds a fresh session.\n\n**Skipped (cost > benefit):** `mcp_server_from_tool_name` dedup in `fabro-store` would require pulling `fabro-mcp` (with `rmcp`) into the store crate or relocating the parse helper into `fabro-types`. The duplicate is 5 lines using the same delimiter convention — not worth the dependency surface.\n\n**Verification:** `cargo check --workspace --all-targets`, `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`, `cargo +nightly-2026-04-14 fmt --check --all`, `cargo nextest run -p fabro-types -p fabro-workflow -p fabro-store -p fabro-api -p fabro-agent` (2250+464 tests), `bun test stage-insights-sidebar` (10/10), and `bun run typecheck` all pass. The 14 unrelated `run-actions.test.ts` failures pre-exist on the parent commit.",
"thread.toolchain.current_node": "preflight_compile",
"internal.retry_count.simplify_gpt": 0,
"outcome": "succeeded",
"internal.work_dir": "/home/daytona/workspace/fabro",
"internal.retry_count.implement": 0,
"response.simplify_gpt": "Reviewed the feature branch diff against `origin/main...HEAD` and ran the requested parallel review pass. I didnt find any actionable reuse/quality/efficiency issues that warranted code changes, so the working tree remains clean.\n\nValidation run:\n\n- `cargo nextest run -p fabro-types -p fabro-store -p fabro-agent -p fabro-workflow -p fabro-api agent_tools_available stage_projection_round_trip to_agent_tool_summary effective_tools --no-tests=pass` ✅\n- `cd apps/fabro-web && bun test stage-insights-sidebar.test.tsx` ✅\n- `cargo +nightly-2026-04-14 fmt --check --all` ✅\n- `cd apps/fabro-web && bun run typecheck` ✅\n- `git status --short` clean ✅",
"internal.node_visit_count": 1,
"thread.implement.current_node": "simplify_opus",
"failure_class": "",
"internal.retry_count.preflight_lint": 0,
"internal.thread_id": "simplify_opus",
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"graph.rankdir": "LR",
"internal.retry_count.toolchain": 0,
"thread.simplify_opus.current_node": "simplify_gpt"
},
"node_outcomes": {
"preflight_compile": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
},
"notes": "Script completed: cargo check -q --workspace 2>&1",
"usage": null
},
"simplify_gpt": {
"status": "succeeded",
"context_updates": {
"last_response": "Reviewed the feature branch diff against `origin/main...HEAD` and ran the requested parallel review pass. I didnt find any actionable reuse/quality/efficiency issues that warranted code changes, so",
"last_stage": "simplify_gpt",
"response.simplify_gpt": "Reviewed the feature branch diff against `origin/main...HEAD` and ran the requested parallel review pass. I didnt find any actionable reuse/quality/efficiency issues that warranted code changes, so the working tree remains clean.\n\nValidation run:\n\n- `cargo nextest run -p fabro-types -p fabro-store -p fabro-agent -p fabro-workflow -p fabro-api agent_tools_available stage_projection_round_trip to_agent_tool_summary effective_tools --no-tests=pass` ✅\n- `cd apps/fabro-web && bun test stage-insights-sidebar.test.tsx` ✅\n- `cargo +nightly-2026-04-14 fmt --check --all` ✅\n- `cd apps/fabro-web && bun run typecheck` ✅\n- `git status --short` clean ✅"
},
"notes": "Stage completed: simplify_gpt",
"usage": {
"input": {
"usage": {
"model": {
"provider": "openai",
"model_id": "gpt-5.5"
},
"tokens": {
"input_tokens": 1008114,
"output_tokens": 3387,
"reasoning_tokens": 2181,
"cache_read_tokens": 569856,
"cache_write_tokens": 0
}
},
"facts": {
"algorithm": "openai"
}
},
"total_usd_micros": 5492538
}
},
"preflight_lint": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
},
"notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
"usage": null
},
"toolchain": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
},
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
"usage": null
},
"simplify_opus": {
"status": "succeeded",
"context_updates": {
"response.simplify_opus": "## Summary\n\nAggregated three review agents' findings and applied the consensus high-value fixes:\n\n**Quality / reuse fixes:**\n- `tool_permissions::known_tool_category` now returns `Option<AgentToolCategory>` directly. Deleted the brittle `Some(\"read\")` → `Read` string match in `handler/llm/api.rs`. `is_auto_approved` and `tool_category` are now enum-typed end-to-end; cli.rs and its tests updated accordingly. Adding a new category is now a compile-time exhaustiveness error instead of a silent `Other`.\n- `ToolSource::Mcp` now carries `original_name` populated from `info.original_tool_name` in `make_mcp_tools`. Workflow code no longer re-parses qualified MCP names (eliminates the silent fallback bug where a malformed prefix would store the qualified name as \"original\").\n- Moved `agent_tool_source` + `agent_tool_summaries_from_definitions` out of `handler/llm/api.rs`. New `ToolDefinitionWithSource::to_agent_tool_summary()` + `Session::agent_tool_summaries()` colocate the conversion next to the source type. Tests moved to `tool_registry.rs`.\n- Collapsed `Session::available_tools` and the duplicate private `effective_tools` into one public `effective_tools()` method.\n- Merged `ToolAvailableIcon` and `TodoPendingIcon` into a single `EmptyCircleIcon` component in the sidebar.\n\n**Efficiency fix:**\n- Gated `emit_agent_tools_available` on `!is_reused` in the primary path. Reused steerable sessions had been re-emitting the full tool list (with descriptions cloned into the event payload) on every prompt even though registry, access policy, and exposure mode are immutable for the session's lifetime. Failover path keeps its emit since it always builds a fresh session.\n\n**Skipped (cost > benefit):** `mcp_server_from_tool_name` dedup in `fabro-store` would require pulling `fabro-mcp` (with `rmcp`) into the store crate or relocating the parse helper into `fabro-types`. The duplicate is 5 lines using the same delimiter convention — not worth the dependency surface.\n\n**Verification:** `cargo check --workspace --all-targets`, `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`, `cargo +nightly-2026-04-14 fmt --check --all`, `cargo nextest run -p fabro-types -p fabro-workflow -p fabro-store -p fabro-api -p fabro-agent` (2250+464 tests), `bun test stage-insights-sidebar` (10/10), and `bun run typecheck` all pass. The 14 unrelated `run-actions.test.ts` failures pre-exist on the parent commit.",
"last_stage": "simplify_opus",
"last_response": "## Summary\n\nAggregated three review agents' findings and applied the consensus high-value fixes:\n\n**Quality / reuse fixes:**\n- `tool_permissions::known_tool_category` now returns `Option<AgentToolCate"
},
"notes": "Stage completed: simplify_opus",
"usage": {
"input": {
"usage": {
"model": {
"provider": "anthropic",
"model_id": "claude-opus-4-7"
},
"tokens": {
"input_tokens": 113693,
"output_tokens": 34970,
"reasoning_tokens": 0,
"cache_read_tokens": 9248300,
"cache_write_tokens": 728711
}
},
"facts": {
"algorithm": "anthropic",
"cache_write_5m_tokens": 728711,
"cache_write_1h_tokens": 0
}
},
"total_usd_micros": 10621308
},
"files_touched": [
"/home/daytona/workspace/fabro/apps/fabro-web/app/components/stage-insights-sidebar.tsx",
"/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/cli.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/context_window.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/mcp_integration.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/session.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/tool_permissions.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/tool_registry.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/handler/llm/api.rs"
]
},
"start": {
"status": "succeeded",
"usage": null
},
"implement": {
"status": "succeeded",
"context_updates": {
"last_stage": "implement",
"last_response": "The TS generator is pinned now, but this sandbox lacks Java, which OpenAPI Generator needs. Ill install a headless JRE and rerun generation.",
"response.implement": "The TS generator is pinned now, but this sandbox lacks Java, which OpenAPI Generator needs. Ill install a headless JRE and rerun generation."
},
"notes": "Stage completed: implement",
"usage": {
"input": {
"usage": {
"model": {
"provider": "openai",
"model_id": "gpt-5.5"
},
"tokens": {
"input_tokens": 6711598,
"output_tokens": 22676,
"reasoning_tokens": 11522,
"cache_read_tokens": 16107008,
"cache_write_tokens": 0
}
},
"facts": {
"algorithm": "openai"
}
},
"total_usd_micros": 42637434
}
}
},
"next_node_id": "verify",
"git_commit_sha": "ed24c71d3dee09708f63d3fd17cfc6eeaaf8235b",
"node_visits": {
"simplify_opus": 1,
"simplify_gpt": 1,
"preflight_lint": 1,
"start": 1,
"toolchain": 1,
"implement": 1,
"preflight_compile": 1
}
},
"diff": {
"summary": {
"files_changed": 33,
"additions": 1187,
"deletions": 79
}
}
},
{
"seq": 0,
"checkpoint": {
"timestamp": "2026-05-24T18:09:44.086717Z",
"current_node": "verify",
"completed_nodes": [
"start",
"toolchain",
"preflight_compile",
"preflight_lint",
"implement",
"simplify_opus",
"simplify_gpt",
"verify"
],
"node_retries": {},
"context_values": {
"graph.rankdir": "LR",
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
"internal.retry_count.start": 0,
"internal.fidelity": "compact",
"failure_signature": "",
"internal.retry_count.toolchain": 0,
"internal.retry_count.preflight_compile": 0,
"thread.simplify_gpt.current_node": "verify",
"thread.implement.current_node": "simplify_opus",
"internal.retry_count.simplify_gpt": 0,
"outcome": "succeeded",
"internal.retry_count.verify": 0,
"failure_class": "",
"response.simplify_opus": "## Summary\n\nAggregated three review agents' findings and applied the consensus high-value fixes:\n\n**Quality / reuse fixes:**\n- `tool_permissions::known_tool_category` now returns `Option<AgentToolCategory>` directly. Deleted the brittle `Some(\"read\")` → `Read` string match in `handler/llm/api.rs`. `is_auto_approved` and `tool_category` are now enum-typed end-to-end; cli.rs and its tests updated accordingly. Adding a new category is now a compile-time exhaustiveness error instead of a silent `Other`.\n- `ToolSource::Mcp` now carries `original_name` populated from `info.original_tool_name` in `make_mcp_tools`. Workflow code no longer re-parses qualified MCP names (eliminates the silent fallback bug where a malformed prefix would store the qualified name as \"original\").\n- Moved `agent_tool_source` + `agent_tool_summaries_from_definitions` out of `handler/llm/api.rs`. New `ToolDefinitionWithSource::to_agent_tool_summary()` + `Session::agent_tool_summaries()` colocate the conversion next to the source type. Tests moved to `tool_registry.rs`.\n- Collapsed `Session::available_tools` and the duplicate private `effective_tools` into one public `effective_tools()` method.\n- Merged `ToolAvailableIcon` and `TodoPendingIcon` into a single `EmptyCircleIcon` component in the sidebar.\n\n**Efficiency fix:**\n- Gated `emit_agent_tools_available` on `!is_reused` in the primary path. Reused steerable sessions had been re-emitting the full tool list (with descriptions cloned into the event payload) on every prompt even though registry, access policy, and exposure mode are immutable for the session's lifetime. Failover path keeps its emit since it always builds a fresh session.\n\n**Skipped (cost > benefit):** `mcp_server_from_tool_name` dedup in `fabro-store` would require pulling `fabro-mcp` (with `rmcp`) into the store crate or relocating the parse helper into `fabro-types`. The duplicate is 5 lines using the same delimiter convention — not worth the dependency surface.\n\n**Verification:** `cargo check --workspace --all-targets`, `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`, `cargo +nightly-2026-04-14 fmt --check --all`, `cargo nextest run -p fabro-types -p fabro-workflow -p fabro-store -p fabro-api -p fabro-agent` (2250+464 tests), `bun test stage-insights-sidebar` (10/10), and `bun run typecheck` all pass. The 14 unrelated `run-actions.test.ts` failures pre-exist on the parent commit.",
"internal.retry_count.simplify_opus": 0,
"response.implement": "The TS generator is pinned now, but this sandbox lacks Java, which OpenAPI Generator needs. Ill install a headless JRE and rerun generation.",
"internal.retry_count.preflight_lint": 0,
"graph.goal": "---\ntitle: feat: StageProjection agent tools API\ntype: feat\nstatus: active\ndate: 2026-05-24\n---\n\n# feat: StageProjection Agent Tools API\n\n## Overview\n\nExpose the complete effective tool list for each agent-backed stage through `StageProjection`, so UI and API consumers can show actual tools such as `apply_patch`, `grep`, `glob`, `read_file`, MCP tools, skill tools, and subagent tools without inferring them from `permission_level`.\n\nThe API should expose tool summaries with `name`, `description`, `source`, `category`, and `invoked`. It must not expose full JSON parameter schemas in the run projection.\n\n## Problem Frame\n\nThe stage sidebar currently has permission metadata such as \"Full access\", but `permission_level` is only an access mode. It does not tell consumers which tools were actually exposed to the model after provider profile setup, optional tool registration, MCP integration, and tool access policy filtering.\n\nThe authoritative list already exists at request-build time in the agent session registry. The API needs to capture that effective list once per stage session and project it onto the stage.\n\n## Requirements Trace\n\n- R1. Add a StageProjection API field containing the complete effective tools for a stage session.\n- R2. Include `name`, `description`, `source`, `category`, and `invoked` for each tool.\n- R3. Do not infer tools from `permission_level` in backend or frontend code.\n- R4. Do not expose full tool parameter schemas through StageProjection.\n- R5. Preserve existing `permission_level` and `mcp_servers` fields for compatibility.\n- R6. Mark individual tools as invoked when matching `agent.tool.started` events are projected.\n- R7. Keep legacy runs backward compatible by defaulting missing tool lists to empty.\n\n## Scope Boundaries\n\n- Do not remove or rename `StageProjection.permission_level`.\n- Do not remove the existing MCP server projection or `AgentMcpToolSummary`.\n- Do not change completion API tool definitions.\n- Do not add ACP-native tool discovery in this pass unless an ACP path already has an equivalent effective tool list available.\n- Do not render parameter schemas in the web UI.\n\n## Context & Research\n\n### Relevant Code and Patterns\n\n- OpenAPI is the source of truth for HTTP contracts in `docs/public/api-reference/fabro-api.yaml`.\n- Shared API/projection DTOs should live in `fabro-types`, with `fabro-api/build.rs` replacements to avoid duplicate generated Rust types.\n- `StageProjection` lives in `lib/crates/fabro-types/src/run_projection.rs`.\n- Durable run event props live in `lib/crates/fabro-types/src/run_event/agent.rs` and `lib/crates/fabro-types/src/run_event/mod.rs`.\n- Workflow event conversion and event names live in `lib/crates/fabro-workflow/src/event/convert.rs`, `events.rs`, and `names.rs`.\n- Projection replay lives in `lib/crates/fabro-store/src/run_state.rs`.\n- The effective request tool list is built in `lib/crates/fabro-agent/src/session.rs` from `ToolRegistry::definitions_with_source_for_policy`.\n- Tool source metadata already exists as `ToolSource` and `ToolDefinitionWithSource` in `lib/crates/fabro-agent/src/tool_registry.rs`.\n- Tool category mapping already exists in `lib/crates/fabro-agent/src/tool_permissions.rs`.\n- The sidebar display lives in `apps/fabro-web/app/components/stage-insights-sidebar.tsx`.\n\n### Strategy Docs\n\n- Read `docs/internal/events-strategy.md` before adding the new durable event.\n- Read `docs/internal/testing-strategy.md` before adding or reorganizing tests.\n- Follow the OpenAPI type ownership guidance in `AGENTS.md`: reuse `fabro-types` through `fabro-api/build.rs` replacements when the API schema has the same product meaning and serde shape.\n\n## Key Technical Decisions\n\n- Add a new StageProjection field named `agent_tools`, not `tools`, to avoid ambiguity with MCP nested tools and completion tool definitions.\n- Add a dedicated durable event named `agent.tools.available` instead of overloading `agent.session.activated`.\n- Capture the effective tool list after session setup and filtering, using the same path as model request construction.\n- Store descriptions in the summary because they are useful API/UI metadata; omit parameter schemas to keep projection payloads small and avoid leaking full implementation detail.\n- Keep `AgentMcpToolSummary` MCP-only. Add a new general-purpose `AgentToolSummary` instead of stretching the MCP type beyond its meaning.\n- Treat `invoked` as projected state. The availability event should emit tools with `invoked: false`; replay of `agent.tool.started` flips matching tools to true.\n\n## API Contract\n\nAdd these schemas to OpenAPI and map them to `fabro_types` replacements:\n\n- `AgentToolSummary`\n - required: `name`, `description`, `source`, `category`, `invoked`\n - `name`: exposed tool name, e.g. `apply_patch` or `mcp__filesystem__read_file`\n - `description`: model-facing tool description\n - `source`: `AgentToolSource`\n - `category`: `AgentToolCategory`\n - `invoked`: boolean\n- `AgentToolSource`\n - tagged by `kind`\n - `native`\n - `mcp` with `server_name` and `original_name`\n - `skill`\n- `AgentToolCategory`\n - enum: `read`, `write`, `shell`, `subagent`, `other`\n- `AgentToolsAvailableProps`\n - required: `tools`, `visit`\n - `tools`: array of `AgentToolSummary`\n - `visit`: stage visit number\n\nAdd to `StageProjection`:\n\n- `agent_tools`: array of `AgentToolSummary`\n- Default to an empty array when omitted.\n- Skip serializing when empty, matching existing projection optional-list style.\n\nAdd event body:\n\n- Serialized event name: `agent.tools.available`\n- Event body type: `AgentToolsAvailableProps`\n\n## Implementation Units\n\n- [ ] **Unit 1: Add shared tool summary types**\n\n**Goal:** Define the canonical API/projection DTOs in `fabro-types`.\n\n**Files:**\n- Modify: `lib/crates/fabro-types/src/run_event/agent.rs`\n- Modify: `lib/crates/fabro-types/src/run_event/mod.rs`\n- Modify: `lib/crates/fabro-types/src/run_projection.rs`\n- Modify: `lib/crates/fabro-types/src/lib.rs`\n\n**Work:**\n- Add `AgentToolSummary`, `AgentToolSource`, `AgentToolCategory`, and `AgentToolsAvailableProps`.\n- Add `EventBody::AgentToolsAvailable` serialized as `agent.tools.available`.\n- Add `agent_tools: Vec<AgentToolSummary>` to `StageProjection`.\n- Ensure all new fields default cleanly for older persisted events/projections.\n- Export the new public types from `fabro-types`.\n\n- [ ] **Unit 2: Capture effective session tools**\n\n**Goal:** Provide an authoritative one-time source for the list that the model can actually call.\n\n**Files:**\n- Modify: `lib/crates/fabro-agent/src/session.rs`\n- Modify: `lib/crates/fabro-workflow/src/handler/llm/api.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/events.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/names.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/convert.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/stored_fields.rs` only if the new event needs non-standard stored fields.\n\n**Work:**\n- Add `Session::available_tools()` that returns the same effective `ToolDefinitionWithSource` list as `build_request()`.\n- Map `ToolDefinitionWithSource` to `AgentToolSummary` at the workflow boundary.\n- Populate `description` from `ToolDefinition.description`.\n- Populate `source` from `ToolSource`.\n- For MCP tools, include the server name from `ToolSource::Mcp` and derive `original_name` from the qualified exposed name using the existing MCP naming convention.\n- Populate `category` from the existing tool category mapping for known exposed names; use `other` when no category mapping applies.\n- Emit `agent.tools.available` once for the stage session after session setup and filtering are complete.\n\n- [ ] **Unit 3: Project available and invoked tools**\n\n**Goal:** Make `StageProjection.agent_tools` replay-authoritative.\n\n**Files:**\n- Modify: `lib/crates/fabro-store/src/run_state.rs`\n\n**Work:**\n- On `EventBody::AgentToolsAvailable`, replace the current stage visit's `agent_tools` with the event tools.\n- On `EventBody::AgentToolStarted`, find a matching `agent_tools` entry by exposed `tool_name` and set `invoked = true`.\n- Keep the existing MCP server `invoked` update unchanged.\n- If a legacy run has no availability event, do not synthesize a full list from permissions.\n\n- [ ] **Unit 4: Update OpenAPI and generated clients**\n\n**Goal:** Expose the new projection field and event contract through public API clients without duplicate Rust API types.\n\n**Files:**\n- Modify: `docs/public/api-reference/fabro-api.yaml`\n- Modify: `lib/crates/fabro-api/build.rs`\n- Modify: `lib/crates/fabro-api/src/lib.rs`\n- Modify generated files under `lib/crates/fabro-api/src/generated.rs` via `cargo build -p fabro-api`.\n- Modify generated files under `lib/packages/fabro-api-client/src` via TypeScript client generation.\n\n**Work:**\n- Add the OpenAPI schemas listed in the API Contract section.\n- Add `StageProjection.agent_tools`.\n- Add `fabro-api/build.rs` replacements for the new `fabro-types` types.\n- Regenerate Rust API code.\n- Regenerate TypeScript API client code.\n\n- [ ] **Unit 5: Render tools in the web sidebar**\n\n**Goal:** Replace the permission-derived sidebar display with the actual projected tool list.\n\n**Files:**\n- Modify: `apps/fabro-web/app/components/stage-insights-sidebar.tsx`\n- Modify: `apps/fabro-web/app/components/stage-insights-sidebar.test.tsx`\n\n**Work:**\n- Render `stage.agent_tools` when present.\n- Show each tool's name, description, source/category, and invoked state.\n- Keep permission level as secondary metadata or fallback for legacy stages with no `agent_tools`.\n- Do not infer tool availability from permission level.\n\n## Test Plan\n\n- `fabro-types`\n - JSON round-trip for `agent.tools.available`.\n - Serialization checks for `AgentToolSource` and `AgentToolCategory`.\n - Backward compatibility check that missing `agent_tools` deserializes to an empty list.\n\n- `fabro-workflow`\n - Event name and conversion tests for `agent.tools.available`.\n - Capture test proving native tools such as `apply_patch`, `grep`, and `glob` are emitted from the effective registry path.\n - MCP source mapping test for a qualified MCP tool name.\n\n- `fabro-store`\n - Projection test that `agent.tools.available` populates `StageProjection.agent_tools`.\n - Projection test that `agent.tool.started` marks only the matching tool as invoked.\n - Regression test that MCP server `invoked` status still updates as before.\n\n- `fabro-api`\n - Type identity/parity tests confirming API types reuse `fabro_types::AgentToolSummary`, `AgentToolSource`, `AgentToolCategory`, and `AgentToolsAvailableProps`.\n - StageProjection round-trip test including `agent_tools`.\n\n- `fabro-web`\n - Sidebar test rendering tool names and descriptions from `stage.agent_tools`.\n - Sidebar test showing invoked state.\n - Legacy fallback test for stages without `agent_tools`.\n\n## Run Checks\n\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo nextest run -p fabro-types -p fabro-workflow -p fabro-store -p fabro-api`\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cd apps/fabro-web && bun test && bun run typecheck`\n\n## Assumptions\n\n- The first implementation targets normal API-backed agent sessions, not ACP-native sessions.\n- `description` is safe to expose because it is already model-facing tool metadata, but parameter schemas remain out of scope for StageProjection.\n- `agent_tools` is a complete list only for runs that emit `agent.tools.available`; legacy runs return an empty list and may still show existing permission/MCP metadata.\n- If tool registration becomes mutable later, the event contract can be re-emitted and projection replacement semantics will still work.\n",
"command.output": "blob://sha256/4a229e77c774df9e4587431eec40dc119a6d55f115aaf1205d37c54bc78d1c8f",
"last_stage": "simplify_gpt",
"internal.thread_id": "simplify_gpt",
"thread.start.current_node": "toolchain",
"thread.preflight_compile.current_node": "preflight_lint",
"internal.node_visit_count": 1,
"thread.toolchain.current_node": "preflight_compile",
"thread.preflight_lint.current_node": "implement",
"last_response": "Reviewed the feature branch diff against `origin/main...HEAD` and ran the requested parallel review pass. I didnt find any actionable reuse/quality/efficiency issues that warranted code changes, so",
"internal.retry_count.implement": 0,
"thread.simplify_opus.current_node": "simplify_gpt",
"response.simplify_gpt": "Reviewed the feature branch diff against `origin/main...HEAD` and ran the requested parallel review pass. I didnt find any actionable reuse/quality/efficiency issues that warranted code changes, so the working tree remains clean.\n\nValidation run:\n\n- `cargo nextest run -p fabro-types -p fabro-store -p fabro-agent -p fabro-workflow -p fabro-api agent_tools_available stage_projection_round_trip to_agent_tool_summary effective_tools --no-tests=pass` ✅\n- `cd apps/fabro-web && bun test stage-insights-sidebar.test.tsx` ✅\n- `cargo +nightly-2026-04-14 fmt --check --all` ✅\n- `cd apps/fabro-web && bun run typecheck` ✅\n- `git status --short` clean ✅",
"current_node": "verify",
"internal.work_dir": "/home/daytona/workspace/fabro",
"internal.run_id": "01KSDFKD9JJS7D2ZJ6JQYXV58Z"
},
"node_outcomes": {
"start": {
"status": "succeeded",
"usage": null
},
"simplify_gpt": {
"status": "succeeded",
"context_updates": {
"last_response": "Reviewed the feature branch diff against `origin/main...HEAD` and ran the requested parallel review pass. I didnt find any actionable reuse/quality/efficiency issues that warranted code changes, so",
"last_stage": "simplify_gpt",
"response.simplify_gpt": "Reviewed the feature branch diff against `origin/main...HEAD` and ran the requested parallel review pass. I didnt find any actionable reuse/quality/efficiency issues that warranted code changes, so the working tree remains clean.\n\nValidation run:\n\n- `cargo nextest run -p fabro-types -p fabro-store -p fabro-agent -p fabro-workflow -p fabro-api agent_tools_available stage_projection_round_trip to_agent_tool_summary effective_tools --no-tests=pass` ✅\n- `cd apps/fabro-web && bun test stage-insights-sidebar.test.tsx` ✅\n- `cargo +nightly-2026-04-14 fmt --check --all` ✅\n- `cd apps/fabro-web && bun run typecheck` ✅\n- `git status --short` clean ✅"
},
"notes": "Stage completed: simplify_gpt",
"usage": {
"input": {
"usage": {
"model": {
"provider": "openai",
"model_id": "gpt-5.5"
},
"tokens": {
"input_tokens": 1008114,
"output_tokens": 3387,
"reasoning_tokens": 2181,
"cache_read_tokens": 569856,
"cache_write_tokens": 0
}
},
"facts": {
"algorithm": "openai"
}
},
"total_usd_micros": 5492538
}
},
"preflight_compile": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
},
"notes": "Script completed: cargo check -q --workspace 2>&1",
"usage": null
},
"toolchain": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
},
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
"usage": null
},
"preflight_lint": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
},
"notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
"usage": null
},
"verify": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/4a229e77c774df9e4587431eec40dc119a6d55f115aaf1205d37c54bc78d1c8f"
},
"notes": "Script completed: git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\\bActorRef\\b|\\bActorKind\\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\\s*==\\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1",
"usage": null
},
"implement": {
"status": "succeeded",
"context_updates": {
"last_stage": "implement",
"last_response": "The TS generator is pinned now, but this sandbox lacks Java, which OpenAPI Generator needs. Ill install a headless JRE and rerun generation.",
"response.implement": "The TS generator is pinned now, but this sandbox lacks Java, which OpenAPI Generator needs. Ill install a headless JRE and rerun generation."
},
"notes": "Stage completed: implement",
"usage": {
"input": {
"usage": {
"model": {
"provider": "openai",
"model_id": "gpt-5.5"
},
"tokens": {
"input_tokens": 6711598,
"output_tokens": 22676,
"reasoning_tokens": 11522,
"cache_read_tokens": 16107008,
"cache_write_tokens": 0
}
},
"facts": {
"algorithm": "openai"
}
},
"total_usd_micros": 42637434
}
},
"simplify_opus": {
"status": "succeeded",
"context_updates": {
"response.simplify_opus": "## Summary\n\nAggregated three review agents' findings and applied the consensus high-value fixes:\n\n**Quality / reuse fixes:**\n- `tool_permissions::known_tool_category` now returns `Option<AgentToolCategory>` directly. Deleted the brittle `Some(\"read\")` → `Read` string match in `handler/llm/api.rs`. `is_auto_approved` and `tool_category` are now enum-typed end-to-end; cli.rs and its tests updated accordingly. Adding a new category is now a compile-time exhaustiveness error instead of a silent `Other`.\n- `ToolSource::Mcp` now carries `original_name` populated from `info.original_tool_name` in `make_mcp_tools`. Workflow code no longer re-parses qualified MCP names (eliminates the silent fallback bug where a malformed prefix would store the qualified name as \"original\").\n- Moved `agent_tool_source` + `agent_tool_summaries_from_definitions` out of `handler/llm/api.rs`. New `ToolDefinitionWithSource::to_agent_tool_summary()` + `Session::agent_tool_summaries()` colocate the conversion next to the source type. Tests moved to `tool_registry.rs`.\n- Collapsed `Session::available_tools` and the duplicate private `effective_tools` into one public `effective_tools()` method.\n- Merged `ToolAvailableIcon` and `TodoPendingIcon` into a single `EmptyCircleIcon` component in the sidebar.\n\n**Efficiency fix:**\n- Gated `emit_agent_tools_available` on `!is_reused` in the primary path. Reused steerable sessions had been re-emitting the full tool list (with descriptions cloned into the event payload) on every prompt even though registry, access policy, and exposure mode are immutable for the session's lifetime. Failover path keeps its emit since it always builds a fresh session.\n\n**Skipped (cost > benefit):** `mcp_server_from_tool_name` dedup in `fabro-store` would require pulling `fabro-mcp` (with `rmcp`) into the store crate or relocating the parse helper into `fabro-types`. The duplicate is 5 lines using the same delimiter convention — not worth the dependency surface.\n\n**Verification:** `cargo check --workspace --all-targets`, `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`, `cargo +nightly-2026-04-14 fmt --check --all`, `cargo nextest run -p fabro-types -p fabro-workflow -p fabro-store -p fabro-api -p fabro-agent` (2250+464 tests), `bun test stage-insights-sidebar` (10/10), and `bun run typecheck` all pass. The 14 unrelated `run-actions.test.ts` failures pre-exist on the parent commit.",
"last_stage": "simplify_opus",
"last_response": "## Summary\n\nAggregated three review agents' findings and applied the consensus high-value fixes:\n\n**Quality / reuse fixes:**\n- `tool_permissions::known_tool_category` now returns `Option<AgentToolCate"
},
"notes": "Stage completed: simplify_opus",
"usage": {
"input": {
"usage": {
"model": {
"provider": "anthropic",
"model_id": "claude-opus-4-7"
},
"tokens": {
"input_tokens": 113693,
"output_tokens": 34970,
"reasoning_tokens": 0,
"cache_read_tokens": 9248300,
"cache_write_tokens": 728711
}
},
"facts": {
"algorithm": "anthropic",
"cache_write_5m_tokens": 728711,
"cache_write_1h_tokens": 0
}
},
"total_usd_micros": 10621308
},
"files_touched": [
"/home/daytona/workspace/fabro/apps/fabro-web/app/components/stage-insights-sidebar.tsx",
"/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/cli.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/context_window.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/mcp_integration.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/session.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/tool_permissions.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/tool_registry.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/handler/llm/api.rs"
]
}
},
"next_node_id": "exit",
"node_visits": {
"toolchain": 1,
"preflight_compile": 1,
"start": 1,
"preflight_lint": 1,
"implement": 1,
"simplify_gpt": 1,
"simplify_opus": 1,
"verify": 1
}
},
"diff": {}
}
],
"conclusion": null,
"sandbox": {
"provider": "daytona",
"snapshot": "fabro-v12",
"runtime": {
"id": "fabro-01KSDFKD9JJS7D2ZJ6JQYXV58Z",
"working_directory": "/home/daytona/workspace/fabro",
"repo_cloned": true,
"clone_origin_url": "https://github.com/fabro-sh/fabro",
"clone_branch": "main",
"workspace_root": "/home/daytona/workspace",
"repos_root": "/home/daytona/repos",
"primary_repo_path": "/home/daytona/repos/fabro-sh/fabro",
"primary_repo_link": "/home/daytona/workspace/fabro"
}
},
"pull_request": null,
"superseded_by": null,
"pending_interviews": {},
"stages": {
"preflight_lint@1": {
"first_event_seq": 42,
"prompt": null,
"response": null,
"completion": {
"outcome": "succeeded",
"notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
"failure_reason": null,
"timestamp": "2026-05-24T17:17:36.540067Z"
},
"provider_used": null,
"diff": null,
"script_invocation": {
"script": "cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
"command": "exec 2>&1\ncargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
"language": "shell"
},
"script_timing": {
"output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"exit_code": 0,
"duration_ms": 135062,
"termination": "exited",
"output_bytes": 0,
"live_streaming": false
},
"parallel_results": null,
"output": null,
"output_bytes": 0,
"live_streaming": false,
"termination": "exited",
"started_at": "2026-05-24T17:15:21.467693Z",
"handler": "command",
"timing": {
"wall_time_ms": 135071,
"inference_time_ms": 0,
"tool_time_ms": 0,
"active_time_ms": 0
},
"usage": {
"input_tokens": 0,
"output_tokens": 0,
"total_tokens": 0,
"reasoning_tokens": 0,
"cache_read_tokens": 0,
"cache_write_tokens": 0
},
"state": "succeeded"
},
"simplify_opus@1": {
"first_event_seq": 979,
"prompt": null,
"response": null,
"completion": {
"outcome": "succeeded",
"notes": "Stage completed: simplify_opus",
"failure_reason": null,
"timestamp": "2026-05-24T17:57:19.242878Z"
},
"provider_used": {
"mode": "agent",
"provider": "anthropic",
"model": "claude-opus-4-7"
},
"diff": null,
"script_invocation": null,
"script_timing": null,
"parallel_results": null,
"output": null,
"started_at": "2026-05-24T17:40:37.850528Z",
"handler": "agent",
"timing": {
"wall_time_ms": 1001385,
"inference_time_ms": 0,
"tool_time_ms": 0,
"active_time_ms": 0
},
"usage": {
"input_tokens": 113693,
"output_tokens": 34970,
"total_tokens": 10125674,
"reasoning_tokens": 0,
"cache_read_tokens": 9248300,
"cache_write_tokens": 728711,
"total_usd_micros": 10621308
},
"model": {
"provider": "anthropic",
"model_id": "claude-opus-4-7"
},
"todos": {
"kind": "anthropic_tasks",
"list_id": "anthropic_tasks:00205b01-3344-49f0-8663-c0a38d32315a",
"items": [
{
"id": "1",
"status": "completed",
"order": 0,
"subject": "Make known_tool_category return Option&lt;AgentToolCategory&gt;",
"description": "Push the enum into tool_permissions.rs and delete the string-keyed agent_tool_category mapping in workflow handler/llm/api.rs.",
"active_form": "Typing known_tool_category"
},
{
"id": "2",
"status": "completed",
"order": 1,
"subject": "Carry original_name on ToolSource::Mcp",
"description": "Extend ToolSource::Mcp { server_name, original_name } in fabro-agent, populate from MCP info, and stop parsing qualified names in workflow code.",
"active_form": "Adding original_name to ToolSource::Mcp"
},
{
"id": "3",
"status": "completed",
"order": 2,
"subject": "Move tool summary conversion to fabro-agent and skip parameters clone",
"description": "Move agent_tool_summaries / agent_tool_source into fabro-agent, expose Session::agent_tool_summaries() that doesn't clone parameters JSON. Collapse available_tools/effective_tools.",
"active_form": "Refactoring tool summary conversion"
},
{
"id": "5",
"status": "completed",
"order": 4,
"subject": "Dedupe ToolAvailableIcon / TodoPendingIcon in sidebar",
"description": "Consolidate to one EmptyCircleIcon component.",
"active_form": "Deduping sidebar icons"
},
{
"id": "6",
"status": "completed",
"order": 5,
"subject": "Skip redundant tools-available emit on reused sessions",
"description": "Only emit agent.tools.available once per session (not on every reused steerable prompt).",
"active_form": "Gating tools-available emit"
}
]
},
"subagents": [
{
"agent_id": "38a1b570",
"depth": 1,
"task": "You are doing a CODE REUSE review on a Rust + TypeScript change.\n\nThe change adds a new `StageProjection.agent_tools` field, a new durable event `agent.tools.available`, and a UI section to show tools. Full diff is at /tmp/review_diff.patch (1696 lines). Read it first.\n\nWorking directory: /home/daytona/workspace/fabro\n\nFor each new function or chunk of inline logic in the diff, search the existing codebase (grep) for utilities or helpers that could replace it. Common locations: `lib/crates/fabro-util`, `lib/crates/fabro-mcp`, `lib/crates/fabro-agent`, `lib/crates/fabro-types`, `lib/crates/fabro-workflow/src/event`, `lib/crates/fabro-store/src/run_state.rs`.\n\nSpecific things to check:\n\n1. `agent_tool_category` in `lib/crates/fabro-workflow/src/handler/llm/api.rs` maps strings like \"read\"/\"write\"/\"shell\"/\"subagent\" to an `AgentToolCategory` enum. Note that `lib/crates/fabro-agent/src/tool_permissions.rs` was changed to return Option<&'static str>. Could the category mapping be done once in `fabro-types` or directly in tool_permissions instead of double-mapping (str -> Option<str> -> enum)?\n\n2. `agent_tool_source` parses MCP qualified names using `fabro_mcp::connection_manager::parse_qualified_name`. Is there an existing place where a `(ToolSource, name) -> AgentToolSource` mapping should live (e.g., shared between agent + workflow)?\n\n3. The MCP server name extraction from tool name uses `parse_qualified_name`, but `lib/crates/fabro-store/src/run_state.rs` has its own `mcp_server_from_tool_name` doing similar work. Are these duplicates?\n\n4. `AgentToolCategory` derives strum::Display/EnumString/IntoStaticStr. Was `tool_permissions::known_tool_category` returning `&'static str` (which is then string-matched into the enum) the right thing, or should it return `Option<AgentToolCategory>` directly to avoid stringly-typed plumbing? Note that `tool_permissions.rs` is in `fabro-agent` and `AgentToolCategory` is in `fabro-types`. Look at `fabro-agent`'s dependencies on `fabro-types`.\n\n5. Look at `agent_tool_summaries_from_definitions` - is there an idiomatic place this should live (e.g. a method on `Session` or on `ToolDefinitionWithSource`)?\n\n6. `ToolAvailableIcon` is a custom React component. Is there an existing icon component already used for \"available but not invoked\" states elsewhere in the app?\n\n7. The new `effective_tools()` helper on Session de-duplicates two call sites. Good. But check if there are other call sites still computing the same thing manually.\n\nReport findings concisely with file:line references and a recommendation per finding. Skip false positives. Do not make any changes.",
"status": {
"kind": "completed",
"success": true,
"turns_used": 73
}
},
{
"agent_id": "12b365ea",
"depth": 1,
"task": "You are doing a CODE QUALITY review on a Rust + TypeScript change.\n\nThe change adds a new `StageProjection.agent_tools` field, a new durable event `agent.tools.available`, and a UI section to show tools. Full diff is at /tmp/review_diff.patch (1696 lines). Read it first.\n\nWorking directory: /home/daytona/workspace/fabro\n\nFocus on hacky patterns:\n\n1. **Stringly-typed plumbing**: `tool_permissions::known_tool_category` returns `Option<&'static str>` and then `agent_tool_category` (in `lib/crates/fabro-workflow/src/handler/llm/api.rs`) matches on those strings to convert to `AgentToolCategory`. This is two layers of string mapping when an enum would do. Look at fabro-agent's Cargo.toml — does it already depend on fabro-types? If yes, `known_tool_category` should return `Option<AgentToolCategory>` directly.\n\n2. **Redundant state**: `agent_tools: Vec<AgentToolSummary>` in StageProjection where `invoked` is mutated during replay. Is the `invoked` bool a derived state that could come from something already projected? Probably fine but check.\n\n3. **Parameter sprawl**: `emit_agent_tools_available` takes 4 params. Check the new functions in handler/llm/api.rs — are any of these parameters easily grouped into an existing struct (e.g., a context already in scope)?\n\n4. **Copy-paste**: The emit call appears twice in `lib/crates/fabro-workflow/src/handler/llm/api.rs` (in two `CodergenBackend::run` paths). Check if these are truly the same logic or have variations.\n\n5. **Leaky abstractions**: `agent_tool_source` calls `fabro_mcp::connection_manager::parse_qualified_name` directly from workflow code to derive `original_name`. Should this derivation live elsewhere (e.g., in `ToolSource::Mcp` carrying the original_name, or in fabro-agent's mcp integration that already knows the original name)?\n\n6. **The category match is brittle**: `match tool_permissions::known_tool_category(name) { Some(\"read\") => ..., ... Some(_) | None => Other }`. If a new category is added to `tool_permissions`, the workflow code silently maps it to `Other`. This is fragile.\n\n7. **AgentToolSource::Mcp original_name** — this is derived by parsing the qualified name. Could it be wrong/empty? The MCP path that generates qualified names is the authority — should original_name come from there directly?\n\n8. **`ToolAvailableIcon` in `stage-insights-sidebar.tsx`**: hand-rolled span with a border class. Is there a more idiomatic existing pattern (e.g., a proper Heroicon)?\n\n9. **Permission_level fallback**: The diff comments say \"Permission remains secondary compatibility metadata, not the source of the tool list.\" Check that the sidebar UI properly handles the legacy fallback case (no agent_tools) — does old permission-derived rendering still work?\n\n10. **Test redundancy**: Are the new tests duplicating coverage that other tests already provide?\n\nReport findings concisely with file:line references and concrete recommendations. Skip false positives. Do not make any changes.",
"status": {
"kind": "completed",
"success": true,
"turns_used": 58
}
},
{
"agent_id": "446d0aa8",
"depth": 1,
"task": "You are doing an EFFICIENCY review on a Rust + TypeScript change.\n\nThe change adds a new `StageProjection.agent_tools` field, a new durable event `agent.tools.available`, and a UI section to show tools. Full diff is at /tmp/review_diff.patch (1696 lines). Read it first.\n\nWorking directory: /home/daytona/workspace/fabro\n\nFocus on:\n\n1. **Hot-path bloat**: `emit_agent_tools_available` is called inside the per-stage processing path in `lib/crates/fabro-workflow/src/handler/llm/api.rs`. The work involves cloning all tool definitions (incl. full description strings). Could this be called once per session rather than once per process_input? Look at the call sites — the diff shows it's called before each `session.process_input_with_runtime`. Is the effective tool list constant for the session, or could it change between turns?\n\n2. **Repeated cloning**: `Session::available_tools()` clones the full `Vec<ToolDefinitionWithSource>` each call. Then `agent_tool_summaries_from_definitions` clones every field again into AgentToolSummary. Could the conversion take owned values or borrow?\n\n3. **N+1 / repeated work**: `definitions_with_source_for_policy` is now called twice per turn — once for the model request and once for emit. Look at how often `effective_tools()` is called per session input.\n\n4. **Memory**: `StageProjection.agent_tools` is a per-stage Vec carrying full descriptions. If a stage has 30 tools each with ~200 char descriptions, that's a few KB per stage in the projection. The projection is in-memory and serialized to clients. Is this bounded/reasonable?\n\n5. **Sorting**: `agent_tool_summaries_from_definitions` does `summaries.sort_by(|left, right| left.name.cmp(&right.name))`. Reasonable for stability but adds O(n log n) per turn. Compare to whether the registry order is already deterministic.\n\n6. **invoked replay**: `lib/crates/fabro-store/src/run_state.rs` ~line 612: for every `AgentToolStarted` event, the code iterates `stage.agent_tools` linearly to find the match. For long-running stages with many tool calls (hundreds), this is O(tools * calls). Is the tools list small enough that this is fine?\n\n7. **React render**: `AgentToolsSection` iterates tools; check if anything obvious like `useMemo` is missing where it would materially help, but don't add unnecessary memoization.\n\n8. **Tool description size**: descriptions can be long (tool prompts often 500+ chars). Multiply by tools count and that's the projection size impact. Note R4 says no parameter schemas, but descriptions are explicit. Just flag the size concern; this was an explicit design decision.\n\nReport findings concisely with file:line references and concrete recommendations. Skip false positives. Do not make any changes.",
"status": {
"kind": "completed",
"success": true,
"turns_used": 69
}
}
],
"permission_level": "full",
"context_window": {
"provider": "anthropic",
"model": "claude-opus-4-7",
"context_window_tokens": 1000000,
"input_tokens": 126197,
"usage_percent": 12.6197,
"count_method": "response_usage_scaled_breakdown",
"staleness": "live",
"generated_at": "2026-05-24T17:57:18.880486Z",
"event_seq": 2276,
"breakdown": [
{
"category": "system_prompt",
"tokens": 2354,
"usage_percent": 0.2354
},
{
"category": "tools",
"tokens": 2699,
"usage_percent": 0.2699
},
{
"category": "memory",
"tokens": 5639,
"usage_percent": 0.5639
},
{
"category": "conversation",
"tokens": 115498,
"usage_percent": 11.5498
},
{
"category": "other",
"tokens": 7,
"usage_percent": 0.0007
}
],
"warnings": []
},
"state": "succeeded"
},
"implement@1": {
"first_event_seq": 52,
"prompt": null,
"response": null,
"completion": {
"outcome": "succeeded",
"notes": "Stage completed: implement",
"failure_reason": null,
"timestamp": "2026-05-24T17:40:34.044793Z"
},
"provider_used": {
"mode": "agent",
"provider": "openai",
"model": "gpt-5.5",
"reasoning_effort": "xhigh"
},
"diff": null,
"script_invocation": null,
"script_timing": null,
"parallel_results": null,
"output": null,
"started_at": "2026-05-24T17:17:39.928850Z",
"handler": "agent",
"timing": {
"wall_time_ms": 1374106,
"inference_time_ms": 0,
"tool_time_ms": 0,
"active_time_ms": 0
},
"usage": {
"input_tokens": 6711598,
"output_tokens": 22676,
"total_tokens": 22852804,
"reasoning_tokens": 11522,
"cache_read_tokens": 16107008,
"cache_write_tokens": 0,
"total_usd_micros": 42637434
},
"model": {
"provider": "openai",
"model_id": "gpt-5.5"
},
"todos": {
"kind": "openai_plan",
"list_id": "openai_plan:6097c25a-506b-4545-8268-4718058dd9a5",
"items": [
{
"id": "a34c32a50dbee061",
"status": "completed",
"order": 0,
"subject": "Read repo guidance and relevant strategy docs"
},
{
"id": "0667291eaf373c23",
"status": "completed",
"order": 1,
"subject": "Add failing tests for new agent tools projection contract"
},
{
"id": "7956e15880861dca",
"status": "in_progress",
"order": 2,
"subject": "Implement shared types, durable event, workflow emission, and store projection"
},
{
"id": "f0acc8560fcaf924",
"status": "pending",
"order": 3,
"subject": "Update OpenAPI, Rust replacements, and generated clients"
},
{
"id": "f8d68fce301a219e",
"status": "pending",
"order": 4,
"subject": "Update web sidebar rendering and tests"
},
{
"id": "04290d4cf3b1b10b",
"status": "pending",
"order": 5,
"subject": "Run focused and broader validation checks"
}
]
},
"permission_level": "full",
"context_window": {
"provider": "openai",
"model": "gpt-5.5",
"context_window_tokens": 1050000,
"input_tokens": 271487,
"usage_percent": 25.85590476190476,
"count_method": "response_usage_scaled_breakdown",
"staleness": "live",
"generated_at": "2026-05-24T17:40:34.020573Z",
"event_seq": 967,
"breakdown": [
{
"category": "system_prompt",
"tokens": 962,
"usage_percent": 0.09161904761904761
},
{
"category": "tools",
"tokens": 1389,
"usage_percent": 0.13228571428571428
},
{
"category": "memory",
"tokens": 3250,
"usage_percent": 0.30952380952380953
},
{
"category": "conversation",
"tokens": 265879,
"usage_percent": 25.321809523809524
},
{
"category": "other",
"tokens": 7,
"usage_percent": 0.0006666666666666666
}
],
"warnings": []
},
"state": "succeeded"
},
"verify@1": {
"first_event_seq": 2700,
"prompt": null,
"response": null,
"completion": null,
"provider_used": null,
"diff": null,
"script_invocation": {
"script": "git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\\bActorRef\\b|\\bActorKind\\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\\s*==\\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1",
"command": "exec 2>&1\ngit fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\\bActorRef\\b|\\bActorKind\\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\\s*==\\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1",
"language": "shell"
},
"script_timing": null,
"parallel_results": null,
"output": null,
"started_at": "2026-05-24T18:01:10.177355Z",
"handler": "command",
"usage": {
"input_tokens": 0,
"output_tokens": 0,
"total_tokens": 0,
"reasoning_tokens": 0,
"cache_read_tokens": 0,
"cache_write_tokens": 0
},
"state": "running"
},
"preflight_compile@1": {
"first_event_seq": 32,
"prompt": null,
"response": null,
"completion": {
"outcome": "succeeded",
"notes": "Script completed: cargo check -q --workspace 2>&1",
"failure_reason": null,
"timestamp": "2026-05-24T17:15:17.921791Z"
},
"provider_used": null,
"diff": null,
"script_invocation": {
"script": "cargo check -q --workspace 2>&1",
"command": "exec 2>&1\ncargo check -q --workspace 2>&1",
"language": "shell"
},
"script_timing": {
"output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"exit_code": 0,
"duration_ms": 120844,
"termination": "exited",
"output_bytes": 0,
"live_streaming": false
},
"parallel_results": null,
"output": null,
"output_bytes": 0,
"live_streaming": false,
"termination": "exited",
"started_at": "2026-05-24T17:13:17.066593Z",
"handler": "command",
"timing": {
"wall_time_ms": 120854,
"inference_time_ms": 0,
"tool_time_ms": 0,
"active_time_ms": 0
},
"usage": {
"input_tokens": 0,
"output_tokens": 0,
"total_tokens": 0,
"reasoning_tokens": 0,
"cache_read_tokens": 0,
"cache_write_tokens": 0
},
"state": "succeeded"
},
"start@1": {
"first_event_seq": 18,
"prompt": null,
"response": null,
"completion": {
"outcome": "succeeded",
"notes": null,
"failure_reason": null,
"timestamp": "2026-05-24T17:13:12.111387Z"
},
"provider_used": null,
"diff": null,
"script_invocation": null,
"script_timing": null,
"parallel_results": null,
"output": null,
"started_at": "2026-05-24T17:13:12.111104Z",
"handler": "start",
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 0,
"active_time_ms": 0
},
"usage": {
"input_tokens": 0,
"output_tokens": 0,
"total_tokens": 0,
"reasoning_tokens": 0,
"cache_read_tokens": 0,
"cache_write_tokens": 0
},
"state": "succeeded"
},
"toolchain@1": {
"first_event_seq": 22,
"prompt": null,
"response": null,
"completion": {
"outcome": "succeeded",
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
"failure_reason": null,
"timestamp": "2026-05-24T17:13:13.450996Z"
},
"provider_used": null,
"diff": null,
"script_invocation": {
"script": "command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
"command": "exec 2>&1\ncommand -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
"language": "shell"
},
"script_timing": {
"output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c",
"exit_code": 0,
"duration_ms": 1325,
"termination": "exited",
"output_bytes": 36,
"live_streaming": true
},
"parallel_results": null,
"output": null,
"output_bytes": 36,
"live_streaming": true,
"termination": "exited",
"started_at": "2026-05-24T17:13:12.111794Z",
"handler": "command",
"timing": {
"wall_time_ms": 1338,
"inference_time_ms": 0,
"tool_time_ms": 0,
"active_time_ms": 0
},
"usage": {
"input_tokens": 0,
"output_tokens": 0,
"total_tokens": 0,
"reasoning_tokens": 0,
"cache_read_tokens": 0,
"cache_write_tokens": 0
},
"state": "succeeded"
},
"simplify_gpt@1": {
"first_event_seq": 2288,
"prompt": null,
"response": null,
"completion": {
"outcome": "succeeded",
"notes": "Stage completed: simplify_gpt",
"failure_reason": null,
"timestamp": "2026-05-24T18:01:06.598167Z"
},
"provider_used": {
"mode": "agent",
"provider": "openai",
"model": "gpt-5.5"
},
"diff": null,
"script_invocation": null,
"script_timing": null,
"parallel_results": null,
"output": null,
"started_at": "2026-05-24T17:57:23.054232Z",
"handler": "agent",
"timing": {
"wall_time_ms": 223542,
"inference_time_ms": 0,
"tool_time_ms": 0,
"active_time_ms": 0
},
"usage": {
"input_tokens": 1008114,
"output_tokens": 3387,
"total_tokens": 1583538,
"reasoning_tokens": 2181,
"cache_read_tokens": 569856,
"cache_write_tokens": 0,
"total_usd_micros": 5492538
},
"model": {
"provider": "openai",
"model_id": "gpt-5.5"
},
"todos": {
"kind": "openai_plan",
"list_id": "openai_plan:16f8e262-d404-4d7f-ac86-1cf476c9b8bc",
"items": [
{
"id": "7933461d2deef81f",
"status": "completed",
"order": 0,
"subject": "Inspect current diff and repository instructions"
},
{
"id": "2219a10ca28969a5",
"status": "completed",
"order": 1,
"subject": "Run three parallel review agents with full diff"
},
{
"id": "e4f3a945762ccf45",
"status": "completed",
"order": 2,
"subject": "Aggregate findings and apply cleanup fixes"
},
{
"id": "7de46a79922831f3",
"status": "completed",
"order": 3,
"subject": "Run targeted validation"
}
]
},
"subagents": [
{
"agent_id": "745dc5c9",
"depth": 1,
"task": "Code Reuse Review. Review the full feature diff saved at /tmp/fabro_feature_diff.patch (origin/main...HEAD, excluding Cargo.lock) in /home/daytona/workspace/fabro. For each change, search for existing utilities/helpers that could replace newly written code. Flag duplicated functionality or inline logic that could use existing utilities. Focus on actionable findings only; include file paths and suggested existing helper names. Do not modify files.",
"status": {
"kind": "completed",
"success": true,
"turns_used": 9
}
},
{
"agent_id": "e7dd3530",
"depth": 1,
"task": "Code Quality Review. Review the full feature diff saved at /tmp/fabro_feature_diff.patch (origin/main...HEAD, excluding Cargo.lock) in /home/daytona/workspace/fabro. Look for redundant state, parameter sprawl, copy-paste, leaky abstractions, stringly typed code, and hacky patterns. Be aggressive but actionable. Include file paths and concrete recommendations. Do not modify files.",
"status": {
"kind": "completed",
"success": true,
"turns_used": 9
}
},
{
"agent_id": "cd84452e",
"depth": 1,
"task": "Efficiency Review. Review the full feature diff saved at /tmp/fabro_feature_diff.patch (origin/main...HEAD, excluding Cargo.lock) in /home/daytona/workspace/fabro. Look for unnecessary work, missed concurrency, hot-path bloat, TOCTOU checks, memory issues, and overly broad operations. Include file paths and concrete recommendations. Do not modify files.",
"status": {
"kind": "completed",
"success": true,
"turns_used": 9
}
}
],
"permission_level": "full",
"context_window": {
"provider": "openai",
"model": "gpt-5.5",
"context_window_tokens": 1050000,
"input_tokens": 54711,
"usage_percent": 5.210571428571429,
"count_method": "response_usage_scaled_breakdown",
"staleness": "live",
"generated_at": "2026-05-24T18:01:06.580733Z",
"event_seq": 2688,
"breakdown": [
{
"category": "system_prompt",
"tokens": 948,
"usage_percent": 0.09028571428571429
},
{
"category": "tools",
"tokens": 1360,
"usage_percent": 0.1295238095238095
},
{
"category": "memory",
"tokens": 3182,
"usage_percent": 0.30304761904761907
},
{
"category": "conversation",
"tokens": 49216,
"usage_percent": 4.687238095238095
},
{
"category": "other",
"tokens": 5,
"usage_percent": 0.0004761904761904762
}
],
"warnings": []
},
"state": "succeeded"
}
}
}