From c844ef428cc326509caf02c97704b81a29fb1a09 Mon Sep 17 00:00:00 2001 From: Fabro Date: Fri, 22 May 2026 15:49:37 -0400 Subject: [PATCH] =?UTF-8?q?checkpoint=20=E2=9A=92=EF=B8=8F=20Generated=20w?= =?UTF-8?q?ith=20[Fabro](https://fabro.sh)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- run.json | 454 +++++++++-- stages/006-simplify_opus@1/diff.patch | 807 +++++++++++++++++++ stages/006-simplify_opus@1/status.json | 6 + stages/007-simplify_gpt@1/prompt.md | 511 ++++++++++++ stages/007-simplify_gpt@1/provider_used.json | 5 + stages/007-simplify_gpt@1/response.md | 24 + 6 files changed, 1742 insertions(+), 65 deletions(-) create mode 100644 stages/006-simplify_opus@1/diff.patch create mode 100644 stages/006-simplify_opus@1/status.json create mode 100644 stages/007-simplify_gpt@1/prompt.md create mode 100644 stages/007-simplify_gpt@1/provider_used.json create mode 100644 stages/007-simplify_gpt@1/response.md diff --git a/run.json b/run.json index e3b096d32..99d68d035 100644 --- a/run.json +++ b/run.json @@ -517,7 +517,7 @@ "kind": "running" }, "status_updated_at": "2026-05-22T18:57:01.494747Z", - "last_event_at": "2026-05-22T19:38:59.232726Z", + "last_event_at": "2026-05-22T19:49:37.027930Z", "pending_control": null, "checkpoints": [ { @@ -900,9 +900,9 @@ } }, { - "seq": 0, + "seq": 987, "checkpoint": { - "timestamp": "2026-05-22T19:38:59.337710Z", + "timestamp": "2026-05-22T19:39:03.370372Z", "current_node": "simplify_opus", "completed_nodes": [ "start", @@ -913,21 +913,209 @@ "simplify_opus" ], "node_retries": {}, + "context_values": { + "internal.fidelity": "compact", + "internal.retry_count.simplify_opus": 0, + "last_response": "All checks green. Here's the summary:\n\n## Summary\n\nThree review agents flagged overlapping issues. I applied the high-value cleanups and skipped low-value or out-of-scope ones.\n\n### Fixed\n1. **Elimina", + "response.simplify_opus": "All checks green. Here's the summary:\n\n## Summary\n\nThree review agents flagged overlapping issues. I applied the high-value cleanups and skipped low-value or out-of-scope ones.\n\n### Fixed\n1. **Eliminated 4 duplicate types** between `fabro-agent` and `fabro-types` (`McpToolSummary`, `MemoryFileSummary`, `SkillSummary`, `SkillActivationSource`). `AgentEvent` now embeds the `fabro-types` versions directly, mirroring the existing `TodoCreated/Updated/Deleted` pattern. Removed ~50 lines of field-by-field cloning in `convert.rs`.\n2. **Deleted dead `SkillExpanded` variant** entirely — its emit site had already been replaced with `SkillActivated { source: Slash }`. Removed from `types.rs` (variant, streaming-noise classification, trace arm, test), `convert.rs` (panic-list), and `names.rs` (name mapping).\n3. **Strongly typed `provider_profile`** as `fabro_model::AgentProfileKind` (Copy enum with existing `Display`) on both wire props and in-memory variants instead of `String`. Removed two `.to_string()` calls and per-event clones.\n4. **Dropped redundant derived fields** from in-memory variants:\n - `MemoryDocument.loaded_bytes` → always equals `content.len()`.\n - `AgentEvent::McpServerReady.tool_count` → always equals `tools.len()`.\n - `AgentEvent::MemoryLoaded.total_loaded_bytes` → sum of `files[].loaded_bytes`.\n - All three are computed at the conversion boundary so the wire shape is preserved.\n\n### Skipped (with reason)\n- **`build_system_prompt` signature change** (memory clone elimination): touches 6 files including public SDK reference docs and 4 profile impls for a single 32 KiB clone at session init. Not worth the API churn.\n- **MCP tool N+1 fix**: one-shot pass at session startup, not a hot path.\n- **`ToolContext` Option accretion**: out of scope for this PR; should be its own refactor when the next field is added.\n- **Two-pass over memory in init**: micro-optimization, current code is clearer.\n\n### Verification\n- `cargo check --workspace --all-targets` — clean\n- `cargo nextest run -p fabro-agent -p fabro-workflow -p fabro-types -p fabro-mcp` — 1840 passed, 0 failed\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` — clean\n- `cargo +nightly-2026-04-14 fmt --all` — applied\n\nNet change: removed ~100 lines while preserving all behavior, wire format, and test coverage.", + "thread.implement.current_node": "simplify_opus", + "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "failure_class": "", + "graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ", + "internal.node_visit_count": 1, + "internal.retry_count.start": 0, + "internal.run_id": "01KS8GR1P97NYP93VVXWYCZHYZ", + "failure_signature": "", + "thread.preflight_compile.current_node": "preflight_lint", + "thread.preflight_lint.current_node": "implement", + "internal.retry_count.preflight_lint": 0, + "internal.retry_count.preflight_compile": 0, + "internal.retry_count.implement": 0, + "thread.toolchain.current_node": "preflight_compile", + "internal.retry_count.toolchain": 0, + "current_node": "simplify_opus", + "graph.goal": "# Agent Context Observability Events Implementation Plan\n\n> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.\n\n**Goal:** Add durable API/backend events that report loaded memory files, discovered and activated skills, and per-server MCP tool names for agent runs.\n\n**Architecture:** Keep this API-backend scoped. Emit typed `AgentEvent` variants from the existing `fabro-agent` initialization and skill activation paths, convert them through `fabro-workflow` into durable `fabro-types` run events, and document the event contracts. Do not add run projection fields in this pass; consumers can read the event stream/history.\n\n**Tech Stack:** Rust, Serde, Fabro agent/session events, Fabro workflow event conversion, Fabro MCP connection manager, `cargo nextest`.\n\n---\n\n## Scope\n\nImplement these event changes:\n\n- Add `agent.memory.loaded` with memory file paths, byte counts, loaded byte counts, truncation flags, provider profile, total loaded bytes, and budget bytes.\n- Add `agent.skills.discovered` with source directories, provider profile, and sorted skill summaries.\n- Add persisted `agent.skill.activated` for slash skill expansion and successful `use_skill` tool calls.\n- Enrich `agent.mcp.ready` with names-only tool summaries: qualified tool name and original server tool name.\n\nDo not implement ACP-native equivalents in this pass. Do not include memory file contents in any event payload. Do not include MCP tool descriptions or schemas.\n\n## Existing Patterns To Follow\n\n- Read `docs/internal/events-strategy.md` before changing event variants, names, conversion, or progress JSONL behavior.\n- Read `docs/internal/testing-strategy.md` before adding or reorganizing tests.\n- Follow the current `AgentEvent` flow:\n - `lib/crates/fabro-agent/src/types.rs`\n - `lib/crates/fabro-agent/src/session.rs`\n - `lib/crates/fabro-workflow/src/handler/llm/api.rs`\n - `lib/crates/fabro-workflow/src/event/convert.rs`\n - `lib/crates/fabro-workflow/src/event/names.rs`\n - `lib/crates/fabro-types/src/run_event/agent.rs`\n - `lib/crates/fabro-types/src/run_event/mod.rs`\n- Follow Rust import style from `AGENTS.md`: import types by name, import functions through their parent module, and avoid glob imports in production code.\n\n## File Map\n\n- Modify `lib/crates/fabro-types/src/run_event/agent.rs`: add new prop structs and extend `AgentMcpReadyProps`.\n- Modify `lib/crates/fabro-types/src/run_event/mod.rs`: add `EventBody` variants for the new event names.\n- Modify `lib/crates/fabro-agent/src/types.rs`: add internal `AgentEvent` variants, trace output, and noise filtering decisions.\n- Modify `lib/crates/fabro-agent/src/memory.rs`: return memory content plus metadata instead of bare strings.\n- Modify `lib/crates/fabro-agent/src/session.rs`: emit memory, skills, skill activation, and enriched MCP events.\n- Modify `lib/crates/fabro-agent/src/skills.rs`: emit tool-sourced skill activation from `use_skill`.\n- Modify `lib/crates/fabro-mcp/src/connection_manager.rs`: expose or support deterministic names-only tool summaries per server.\n- Modify `lib/crates/fabro-workflow/src/event/convert.rs`: convert new agent events to durable event bodies.\n- Modify `lib/crates/fabro-workflow/src/event/names.rs`: add event names.\n- Modify `lib/crates/fabro-workflow/src/event/events.rs` only if the agent event name mapping also lives there for these variants.\n- Modify `lib/crates/fabro-workflow/src/event/stored_fields.rs` only if a new event needs non-standard stored fields; otherwise rely on existing `Event::Agent` handling.\n- Modify `docs/internal/events.md`: document new event shapes and the richer MCP payload.\n- Add or update tests in `lib/crates/fabro-agent`, `lib/crates/fabro-mcp`, `lib/crates/fabro-types`, and `lib/crates/fabro-workflow`.\n\n---\n\n### Task 1: Add Typed Durable Event Contracts\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- Test: existing `fabro-types` run event serde tests, or add focused coverage near the existing run event tests.\n\n- [ ] **Step 1: Add agent memory props**\n\nAdd event prop structs with this shape:\n\n```rust\npub struct AgentMemoryLoadedProps {\n pub provider_profile: String,\n pub files: Vec,\n pub total_loaded_bytes: usize,\n pub budget_bytes: usize,\n pub visit: u32,\n}\n\npub struct AgentMemoryFileProps {\n pub path: String,\n pub byte_count: usize,\n pub loaded_bytes: usize,\n pub truncated: bool,\n}\n```\n\n- [ ] **Step 2: Add skill props**\n\nAdd skill discovery and activation props:\n\n```rust\npub struct AgentSkillsDiscoveredProps {\n pub provider_profile: String,\n pub source_dirs: Vec,\n pub skills: Vec,\n pub visit: u32,\n}\n\npub struct AgentSkillSummary {\n pub name: String,\n pub description: String,\n}\n\npub enum AgentSkillActivationSource {\n Slash,\n Tool,\n}\n\npub struct AgentSkillActivatedProps {\n pub skill_name: String,\n pub source: AgentSkillActivationSource,\n pub visit: u32,\n}\n```\n\nUse serde names `slash` and `tool` for `AgentSkillActivationSource`. If a local enum string pattern already exists, follow that pattern.\n\n- [ ] **Step 3: Extend MCP ready props**\n\nExtend `AgentMcpReadyProps` with a backwards-compatible field:\n\n```rust\n#[serde(default, skip_serializing_if = \"Vec::is_empty\")]\npub tools: Vec,\n```\n\nAdd:\n\n```rust\npub struct AgentMcpToolSummary {\n pub name: String,\n pub original_name: String,\n}\n```\n\n- [ ] **Step 4: Add EventBody variants**\n\nAdd `EventBody` variants using these serialized event names:\n\n- `agent.memory.loaded`\n- `agent.skills.discovered`\n- `agent.skill.activated`\n\nKeep existing `agent.mcp.ready` name unchanged and only enrich its props.\n\n- [ ] **Step 5: Add serde tests**\n\nCover:\n\n- New event names serialize to the expected dot names.\n- `AgentSkillActivationSource` serializes as `slash` and `tool`.\n- Old `agent.mcp.ready` JSON without `tools` still deserializes with `tools == []`.\n\n---\n\n### Task 2: Add Internal Agent Events And Conversion\n\n**Files:**\n- Modify: `lib/crates/fabro-agent/src/types.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/convert.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/names.rs`\n- Modify: `lib/crates/fabro-workflow/src/event/events.rs` if needed by the existing name mapping.\n- Test: `lib/crates/fabro-workflow` event conversion tests.\n\n- [ ] **Step 1: Add internal AgentEvent variants**\n\nAdd variants equivalent to:\n\n```rust\nMemoryLoaded {\n provider_profile: String,\n files: Vec,\n total_loaded_bytes: usize,\n budget_bytes: usize,\n}\n\nSkillsDiscovered {\n provider_profile: String,\n source_dirs: Vec,\n skills: Vec,\n}\n\nSkillActivated {\n skill_name: String,\n source: SkillActivationSource,\n}\n\nMcpServerReady {\n server_name: String,\n tool_count: usize,\n tools: Vec,\n}\n```\n\nPrefer small shared internal structs near `AgentEvent` if that matches the existing file organization.\n\n- [ ] **Step 2: Persist skill activation**\n\nDo not classify `SkillActivated` as streaming noise. The existing `SkillExpanded` event is currently filtered before persistence; replace slash expansion emissions with `SkillActivated { source: Slash }` or keep `SkillExpanded` internal-only if removing it would create unnecessary churn.\n\n- [ ] **Step 3: Add trace behavior**\n\nUpdate `AgentEvent::trace` so the new events emit concise tracing summaries:\n\n- memory loaded: profile, file count, total loaded bytes, budget bytes\n- skills discovered: profile, skill count, source dir count\n- skill activated: name and source\n- MCP ready: server, count, and summary count\n\n- [ ] **Step 4: Convert to durable events**\n\nUpdate `fabro-workflow` event conversion so the new agent events map to the new `fabro-types` props and include `visit`.\n\n- [ ] **Step 5: Add conversion tests**\n\nCover each new event with a focused conversion assertion that checks:\n\n- durable event name\n- `visit`\n- core fields\n- no memory content in the converted payload\n\n---\n\n### Task 3: Emit Memory Loaded Metadata\n\n**Files:**\n- Modify: `lib/crates/fabro-agent/src/memory.rs`\n- Modify: `lib/crates/fabro-agent/src/session.rs`\n- Test: relevant `fabro-agent` memory/session tests.\n\n- [ ] **Step 1: Change memory discovery return type**\n\nChange memory discovery from bare `Vec` to a document type carrying both prompt content and event metadata:\n\n```rust\npub struct MemoryDocument {\n pub path: String,\n pub content: String,\n pub byte_count: usize,\n pub loaded_bytes: usize,\n pub truncated: bool,\n}\n```\n\nKeep existing behavior unchanged:\n\n- provider profile filename candidates stay the same\n- root-to-working-dir walk stays the same\n- content dedupe stays the same\n- empty files are skipped\n- total budget remains 32 KiB\n- truncated content keeps the existing truncation marker\n\n- [ ] **Step 2: Preserve prompt assembly behavior**\n\nAdjust session/profile prompt assembly to pass only memory contents where prompt assembly expects memory text. The system prompt should be byte-for-byte equivalent except where existing tests allow non-semantic ordering differences.\n\n- [ ] **Step 3: Emit agent.memory.loaded**\n\nIn `Session::initialize()`, emit `AgentEvent::MemoryLoaded` immediately after memory discovery, before skills and MCP initialization.\n\nEmit the event even when no memory files are loaded. That lets consumers distinguish \"no memory\" from \"not reported.\"\n\n- [ ] **Step 4: Add memory tests**\n\nCover:\n\n- loaded file path appears in event metadata\n- `byte_count` is the original file byte count\n- `loaded_bytes` reflects bytes actually loaded into the prompt budget\n- `truncated` is true only for truncated files\n- event payload never contains memory file contents\n- empty discovery still emits a memory-loaded event with `files == []`\n\n---\n\n### Task 4: Emit Skills Discovered And Skill Activated\n\n**Files:**\n- Modify: `lib/crates/fabro-agent/src/session.rs`\n- Modify: `lib/crates/fabro-agent/src/skills.rs`\n- Test: relevant `fabro-agent` skill/session tests.\n\n- [ ] **Step 1: Emit skills discovered**\n\nAfter `discover_skills(...)`, emit `AgentEvent::SkillsDiscovered` with:\n\n- `provider_profile`\n- `source_dirs`\n- sorted `skills: [{ name, description }]`\n\nEmit the event even when no skills are discovered.\n\n- [ ] **Step 2: Emit slash activation**\n\nWhere slash skill expansion currently emits or creates `SkillExpanded`, emit:\n\n```rust\nAgentEvent::SkillActivated {\n skill_name,\n source: SkillActivationSource::Slash,\n}\n```\n\n- [ ] **Step 3: Emit tool activation**\n\nIn `make_use_skill_tool`, use `ToolContext::emit_agent_event(...)` after a requested skill is found and before returning the skill template. Emit:\n\n```rust\nAgentEvent::SkillActivated {\n skill_name: name.to_string(),\n source: SkillActivationSource::Tool,\n}\n```\n\nDo not emit activation for failed `use_skill` lookups.\n\n- [ ] **Step 4: Add skill tests**\n\nCover:\n\n- discovery event includes all discovered skills sorted by name\n- discovery event includes configured source directories\n- empty discovery emits `skills == []`\n- slash expansion emits `source == slash`\n- successful `use_skill` emits `source == tool`\n- failed `use_skill` does not emit activation\n\n---\n\n### Task 5: Enrich agent.mcp.ready With Names-Only Tool Summaries\n\n**Files:**\n- Modify: `lib/crates/fabro-mcp/src/connection_manager.rs`\n- Modify: `lib/crates/fabro-agent/src/session.rs`\n- Test: relevant `fabro-mcp` or `fabro-agent` MCP tests.\n\n- [ ] **Step 1: Add deterministic tool summaries**\n\nExpose a helper on `McpConnectionManager` or compute in `Session` from `all_tools()`:\n\n- filter tools by `server_name`\n- return qualified tool name as `name`\n- return server-provided tool name as `original_name`\n- sort by qualified `name`\n\n- [ ] **Step 2: Enrich ready emissions**\n\nWhen emitting `AgentEvent::McpServerReady`, include the tool summaries for that server. Keep existing `server_name` and `tool_count`.\n\n- [ ] **Step 3: Add MCP tests**\n\nCover:\n\n- ready event includes only tools from the ready server\n- summaries are sorted by qualified name\n- `name` is the Fabro-qualified MCP tool name\n- `original_name` is the server-provided tool name\n- descriptions and input schemas are not included\n\n---\n\n### Task 6: Update Event Documentation\n\n**Files:**\n- Modify: `docs/internal/events.md`\n\n- [ ] **Step 1: Document new events**\n\nAdd sections for:\n\n- `agent.memory.loaded`\n- `agent.skills.discovered`\n- `agent.skill.activated`\n\nFor `agent.memory.loaded`, explicitly state that file contents are excluded.\n\n- [ ] **Step 2: Update MCP ready docs**\n\nUpdate `agent.mcp.ready` to show:\n\n```json\n{\n \"server_name\": \"github\",\n \"tool_count\": 2,\n \"tools\": [\n {\n \"name\": \"mcp__github__create_issue\",\n \"original_name\": \"create_issue\"\n }\n ],\n \"visit\": 1\n}\n```\n\n- [ ] **Step 3: Record skill event replacement**\n\nIf `agent.skill.expanded` remains in internal code or docs, mark it internal-only or replaced by `agent.skill.activated`.\n\n---\n\n### Task 7: Verify\n\n**Files:**\n- No new files unless test placement requires it.\n\n- [ ] **Step 1: Run focused tests**\n\nRun:\n\n```bash\ncargo nextest run -p fabro-agent -p fabro-workflow -p fabro-types -p fabro-mcp\n```\n\n- [ ] **Step 2: Run formatting**\n\nRun:\n\n```bash\ncargo +nightly-2026-04-14 fmt --all\n```\n\n- [ ] **Step 3: Run clippy for touched crates or workspace**\n\nPrefer the workspace command if time permits:\n\n```bash\ncargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings\n```\n\n- [ ] **Step 4: Final sanity checks**\n\nConfirm:\n\n- memory events never contain file contents\n- skills discovered and memory loaded are emitted even for empty lists\n- skill activation is persisted rather than filtered as streaming noise\n- `agent.mcp.ready` remains backwards-compatible for old events without `tools`\n- docs match the serialized event names and payload shapes\n\n", + "graph.rankdir": "LR", + "internal.thread_id": "implement", + "last_stage": "simplify_opus", + "thread.start.current_node": "toolchain", + "internal.work_dir": "/home/daytona/workspace/fabro", + "outcome": "succeeded", + "response.implement": "Let me do a quick sanity check on the plan's \"final sanity checks\":\n\n1. ✅ Memory events never contain file contents — verified by test `agent_memory_loaded_payload_excludes_file_contents` and structural design.\n2. ✅ Skills discovered and memory loaded emitted even for empty lists — verified by tests `initialize_emits_memory_loaded_event_with_empty_files_when_no_memory` and `initialize_emits_skills_discovered_event_when_no_skills`.\n3. ✅ Skill activation is persisted rather than filtered as streaming noise — `SkillActivated` is not in `is_streaming_noise()` and has conversion + name mapping.\n4. ✅ `agent.mcp.ready` backwards-compatible — verified by `agent_mcp_ready_deserializes_legacy_payload_without_tools` test and `#[serde(default, skip_serializing_if = \"Vec::is_empty\")]`.\n5. ✅ Docs match the serialized event names and payload shapes.\n\nSummary of changes:\n\n**Task 1 (fabro-types):** Added `AgentMemoryLoadedProps`, `AgentMemoryFileProps`, `AgentSkillsDiscoveredProps`, `AgentSkillSummary`, `AgentSkillActivationSource`, `AgentSkillActivatedProps`, `AgentMcpToolSummary`. Extended `AgentMcpReadyProps` with a backwards-compatible `tools` field. Added 3 new `EventBody` variants (`AgentMemoryLoaded`, `AgentSkillsDiscovered`, `AgentSkillActivated`) plus the legacy whitelist entries. Added 5 serde tests.\n\n**Task 2 (fabro-agent / fabro-workflow):** Added internal `AgentEvent::MemoryLoaded`, `SkillsDiscovered`, `SkillActivated` variants plus support structs (`MemoryFileSummary`, `SkillSummary`, `McpToolSummary`, `SkillActivationSource`). Extended `AgentEvent::McpServerReady` with `tools`. Added tracing for the new variants. Updated workflow `convert.rs` and `names.rs`. Added 4 conversion tests.\n\n**Task 3 (memory):** Changed `discover_memory` to return `Vec` with `{path, content, byte_count, loaded_bytes, truncated}`. `Session::initialize` emits `MemoryLoaded` immediately after discovery, always (even for empty results). The prompt assembly receives content-only strings. Memory test added for truncation metadata.\n\n**Task 4 (skills):** `Session::initialize` emits `SkillsDiscovered` (always). Slash expansion now emits `SkillActivated { Slash }` instead of `SkillExpanded`. `make_use_skill_tool` emits `SkillActivated { Tool }` only on successful lookup. Failed lookups do not emit. Added 4 session tests.\n\n**Task 5 (MCP):** Added `McpConnectionManager::tool_summaries_for_server` that filters/sorts by qualified name. `Session::initialize` includes the summaries in `McpServerReady`. Existing MCP e2e test now asserts the names-only summary content. Added an mcp unit test.\n\n**Task 6 (docs):** Updated `docs/internal/events.md` with the enriched `agent.mcp.ready` payload, three new sections for `agent.memory.loaded`, `agent.skills.discovered`, `agent.skill.activated`, and a note about the retired `agent.skill.expanded` notification.\n\n**Task 7:** All 1841 tests in `fabro-agent`, `fabro-workflow`, `fabro-types`, `fabro-mcp` pass; workspace clippy clean under nightly-2026-04-14; formatting checked." + }, + "node_outcomes": { + "simplify_opus": { + "status": "succeeded", + "context_updates": { + "response.simplify_opus": "All checks green. Here's the summary:\n\n## Summary\n\nThree review agents flagged overlapping issues. I applied the high-value cleanups and skipped low-value or out-of-scope ones.\n\n### Fixed\n1. **Eliminated 4 duplicate types** between `fabro-agent` and `fabro-types` (`McpToolSummary`, `MemoryFileSummary`, `SkillSummary`, `SkillActivationSource`). `AgentEvent` now embeds the `fabro-types` versions directly, mirroring the existing `TodoCreated/Updated/Deleted` pattern. Removed ~50 lines of field-by-field cloning in `convert.rs`.\n2. **Deleted dead `SkillExpanded` variant** entirely — its emit site had already been replaced with `SkillActivated { source: Slash }`. Removed from `types.rs` (variant, streaming-noise classification, trace arm, test), `convert.rs` (panic-list), and `names.rs` (name mapping).\n3. **Strongly typed `provider_profile`** as `fabro_model::AgentProfileKind` (Copy enum with existing `Display`) on both wire props and in-memory variants instead of `String`. Removed two `.to_string()` calls and per-event clones.\n4. **Dropped redundant derived fields** from in-memory variants:\n - `MemoryDocument.loaded_bytes` → always equals `content.len()`.\n - `AgentEvent::McpServerReady.tool_count` → always equals `tools.len()`.\n - `AgentEvent::MemoryLoaded.total_loaded_bytes` → sum of `files[].loaded_bytes`.\n - All three are computed at the conversion boundary so the wire shape is preserved.\n\n### Skipped (with reason)\n- **`build_system_prompt` signature change** (memory clone elimination): touches 6 files including public SDK reference docs and 4 profile impls for a single 32 KiB clone at session init. Not worth the API churn.\n- **MCP tool N+1 fix**: one-shot pass at session startup, not a hot path.\n- **`ToolContext` Option accretion**: out of scope for this PR; should be its own refactor when the next field is added.\n- **Two-pass over memory in init**: micro-optimization, current code is clearer.\n\n### Verification\n- `cargo check --workspace --all-targets` — clean\n- `cargo nextest run -p fabro-agent -p fabro-workflow -p fabro-types -p fabro-mcp` — 1840 passed, 0 failed\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` — clean\n- `cargo +nightly-2026-04-14 fmt --all` — applied\n\nNet change: removed ~100 lines while preserving all behavior, wire format, and test coverage.", + "last_response": "All checks green. Here's the summary:\n\n## Summary\n\nThree review agents flagged overlapping issues. I applied the high-value cleanups and skipped low-value or out-of-scope ones.\n\n### Fixed\n1. **Elimina", + "last_stage": "simplify_opus" + }, + "notes": "Stage completed: simplify_opus", + "usage": { + "input": { + "usage": { + "model": { + "provider": "anthropic", + "model_id": "claude-opus-4-7" + }, + "tokens": { + "input_tokens": 119287, + "output_tokens": 38365, + "reasoning_tokens": 0, + "cache_read_tokens": 8304451, + "cache_write_tokens": 129921 + } + }, + "facts": { + "algorithm": "anthropic", + "cache_write_5m_tokens": 129921, + "cache_write_1h_tokens": 0 + } + }, + "total_usd_micros": 6519791 + }, + "files_touched": [ + "/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/lib.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/memory.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/session.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/skills.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/types.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/src/lib.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/src/run_event/agent.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/src/run_event/mod.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/event/convert.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/event/names.rs" + ] + }, + "implement": { + "status": "succeeded", + "context_updates": { + "last_response": "Let me do a quick sanity check on the plan's \"final sanity checks\":\n\n1. ✅ Memory events never contain file contents — verified by test `agent_memory_loaded_payload_excludes_file_contents` and stru", + "response.implement": "Let me do a quick sanity check on the plan's \"final sanity checks\":\n\n1. ✅ Memory events never contain file contents — verified by test `agent_memory_loaded_payload_excludes_file_contents` and structural design.\n2. ✅ Skills discovered and memory loaded emitted even for empty lists — verified by tests `initialize_emits_memory_loaded_event_with_empty_files_when_no_memory` and `initialize_emits_skills_discovered_event_when_no_skills`.\n3. ✅ Skill activation is persisted rather than filtered as streaming noise — `SkillActivated` is not in `is_streaming_noise()` and has conversion + name mapping.\n4. ✅ `agent.mcp.ready` backwards-compatible — verified by `agent_mcp_ready_deserializes_legacy_payload_without_tools` test and `#[serde(default, skip_serializing_if = \"Vec::is_empty\")]`.\n5. ✅ Docs match the serialized event names and payload shapes.\n\nSummary of changes:\n\n**Task 1 (fabro-types):** Added `AgentMemoryLoadedProps`, `AgentMemoryFileProps`, `AgentSkillsDiscoveredProps`, `AgentSkillSummary`, `AgentSkillActivationSource`, `AgentSkillActivatedProps`, `AgentMcpToolSummary`. Extended `AgentMcpReadyProps` with a backwards-compatible `tools` field. Added 3 new `EventBody` variants (`AgentMemoryLoaded`, `AgentSkillsDiscovered`, `AgentSkillActivated`) plus the legacy whitelist entries. Added 5 serde tests.\n\n**Task 2 (fabro-agent / fabro-workflow):** Added internal `AgentEvent::MemoryLoaded`, `SkillsDiscovered`, `SkillActivated` variants plus support structs (`MemoryFileSummary`, `SkillSummary`, `McpToolSummary`, `SkillActivationSource`). Extended `AgentEvent::McpServerReady` with `tools`. Added tracing for the new variants. Updated workflow `convert.rs` and `names.rs`. Added 4 conversion tests.\n\n**Task 3 (memory):** Changed `discover_memory` to return `Vec` with `{path, content, byte_count, loaded_bytes, truncated}`. `Session::initialize` emits `MemoryLoaded` immediately after discovery, always (even for empty results). The prompt assembly receives content-only strings. Memory test added for truncation metadata.\n\n**Task 4 (skills):** `Session::initialize` emits `SkillsDiscovered` (always). Slash expansion now emits `SkillActivated { Slash }` instead of `SkillExpanded`. `make_use_skill_tool` emits `SkillActivated { Tool }` only on successful lookup. Failed lookups do not emit. Added 4 session tests.\n\n**Task 5 (MCP):** Added `McpConnectionManager::tool_summaries_for_server` that filters/sorts by qualified name. `Session::initialize` includes the summaries in `McpServerReady`. Existing MCP e2e test now asserts the names-only summary content. Added an mcp unit test.\n\n**Task 6 (docs):** Updated `docs/internal/events.md` with the enriched `agent.mcp.ready` payload, three new sections for `agent.memory.loaded`, `agent.skills.discovered`, `agent.skill.activated`, and a note about the retired `agent.skill.expanded` notification.\n\n**Task 7:** All 1841 tests in `fabro-agent`, `fabro-workflow`, `fabro-types`, `fabro-mcp` pass; workspace clippy clean under nightly-2026-04-14; formatting checked.", + "last_stage": "implement" + }, + "notes": "Stage completed: implement", + "usage": { + "input": { + "usage": { + "model": { + "provider": "anthropic", + "model_id": "claude-opus-4-7" + }, + "tokens": { + "input_tokens": 220090, + "output_tokens": 51681, + "reasoning_tokens": 0, + "cache_read_tokens": 24871894, + "cache_write_tokens": 231090 + } + }, + "facts": { + "algorithm": "anthropic", + "cache_write_5m_tokens": 231090, + "cache_write_1h_tokens": 0 + } + }, + "total_usd_micros": 16272734 + }, + "files_touched": [ + "/home/daytona/workspace/fabro/docs/internal/events.md", + "/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/lib.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/memory.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/session.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/skills.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/types.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-mcp/src/connection_manager.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/src/run_event/agent.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/src/run_event/mod.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/event/convert.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/event/names.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/handler/prompt.rs" + ] + }, + "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 + }, + "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 + } + }, + "next_node_id": "simplify_gpt", + "git_commit_sha": "322d835928816d3201bb18b2b1933094fc490879", + "node_visits": { + "start": 1, + "preflight_compile": 1, + "simplify_opus": 1, + "preflight_lint": 1, + "toolchain": 1, + "implement": 1 + } + }, + "diff": { + "patch": "diff --git a/lib/crates/fabro-agent/src/lib.rs b/lib/crates/fabro-agent/src/lib.rs\nindex 00be22526..13e6aa049 100644\n--- a/lib/crates/fabro-agent/src/lib.rs\n+++ b/lib/crates/fabro-agent/src/lib.rs\n@@ -70,10 +70,7 @@ pub use tools::{\n make_shell_tool, make_shell_tool_with_config, make_write_file_tool, register_core_tools,\n };\n pub use truncation::{TruncationMode, truncate_lines, truncate_output, truncate_tool_output};\n-pub use types::{\n- AgentEvent, McpToolSummary, MemoryFileSummary, Message, SessionEvent, SessionState,\n- SkillActivationSource, SkillSummary,\n-};\n+pub use types::{AgentEvent, Message, SessionEvent, SessionState};\n \n #[cfg(test)]\n #[allow(\ndiff --git a/lib/crates/fabro-agent/src/memory.rs b/lib/crates/fabro-agent/src/memory.rs\nindex 07e59c6da..7d413556c 100644\n--- a/lib/crates/fabro-agent/src/memory.rs\n+++ b/lib/crates/fabro-agent/src/memory.rs\n@@ -11,14 +11,14 @@ pub const BUDGET_BYTES: usize = 32768;\n \n /// One discovered memory file. `content` is what gets inlined into the\n /// system prompt. The remaining fields describe the file for\n-/// observability and never carry the file's text.\n+/// observability and never carry the file's text. The number of bytes\n+/// actually loaded into the prompt is `content.len()`.\n #[derive(Debug, Clone, PartialEq)]\n pub struct MemoryDocument {\n- pub path: String,\n- pub content: String,\n- pub byte_count: usize,\n- pub loaded_bytes: usize,\n- pub truncated: bool,\n+ pub path: String,\n+ pub content: String,\n+ pub byte_count: usize,\n+ pub truncated: bool,\n }\n \n pub async fn discover_memory(\n@@ -67,7 +67,6 @@ pub async fn discover_memory(\n path,\n content,\n byte_count,\n- loaded_bytes: byte_count,\n truncated: false,\n });\n } else if budget_remaining > 0 {\n@@ -78,13 +77,11 @@ pub async fn discover_memory(\n \"Project doc truncated to fit budget\"\n );\n let truncated = truncate_to_budget(&content, budget_remaining);\n- let loaded_bytes = truncated.len();\n budget_remaining = 0;\n results.push(MemoryDocument {\n path,\n content: truncated,\n byte_count,\n- loaded_bytes,\n truncated: true,\n });\n } else {\n@@ -94,7 +91,7 @@ pub async fn discover_memory(\n }\n }\n \n- let total_bytes: usize = results.iter().map(|doc| doc.loaded_bytes).sum();\n+ let total_bytes: usize = results.iter().map(|doc| doc.content.len()).sum();\n info!(files = results.len(), total_bytes, \"Project docs loaded\");\n \n Ok(results)\n@@ -173,7 +170,7 @@ mod tests {\n assert_eq!(docs[0].content, \"Agent instructions\");\n assert_eq!(docs[0].path, \"/repo/AGENTS.md\");\n assert_eq!(docs[0].byte_count, \"Agent instructions\".len());\n- assert_eq!(docs[0].loaded_bytes, \"Agent instructions\".len());\n+ assert_eq!(docs[0].content.len(), docs[0].byte_count);\n assert!(!docs[0].truncated);\n }\n \n@@ -262,7 +259,7 @@ mod tests {\n assert_eq!(docs.len(), 2);\n assert_eq!(docs[0].content, large_content);\n assert!(!docs[0].truncated);\n- assert_eq!(docs[0].byte_count, docs[0].loaded_bytes);\n+ assert_eq!(docs[0].byte_count, docs[0].content.len());\n // Second doc should be truncated to fit remaining budget\n assert!(\n docs[1]\n@@ -270,7 +267,7 @@ mod tests {\n .ends_with(\"[Project instructions truncated at 32KB]\")\n );\n assert!(docs[1].truncated);\n- assert!(docs[1].byte_count > docs[1].loaded_bytes);\n+ assert!(docs[1].byte_count > docs[1].content.len());\n assert!(docs[0].content.len() + docs[1].content.len() <= BUDGET_BYTES);\n }\n \n@@ -342,8 +339,8 @@ mod tests {\n assert_eq!(docs.len(), 1);\n assert!(docs[0].truncated);\n assert_eq!(docs[0].byte_count, large_content.len());\n- assert!(docs[0].loaded_bytes < docs[0].byte_count);\n- assert!(docs[0].loaded_bytes <= BUDGET_BYTES);\n+ assert!(docs[0].content.len() < docs[0].byte_count);\n+ assert!(docs[0].content.len() <= BUDGET_BYTES);\n }\n \n #[tokio::test]\ndiff --git a/lib/crates/fabro-agent/src/session.rs b/lib/crates/fabro-agent/src/session.rs\nindex 4163e70ed..30a1b1201 100644\n--- a/lib/crates/fabro-agent/src/session.rs\n+++ b/lib/crates/fabro-agent/src/session.rs\n@@ -39,10 +39,7 @@ use crate::skills::{\n };\n use crate::subagent::{SubAgentCallbackEvent, SubAgentEventCallback, SubAgentManager};\n use crate::tool_execution::execute_tool_calls;\n-use crate::types::{\n- AgentEvent, McpToolSummary, MemoryFileSummary, Message, SessionEvent, SessionState,\n- SkillActivationSource, SkillSummary,\n-};\n+use crate::types::{AgentEvent, Message, SessionEvent, SessionState};\n \n /// One queued external control item for a live session.\n #[derive(Debug, Clone)]\n@@ -492,26 +489,24 @@ impl Session {\n )\n .await?;\n \n- let provider_profile_name = self.provider_profile.profile_kind().to_string();\n+ let provider_profile = self.provider_profile.profile_kind();\n \n // Emit memory loaded event with file metadata. Contents are deliberately\n // omitted so the durable event stream never carries file bytes.\n- let memory_files: Vec = self\n+ let memory_files: Vec = self\n .memory\n .iter()\n- .map(|doc| MemoryFileSummary {\n+ .map(|doc| fabro_types::AgentMemoryFileProps {\n path: doc.path.clone(),\n byte_count: doc.byte_count,\n- loaded_bytes: doc.loaded_bytes,\n+ loaded_bytes: doc.content.len(),\n truncated: doc.truncated,\n })\n .collect();\n- let total_loaded_bytes: usize = self.memory.iter().map(|doc| doc.loaded_bytes).sum();\n self.event_emitter\n .emit(self.id.clone(), AgentEvent::MemoryLoaded {\n- provider_profile: provider_profile_name.clone(),\n+ provider_profile,\n files: memory_files,\n- total_loaded_bytes,\n budget_bytes: BUDGET_BYTES,\n });\n \n@@ -526,19 +521,19 @@ impl Session {\n self.skills = discover_skills(self.sandbox.as_ref(), &skill_dirs, &cancel_token).await?;\n debug!(skill_count = self.skills.len(), \"Skills discovered\");\n \n- let skill_summaries: Vec = self\n+ let skill_summaries: Vec = self\n .skills\n .iter()\n- .map(|skill| SkillSummary {\n+ .map(|skill| fabro_types::AgentSkillSummary {\n name: skill.name.clone(),\n description: skill.description.clone(),\n })\n .collect();\n self.event_emitter\n .emit(self.id.clone(), AgentEvent::SkillsDiscovered {\n- provider_profile: provider_profile_name.clone(),\n- source_dirs: skill_dirs.clone(),\n- skills: skill_summaries,\n+ provider_profile,\n+ source_dirs: skill_dirs.clone(),\n+ skills: skill_summaries,\n });\n \n // Register use_skill tool when skills are available\n@@ -562,11 +557,11 @@ impl Session {\n \n for (server_name, result) in &results {\n match result {\n- Ok(tool_count) => {\n+ Ok(_) => {\n let tools = manager\n .tool_summaries_for_server(server_name)\n .into_iter()\n- .map(|(name, original_name)| McpToolSummary {\n+ .map(|(name, original_name)| fabro_types::AgentMcpToolSummary {\n name,\n original_name,\n })\n@@ -574,7 +569,6 @@ impl Session {\n self.event_emitter\n .emit(self.id.clone(), AgentEvent::McpServerReady {\n server_name: server_name.clone(),\n- tool_count: *tool_count,\n tools,\n });\n }\n@@ -1194,7 +1188,7 @@ impl Session {\n self.event_emitter\n .emit(self.id.clone(), AgentEvent::SkillActivated {\n skill_name: name.clone(),\n- source: SkillActivationSource::Slash,\n+ source: fabro_types::AgentSkillActivationSource::Slash,\n });\n }\n let expanded_input = expanded.text;\n@@ -3709,14 +3703,8 @@ mod tests {\n // summaries pulled from the connection manager.\n let mut mcp_ready = false;\n while let Ok(event) = rx.try_recv() {\n- if let AgentEvent::McpServerReady {\n- server_name,\n- tool_count,\n- tools,\n- } = &event.event\n- {\n+ if let AgentEvent::McpServerReady { server_name, tools } = &event.event {\n assert_eq!(server_name, \"test-echo\");\n- assert_eq!(*tool_count, 1);\n assert_eq!(tools.len(), 1);\n assert_eq!(tools[0].name, \"mcp__test_echo__echo\");\n assert_eq!(tools[0].original_name, \"echo\");\n@@ -3957,20 +3945,18 @@ mod tests {\n while let Ok(envelope) = rx.try_recv() {\n if let AgentEvent::MemoryLoaded {\n files,\n- total_loaded_bytes,\n budget_bytes,\n provider_profile,\n } = envelope.event\n {\n- memory_event = Some((files, total_loaded_bytes, budget_bytes, provider_profile));\n+ memory_event = Some((files, budget_bytes, provider_profile));\n break;\n }\n }\n- let (files, total_loaded_bytes, budget_bytes, provider_profile) =\n+ let (files, budget_bytes, provider_profile) =\n memory_event.expect(\"MemoryLoaded should be emitted\");\n- assert_eq!(provider_profile, \"anthropic\");\n+ assert_eq!(provider_profile, fabro_model::AgentProfileKind::Anthropic);\n assert_eq!(budget_bytes, 32768);\n- assert_eq!(total_loaded_bytes, \"Hello world\".len());\n assert_eq!(files.len(), 1);\n assert_eq!(files[0].path, \"/home/test/AGENTS.md\");\n assert_eq!(files[0].byte_count, \"Hello world\".len());\n@@ -4039,7 +4025,7 @@ mod tests {\n }\n let (provider_profile, source_dirs, skills) =\n got.expect(\"SkillsDiscovered must be emitted\");\n- assert_eq!(provider_profile, \"anthropic\");\n+ assert_eq!(provider_profile, fabro_model::AgentProfileKind::Anthropic);\n assert_eq!(source_dirs, vec![\"/skills\".to_string()]);\n assert_eq!(skills.len(), 1);\n assert_eq!(skills[0].name, \"commit\");\n@@ -4098,16 +4084,15 @@ mod tests {\n let mut rx = session.subscribe();\n session.process_input(\"/commit fix things\").await.unwrap();\n \n- let mut activations: Vec<(String, SkillActivationSource)> = Vec::new();\n+ let mut activations: Vec<(String, fabro_types::AgentSkillActivationSource)> = Vec::new();\n while let Ok(envelope) = rx.try_recv() {\n if let AgentEvent::SkillActivated { skill_name, source } = envelope.event {\n activations.push((skill_name, source));\n }\n }\n assert!(\n- activations\n- .iter()\n- .any(|(name, source)| name == \"commit\" && *source == SkillActivationSource::Slash),\n+ activations.iter().any(|(name, source)| name == \"commit\"\n+ && *source == fabro_types::AgentSkillActivationSource::Slash),\n \"expected slash skill activation, got {activations:?}\"\n );\n }\n@@ -4150,7 +4135,8 @@ mod tests {\n let mut tool_activations = 0;\n while let Ok(envelope) = rx.try_recv() {\n if let AgentEvent::SkillActivated { source, skill_name } = envelope.event {\n- if source == SkillActivationSource::Tool && skill_name == \"commit\" {\n+ if source == fabro_types::AgentSkillActivationSource::Tool && skill_name == \"commit\"\n+ {\n tool_activations += 1;\n }\n }\ndiff --git a/lib/crates/fabro-agent/src/skills.rs b/lib/crates/fabro-agent/src/skills.rs\nindex f7b4e3eaf..34c6e126b 100644\n--- a/lib/crates/fabro-agent/src/skills.rs\n+++ b/lib/crates/fabro-agent/src/skills.rs\n@@ -7,7 +7,7 @@ use crate::error::{Error, InterruptReason};\n use crate::sandbox::Sandbox;\n use crate::tool_registry::RegisteredTool;\n use crate::tools::required_str;\n-use crate::types::{AgentEvent, SkillActivationSource};\n+use crate::types::AgentEvent;\n \n #[derive(Debug, Clone)]\n pub struct Skill {\n@@ -187,7 +187,7 @@ pub fn make_use_skill_tool(skills: Arc>) -> RegisteredTool {\n .ok_or_else(|| format!(\"Unknown skill: {name}\"))?;\n ctx.emit_agent_event(AgentEvent::SkillActivated {\n skill_name: name.to_string(),\n- source: SkillActivationSource::Tool,\n+ source: fabro_types::AgentSkillActivationSource::Tool,\n });\n Ok(skill.template.clone())\n })\ndiff --git a/lib/crates/fabro-agent/src/types.rs b/lib/crates/fabro-agent/src/types.rs\nindex 95833a2d8..9e497bbd0 100644\n--- a/lib/crates/fabro-agent/src/types.rs\n+++ b/lib/crates/fabro-agent/src/types.rs\n@@ -3,7 +3,7 @@ use std::time::SystemTime;\n use chrono::{DateTime, Utc};\n use fabro_llm::Error as LlmError;\n use fabro_llm::types::{ContentPart, ThinkingData, TokenCounts, ToolCall, ToolResult};\n-use fabro_model::ModelRef;\n+use fabro_model::{AgentProfileKind, ModelRef};\n use fabro_types::SessionMessage;\n use serde::de::DeserializeOwned;\n use serde::{Deserialize, Serialize};\n@@ -194,33 +194,6 @@ pub enum SessionState {\n Closed,\n }\n \n-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\n-pub struct MemoryFileSummary {\n- pub path: String,\n- pub byte_count: usize,\n- pub loaded_bytes: usize,\n- pub truncated: bool,\n-}\n-\n-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\n-pub struct SkillSummary {\n- pub name: String,\n- pub description: String,\n-}\n-\n-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\n-pub struct McpToolSummary {\n- pub name: String,\n- pub original_name: String,\n-}\n-\n-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\n-#[serde(rename_all = \"snake_case\")]\n-pub enum SkillActivationSource {\n- Slash,\n- Tool,\n-}\n-\n #[derive(Debug, Clone, Serialize, Deserialize)]\n pub enum AgentEvent {\n SessionStarted {\n@@ -279,9 +252,6 @@ pub enum AgentEvent {\n TurnLimitReached {\n max_turns: usize,\n },\n- SkillExpanded {\n- skill_name: String,\n- },\n SteeringInjected {\n text: String,\n /// Principal that authored the steer. Lifted to top-level\n@@ -329,27 +299,25 @@ pub enum AgentEvent {\n },\n McpServerReady {\n server_name: String,\n- tool_count: usize,\n- tools: Vec,\n+ tools: Vec,\n },\n McpServerFailed {\n server_name: String,\n error: String,\n },\n MemoryLoaded {\n- provider_profile: String,\n- files: Vec,\n- total_loaded_bytes: usize,\n- budget_bytes: usize,\n+ provider_profile: AgentProfileKind,\n+ files: Vec,\n+ budget_bytes: usize,\n },\n SkillsDiscovered {\n- provider_profile: String,\n+ provider_profile: AgentProfileKind,\n source_dirs: Vec,\n- skills: Vec,\n+ skills: Vec,\n },\n SkillActivated {\n skill_name: String,\n- source: SkillActivationSource,\n+ source: fabro_types::AgentSkillActivationSource,\n },\n /// New todo / task was created. Carries the full row so the projection\n /// can be reconstructed from `todo.created` alone.\n@@ -373,7 +341,6 @@ impl AgentEvent {\n | Self::TextDelta { .. }\n | Self::ReasoningDelta { .. }\n | Self::ToolCallOutputDelta { .. }\n- | Self::SkillExpanded { .. }\n )\n }\n \n@@ -463,9 +430,6 @@ impl AgentEvent {\n Self::TurnLimitReached { max_turns } => {\n warn!(session_id, max_turns, \"Message limit reached\");\n }\n- Self::SkillExpanded { skill_name } => {\n- debug!(session_id, skill = skill_name.as_str(), \"Skill expanded\");\n- }\n Self::SteeringInjected { text, .. } => {\n debug!(session_id, text_len = text.len(), \"Steering injected\");\n }\n@@ -544,30 +508,24 @@ impl AgentEvent {\n Self::SubAgentClosed { agent_id, depth } => {\n debug!(session_id, agent_id, depth, \"Sub-agent closed\");\n }\n- Self::McpServerReady {\n- server_name,\n- tool_count,\n- tools,\n- } => {\n+ Self::McpServerReady { server_name, tools } => {\n info!(\n session_id,\n server = server_name.as_str(),\n- tool_count,\n- summary_count = tools.len(),\n+ tool_count = tools.len(),\n \"MCP server ready\"\n );\n }\n Self::MemoryLoaded {\n provider_profile,\n files,\n- total_loaded_bytes,\n budget_bytes,\n } => {\n info!(\n session_id,\n- provider_profile = provider_profile.as_str(),\n+ provider_profile = %provider_profile,\n file_count = files.len(),\n- total_loaded_bytes,\n+ total_loaded_bytes = files.iter().map(|f| f.loaded_bytes).sum::(),\n budget_bytes,\n \"Agent memory loaded\"\n );\n@@ -579,7 +537,7 @@ impl AgentEvent {\n } => {\n info!(\n session_id,\n- provider_profile = provider_profile.as_str(),\n+ provider_profile = %provider_profile,\n skill_count = skills.len(),\n source_dir_count = source_dirs.len(),\n \"Agent skills discovered\"\n@@ -690,16 +648,6 @@ mod tests {\n }));\n }\n \n- #[test]\n- fn skill_expanded_constructible() {\n- let event = AgentEvent::SkillExpanded {\n- skill_name: \"commit\".into(),\n- };\n- assert!(\n- matches!(event, AgentEvent::SkillExpanded { skill_name } if skill_name == \"commit\")\n- );\n- }\n-\n #[test]\n fn subagent_spawned_constructible() {\n let event = AgentEvent::SubAgentSpawned {\n@@ -832,13 +780,12 @@ mod tests {\n fn mcp_server_ready_constructible() {\n let event = AgentEvent::McpServerReady {\n server_name: \"filesystem\".into(),\n- tool_count: 3,\n tools: Vec::new(),\n };\n- assert!(matches!(event, AgentEvent::McpServerReady {\n- tool_count: 3,\n- ..\n- }));\n+ assert!(matches!(\n+ event,\n+ AgentEvent::McpServerReady { server_name, .. } if server_name == \"filesystem\"\n+ ));\n }\n \n #[test]\n@@ -857,7 +804,6 @@ mod tests {\n let events = vec![\n AgentEvent::McpServerReady {\n server_name: \"fs\".into(),\n- tool_count: 5,\n tools: Vec::new(),\n },\n AgentEvent::McpServerFailed {\n@@ -868,10 +814,10 @@ mod tests {\n let json = serde_json::to_string(&events).unwrap();\n let deserialized: Vec = serde_json::from_str(&json).unwrap();\n assert_eq!(deserialized.len(), 2);\n- assert!(matches!(&deserialized[0], AgentEvent::McpServerReady {\n- tool_count: 5,\n- ..\n- }));\n+ assert!(matches!(\n+ &deserialized[0],\n+ AgentEvent::McpServerReady { server_name, .. } if server_name == \"fs\"\n+ ));\n assert!(matches!(\n &deserialized[1],\n AgentEvent::McpServerFailed { .. }\ndiff --git a/lib/crates/fabro-types/src/lib.rs b/lib/crates/fabro-types/src/lib.rs\nindex 45f1d1f4d..2430a7cc3 100644\n--- a/lib/crates/fabro-types/src/lib.rs\n+++ b/lib/crates/fabro-types/src/lib.rs\n@@ -93,6 +93,7 @@ pub use run::{\n };\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 SessionCapability, TodoCreatedProps, TodoDeletedProps, TodoUpdatedProps,\ndiff --git a/lib/crates/fabro-types/src/run_event/agent.rs b/lib/crates/fabro-types/src/run_event/agent.rs\nindex 56106dc60..1666e6c43 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,3 +1,4 @@\n+use fabro_model::AgentProfileKind;\n use serde::{Deserialize, Serialize};\n use serde_json::Value;\n \n@@ -235,7 +236,7 @@ pub struct AgentMcpFailedProps {\n \n #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\n pub struct AgentMemoryLoadedProps {\n- pub provider_profile: String,\n+ pub provider_profile: AgentProfileKind,\n pub files: Vec,\n pub total_loaded_bytes: usize,\n pub budget_bytes: usize,\n@@ -252,7 +253,7 @@ pub struct AgentMemoryFileProps {\n \n #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\n pub struct AgentSkillsDiscoveredProps {\n- pub provider_profile: String,\n+ pub provider_profile: AgentProfileKind,\n pub source_dirs: Vec,\n pub skills: Vec,\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 564dea99a..91fba066d 100644\n--- a/lib/crates/fabro-types/src/run_event/mod.rs\n+++ b/lib/crates/fabro-types/src/run_event/mod.rs\n@@ -1999,7 +1999,7 @@ mod tests {\n #[test]\n fn agent_memory_loaded_serializes_with_canonical_name() {\n let body = EventBody::AgentMemoryLoaded(AgentMemoryLoadedProps {\n- provider_profile: \"anthropic\".to_string(),\n+ provider_profile: fabro_model::AgentProfileKind::Anthropic,\n files: vec![AgentMemoryFileProps {\n path: \"/repo/AGENTS.md\".to_string(),\n byte_count: 100,\n@@ -2029,7 +2029,7 @@ mod tests {\n #[test]\n fn agent_skills_discovered_serializes_with_canonical_name() {\n let body = EventBody::AgentSkillsDiscovered(AgentSkillsDiscoveredProps {\n- provider_profile: \"openai\".to_string(),\n+ provider_profile: fabro_model::AgentProfileKind::OpenAi,\n source_dirs: vec![\"/repo/.fabro/skills\".to_string()],\n skills: vec![AgentSkillSummary {\n name: \"commit\".to_string(),\ndiff --git a/lib/crates/fabro-workflow/src/event/convert.rs b/lib/crates/fabro-workflow/src/event/convert.rs\nindex c1e008159..bc038d71c 100644\n--- a/lib/crates/fabro-workflow/src/event/convert.rs\n+++ b/lib/crates/fabro-workflow/src/event/convert.rs\n@@ -719,22 +719,14 @@ fn event_body_from_event(event: &Event) -> EventBody {\n visit: *visit,\n })\n }\n- AgentEvent::McpServerReady {\n- server_name,\n- tool_count,\n- tools,\n- } => EventBody::AgentMcpReady(fabro_types::AgentMcpReadyProps {\n- server_name: server_name.clone(),\n- tool_count: *tool_count,\n- tools: tools\n- .iter()\n- .map(|tool| fabro_types::AgentMcpToolSummary {\n- name: tool.name.clone(),\n- original_name: tool.original_name.clone(),\n- })\n- .collect(),\n- visit: *visit,\n- }),\n+ AgentEvent::McpServerReady { server_name, tools } => {\n+ EventBody::AgentMcpReady(fabro_types::AgentMcpReadyProps {\n+ server_name: server_name.clone(),\n+ tool_count: tools.len(),\n+ tools: tools.clone(),\n+ visit: *visit,\n+ })\n+ }\n AgentEvent::McpServerFailed { server_name, error } => {\n EventBody::AgentMcpFailed(fabro_types::AgentMcpFailedProps {\n server_name: server_name.clone(),\n@@ -745,20 +737,11 @@ fn event_body_from_event(event: &Event) -> EventBody {\n AgentEvent::MemoryLoaded {\n provider_profile,\n files,\n- total_loaded_bytes,\n budget_bytes,\n } => EventBody::AgentMemoryLoaded(fabro_types::AgentMemoryLoadedProps {\n- provider_profile: provider_profile.clone(),\n- files: files\n- .iter()\n- .map(|file| fabro_types::AgentMemoryFileProps {\n- path: file.path.clone(),\n- byte_count: file.byte_count,\n- loaded_bytes: file.loaded_bytes,\n- truncated: file.truncated,\n- })\n- .collect(),\n- total_loaded_bytes: *total_loaded_bytes,\n+ provider_profile: *provider_profile,\n+ total_loaded_bytes: files.iter().map(|f| f.loaded_bytes).sum(),\n+ files: files.clone(),\n budget_bytes: *budget_bytes,\n visit: *visit,\n }),\n@@ -767,28 +750,15 @@ fn event_body_from_event(event: &Event) -> EventBody {\n source_dirs,\n skills,\n } => EventBody::AgentSkillsDiscovered(fabro_types::AgentSkillsDiscoveredProps {\n- provider_profile: provider_profile.clone(),\n+ provider_profile: *provider_profile,\n source_dirs: source_dirs.clone(),\n- skills: skills\n- .iter()\n- .map(|skill| fabro_types::AgentSkillSummary {\n- name: skill.name.clone(),\n- description: skill.description.clone(),\n- })\n- .collect(),\n+ skills: skills.clone(),\n visit: *visit,\n }),\n AgentEvent::SkillActivated { skill_name, source } => {\n EventBody::AgentSkillActivated(fabro_types::AgentSkillActivatedProps {\n skill_name: skill_name.clone(),\n- source: match source {\n- fabro_agent::SkillActivationSource::Slash => {\n- fabro_types::AgentSkillActivationSource::Slash\n- }\n- fabro_agent::SkillActivationSource::Tool => {\n- fabro_types::AgentSkillActivationSource::Tool\n- }\n- },\n+ source: *source,\n visit: *visit,\n })\n }\n@@ -800,7 +770,6 @@ fn event_body_from_event(event: &Event) -> EventBody {\n | AgentEvent::TextDelta { .. }\n | AgentEvent::ReasoningDelta { .. }\n | AgentEvent::ToolCallOutputDelta { .. }\n- | AgentEvent::SkillExpanded { .. }\n | AgentEvent::SessionStarted { .. }\n | AgentEvent::SessionEnded => panic!(\n \"agent event should not be converted through the stage-scoped Event::Agent wrapper\"\n@@ -2327,15 +2296,14 @@ mod tests {\n stage: \"code\".to_string(),\n visit: 3,\n event: AgentEvent::MemoryLoaded {\n- provider_profile: \"anthropic\".to_string(),\n- files: vec![fabro_agent::MemoryFileSummary {\n+ provider_profile: fabro_model::AgentProfileKind::Anthropic,\n+ files: vec![fabro_types::AgentMemoryFileProps {\n path: \"/repo/AGENTS.md\".to_string(),\n byte_count: 200,\n loaded_bytes: 200,\n truncated: false,\n }],\n- total_loaded_bytes: 200,\n- budget_bytes: 32768,\n+ budget_bytes: 32768,\n },\n session_id: Some(\"ses_1\".to_string()),\n parent_session_id: None,\n@@ -2345,7 +2313,10 @@ mod tests {\n match stored.body {\n EventBody::AgentMemoryLoaded(props) => {\n assert_eq!(props.visit, 3);\n- assert_eq!(props.provider_profile, \"anthropic\");\n+ assert_eq!(\n+ props.provider_profile,\n+ fabro_model::AgentProfileKind::Anthropic\n+ );\n assert_eq!(props.budget_bytes, 32768);\n assert_eq!(props.total_loaded_bytes, 200);\n assert_eq!(props.files.len(), 1);\n@@ -2364,15 +2335,14 @@ mod tests {\n stage: \"code\".to_string(),\n visit: 1,\n event: AgentEvent::MemoryLoaded {\n- provider_profile: \"openai\".to_string(),\n- files: vec![fabro_agent::MemoryFileSummary {\n+ provider_profile: fabro_model::AgentProfileKind::OpenAi,\n+ files: vec![fabro_types::AgentMemoryFileProps {\n path: \"/repo/AGENTS.md\".to_string(),\n byte_count: 100,\n loaded_bytes: 100,\n truncated: false,\n }],\n- total_loaded_bytes: 100,\n- budget_bytes: 32768,\n+ budget_bytes: 32768,\n },\n session_id: None,\n parent_session_id: None,\n@@ -2391,9 +2361,9 @@ mod tests {\n stage: \"code\".to_string(),\n visit: 2,\n event: AgentEvent::SkillsDiscovered {\n- provider_profile: \"anthropic\".to_string(),\n+ provider_profile: fabro_model::AgentProfileKind::Anthropic,\n source_dirs: vec![\"/repo/.fabro/skills\".to_string()],\n- skills: vec![fabro_agent::SkillSummary {\n+ skills: vec![fabro_types::AgentSkillSummary {\n name: \"commit\".to_string(),\n description: \"Make a commit\".to_string(),\n }],\n@@ -2406,7 +2376,10 @@ mod tests {\n match stored.body {\n EventBody::AgentSkillsDiscovered(props) => {\n assert_eq!(props.visit, 2);\n- assert_eq!(props.provider_profile, \"anthropic\");\n+ assert_eq!(\n+ props.provider_profile,\n+ fabro_model::AgentProfileKind::Anthropic\n+ );\n assert_eq!(props.source_dirs, vec![\"/repo/.fabro/skills\".to_string()]);\n assert_eq!(props.skills.len(), 1);\n assert_eq!(props.skills[0].name, \"commit\");\n@@ -2423,7 +2396,7 @@ mod tests {\n visit: 1,\n event: AgentEvent::SkillActivated {\n skill_name: \"commit\".to_string(),\n- source: fabro_agent::SkillActivationSource::Slash,\n+ source: fabro_types::AgentSkillActivationSource::Slash,\n },\n session_id: Some(\"ses_1\".to_string()),\n parent_session_id: None,\n@@ -2444,7 +2417,7 @@ mod tests {\n visit: 4,\n event: AgentEvent::SkillActivated {\n skill_name: \"review\".to_string(),\n- source: fabro_agent::SkillActivationSource::Tool,\n+ source: fabro_types::AgentSkillActivationSource::Tool,\n },\n session_id: None,\n parent_session_id: None,\n@@ -2467,13 +2440,12 @@ mod tests {\n visit: 5,\n event: AgentEvent::McpServerReady {\n server_name: \"github\".to_string(),\n- tool_count: 2,\n tools: vec![\n- fabro_agent::McpToolSummary {\n+ fabro_types::AgentMcpToolSummary {\n name: \"mcp__github__create_issue\".to_string(),\n original_name: \"create_issue\".to_string(),\n },\n- fabro_agent::McpToolSummary {\n+ fabro_types::AgentMcpToolSummary {\n name: \"mcp__github__list_issues\".to_string(),\n original_name: \"list_issues\".to_string(),\n },\ndiff --git a/lib/crates/fabro-workflow/src/event/names.rs b/lib/crates/fabro-workflow/src/event/names.rs\nindex d730c1872..51eea15c8 100644\n--- a/lib/crates/fabro-workflow/src/event/names.rs\n+++ b/lib/crates/fabro-workflow/src/event/names.rs\n@@ -78,7 +78,6 @@ pub fn event_name(event: &Event) -> &'static str {\n AgentEvent::Warning { .. } => \"agent.warning\",\n AgentEvent::LoopDetected => \"agent.loop.detected\",\n AgentEvent::TurnLimitReached { .. } => \"agent.turn.limit\",\n- AgentEvent::SkillExpanded { .. } => \"agent.skill.expanded\",\n AgentEvent::SteeringInjected { .. } => \"agent.steering.injected\",\n AgentEvent::CompactionStarted { .. } => \"agent.compaction.started\",\n AgentEvent::CompactionCompleted { .. } => \"agent.compaction.completed\",\n", + "summary": { + "files_changed": 13, + "additions": 1109, + "deletions": 91 + } + } + }, + { + "seq": 0, + "checkpoint": { + "timestamp": "2026-05-22T19:49:37.073882Z", + "current_node": "simplify_gpt", + "completed_nodes": [ + "start", + "toolchain", + "preflight_compile", + "preflight_lint", + "implement", + "simplify_opus", + "simplify_gpt" + ], + "node_retries": {}, "context_values": { "thread.toolchain.current_node": "preflight_compile", "graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ", "response.implement": "Let me do a quick sanity check on the plan's \"final sanity checks\":\n\n1. ✅ Memory events never contain file contents — verified by test `agent_memory_loaded_payload_excludes_file_contents` and structural design.\n2. ✅ Skills discovered and memory loaded emitted even for empty lists — verified by tests `initialize_emits_memory_loaded_event_with_empty_files_when_no_memory` and `initialize_emits_skills_discovered_event_when_no_skills`.\n3. ✅ Skill activation is persisted rather than filtered as streaming noise — `SkillActivated` is not in `is_streaming_noise()` and has conversion + name mapping.\n4. ✅ `agent.mcp.ready` backwards-compatible — verified by `agent_mcp_ready_deserializes_legacy_payload_without_tools` test and `#[serde(default, skip_serializing_if = \"Vec::is_empty\")]`.\n5. ✅ Docs match the serialized event names and payload shapes.\n\nSummary of changes:\n\n**Task 1 (fabro-types):** Added `AgentMemoryLoadedProps`, `AgentMemoryFileProps`, `AgentSkillsDiscoveredProps`, `AgentSkillSummary`, `AgentSkillActivationSource`, `AgentSkillActivatedProps`, `AgentMcpToolSummary`. Extended `AgentMcpReadyProps` with a backwards-compatible `tools` field. Added 3 new `EventBody` variants (`AgentMemoryLoaded`, `AgentSkillsDiscovered`, `AgentSkillActivated`) plus the legacy whitelist entries. Added 5 serde tests.\n\n**Task 2 (fabro-agent / fabro-workflow):** Added internal `AgentEvent::MemoryLoaded`, `SkillsDiscovered`, `SkillActivated` variants plus support structs (`MemoryFileSummary`, `SkillSummary`, `McpToolSummary`, `SkillActivationSource`). Extended `AgentEvent::McpServerReady` with `tools`. Added tracing for the new variants. Updated workflow `convert.rs` and `names.rs`. Added 4 conversion tests.\n\n**Task 3 (memory):** Changed `discover_memory` to return `Vec` with `{path, content, byte_count, loaded_bytes, truncated}`. `Session::initialize` emits `MemoryLoaded` immediately after discovery, always (even for empty results). The prompt assembly receives content-only strings. Memory test added for truncation metadata.\n\n**Task 4 (skills):** `Session::initialize` emits `SkillsDiscovered` (always). Slash expansion now emits `SkillActivated { Slash }` instead of `SkillExpanded`. `make_use_skill_tool` emits `SkillActivated { Tool }` only on successful lookup. Failed lookups do not emit. Added 4 session tests.\n\n**Task 5 (MCP):** Added `McpConnectionManager::tool_summaries_for_server` that filters/sorts by qualified name. `Session::initialize` includes the summaries in `McpServerReady`. Existing MCP e2e test now asserts the names-only summary content. Added an mcp unit test.\n\n**Task 6 (docs):** Updated `docs/internal/events.md` with the enriched `agent.mcp.ready` payload, three new sections for `agent.memory.loaded`, `agent.skills.discovered`, `agent.skill.activated`, and a note about the retired `agent.skill.expanded` notification.\n\n**Task 7:** All 1841 tests in `fabro-agent`, `fabro-workflow`, `fabro-types`, `fabro-mcp` pass; workspace clippy clean under nightly-2026-04-14; formatting checked.", - "last_response": "All checks green. Here's the summary:\n\n## Summary\n\nThree review agents flagged overlapping issues. I applied the high-value cleanups and skipped low-value or out-of-scope ones.\n\n### Fixed\n1. **Elimina", + "response.simplify_gpt": "Implemented review cleanup and validation.\n\nWhat I fixed:\n- Restored the durable `provider_profile` contract to `String` for:\n - `AgentMemoryLoadedProps`\n - `AgentSkillsDiscoveredProps`\n- Added explicit `loaded_bytes` to `MemoryDocument` instead of deriving it ad hoc from `content.len()`.\n- Reduced `fabro-agent` coupling to durable API props by adding internal agent summary types:\n - `MemoryFileSummary`\n - `SkillSummary`\n - `SkillActivationSource`\n - `McpToolSummary`\n- Restored explicit internal `tool_count` on `AgentEvent::McpServerReady`, while still carrying names-only tool summaries.\n- Updated workflow conversion to map internal agent summary types into `fabro-types` event props.\n- Updated tests to match the cleaned-up internal/durable boundaries.\n- Ran rustfmt.\n\nValidation passed:\n- `cargo nextest run -p fabro-agent -p fabro-workflow -p fabro-types -p fabro-mcp`\n - 1840 passed, 122 skipped\n- `cargo +nightly-2026-04-14 fmt --all`\n- `cargo +nightly-2026-04-14 clippy -q -p fabro-agent -p fabro-workflow -p fabro-types -p fabro-mcp --all-targets -- -D warnings`\n\nWorking tree now has cleanup modifications in 8 Rust files; no docs changes were needed during this review pass.", + "last_response": "Implemented review cleanup and validation.\n\nWhat I fixed:\n- Restored the durable `provider_profile` contract to `String` for:\n - `AgentMemoryLoadedProps`\n - `AgentSkillsDiscoveredProps`\n- Added expl", "failure_class": "", "internal.work_dir": "/home/daytona/workspace/fabro", "internal.retry_count.simplify_opus": 0, - "internal.thread_id": "implement", + "internal.thread_id": "simplify_opus", "internal.retry_count.preflight_compile": 0, "internal.fidelity": "compact", "outcome": "succeeded", "internal.retry_count.implement": 0, - "current_node": "simplify_opus", + "current_node": "simplify_gpt", + "thread.simplify_opus.current_node": "simplify_gpt", "graph.rankdir": "LR", + "internal.retry_count.simplify_gpt": 0, "internal.run_id": "01KS8GR1P97NYP93VVXWYCZHYZ", "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", "thread.implement.current_node": "simplify_opus", @@ -936,7 +1124,7 @@ "internal.node_visit_count": 1, "internal.retry_count.start": 0, "internal.retry_count.toolchain": 0, - "last_stage": "simplify_opus", + "last_stage": "simplify_gpt", "thread.start.current_node": "toolchain", "thread.preflight_lint.current_node": "implement", "thread.preflight_compile.current_node": "preflight_lint", @@ -944,6 +1132,90 @@ "response.simplify_opus": "All checks green. Here's the summary:\n\n## Summary\n\nThree review agents flagged overlapping issues. I applied the high-value cleanups and skipped low-value or out-of-scope ones.\n\n### Fixed\n1. **Eliminated 4 duplicate types** between `fabro-agent` and `fabro-types` (`McpToolSummary`, `MemoryFileSummary`, `SkillSummary`, `SkillActivationSource`). `AgentEvent` now embeds the `fabro-types` versions directly, mirroring the existing `TodoCreated/Updated/Deleted` pattern. Removed ~50 lines of field-by-field cloning in `convert.rs`.\n2. **Deleted dead `SkillExpanded` variant** entirely — its emit site had already been replaced with `SkillActivated { source: Slash }`. Removed from `types.rs` (variant, streaming-noise classification, trace arm, test), `convert.rs` (panic-list), and `names.rs` (name mapping).\n3. **Strongly typed `provider_profile`** as `fabro_model::AgentProfileKind` (Copy enum with existing `Display`) on both wire props and in-memory variants instead of `String`. Removed two `.to_string()` calls and per-event clones.\n4. **Dropped redundant derived fields** from in-memory variants:\n - `MemoryDocument.loaded_bytes` → always equals `content.len()`.\n - `AgentEvent::McpServerReady.tool_count` → always equals `tools.len()`.\n - `AgentEvent::MemoryLoaded.total_loaded_bytes` → sum of `files[].loaded_bytes`.\n - All three are computed at the conversion boundary so the wire shape is preserved.\n\n### Skipped (with reason)\n- **`build_system_prompt` signature change** (memory clone elimination): touches 6 files including public SDK reference docs and 4 profile impls for a single 32 KiB clone at session init. Not worth the API churn.\n- **MCP tool N+1 fix**: one-shot pass at session startup, not a hot path.\n- **`ToolContext` Option accretion**: out of scope for this PR; should be its own refactor when the next field is added.\n- **Two-pass over memory in init**: micro-optimization, current code is clearer.\n\n### Verification\n- `cargo check --workspace --all-targets` — clean\n- `cargo nextest run -p fabro-agent -p fabro-workflow -p fabro-types -p fabro-mcp` — 1840 passed, 0 failed\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` — clean\n- `cargo +nightly-2026-04-14 fmt --all` — applied\n\nNet change: removed ~100 lines while preserving all behavior, wire format, and test coverage." }, "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 + }, + "implement": { + "status": "succeeded", + "context_updates": { + "last_response": "Let me do a quick sanity check on the plan's \"final sanity checks\":\n\n1. ✅ Memory events never contain file contents — verified by test `agent_memory_loaded_payload_excludes_file_contents` and stru", + "response.implement": "Let me do a quick sanity check on the plan's \"final sanity checks\":\n\n1. ✅ Memory events never contain file contents — verified by test `agent_memory_loaded_payload_excludes_file_contents` and structural design.\n2. ✅ Skills discovered and memory loaded emitted even for empty lists — verified by tests `initialize_emits_memory_loaded_event_with_empty_files_when_no_memory` and `initialize_emits_skills_discovered_event_when_no_skills`.\n3. ✅ Skill activation is persisted rather than filtered as streaming noise — `SkillActivated` is not in `is_streaming_noise()` and has conversion + name mapping.\n4. ✅ `agent.mcp.ready` backwards-compatible — verified by `agent_mcp_ready_deserializes_legacy_payload_without_tools` test and `#[serde(default, skip_serializing_if = \"Vec::is_empty\")]`.\n5. ✅ Docs match the serialized event names and payload shapes.\n\nSummary of changes:\n\n**Task 1 (fabro-types):** Added `AgentMemoryLoadedProps`, `AgentMemoryFileProps`, `AgentSkillsDiscoveredProps`, `AgentSkillSummary`, `AgentSkillActivationSource`, `AgentSkillActivatedProps`, `AgentMcpToolSummary`. Extended `AgentMcpReadyProps` with a backwards-compatible `tools` field. Added 3 new `EventBody` variants (`AgentMemoryLoaded`, `AgentSkillsDiscovered`, `AgentSkillActivated`) plus the legacy whitelist entries. Added 5 serde tests.\n\n**Task 2 (fabro-agent / fabro-workflow):** Added internal `AgentEvent::MemoryLoaded`, `SkillsDiscovered`, `SkillActivated` variants plus support structs (`MemoryFileSummary`, `SkillSummary`, `McpToolSummary`, `SkillActivationSource`). Extended `AgentEvent::McpServerReady` with `tools`. Added tracing for the new variants. Updated workflow `convert.rs` and `names.rs`. Added 4 conversion tests.\n\n**Task 3 (memory):** Changed `discover_memory` to return `Vec` with `{path, content, byte_count, loaded_bytes, truncated}`. `Session::initialize` emits `MemoryLoaded` immediately after discovery, always (even for empty results). The prompt assembly receives content-only strings. Memory test added for truncation metadata.\n\n**Task 4 (skills):** `Session::initialize` emits `SkillsDiscovered` (always). Slash expansion now emits `SkillActivated { Slash }` instead of `SkillExpanded`. `make_use_skill_tool` emits `SkillActivated { Tool }` only on successful lookup. Failed lookups do not emit. Added 4 session tests.\n\n**Task 5 (MCP):** Added `McpConnectionManager::tool_summaries_for_server` that filters/sorts by qualified name. `Session::initialize` includes the summaries in `McpServerReady`. Existing MCP e2e test now asserts the names-only summary content. Added an mcp unit test.\n\n**Task 6 (docs):** Updated `docs/internal/events.md` with the enriched `agent.mcp.ready` payload, three new sections for `agent.memory.loaded`, `agent.skills.discovered`, `agent.skill.activated`, and a note about the retired `agent.skill.expanded` notification.\n\n**Task 7:** All 1841 tests in `fabro-agent`, `fabro-workflow`, `fabro-types`, `fabro-mcp` pass; workspace clippy clean under nightly-2026-04-14; formatting checked.", + "last_stage": "implement" + }, + "notes": "Stage completed: implement", + "usage": { + "input": { + "usage": { + "model": { + "provider": "anthropic", + "model_id": "claude-opus-4-7" + }, + "tokens": { + "input_tokens": 220090, + "output_tokens": 51681, + "reasoning_tokens": 0, + "cache_read_tokens": 24871894, + "cache_write_tokens": 231090 + } + }, + "facts": { + "algorithm": "anthropic", + "cache_write_5m_tokens": 231090, + "cache_write_1h_tokens": 0 + } + }, + "total_usd_micros": 16272734 + }, + "files_touched": [ + "/home/daytona/workspace/fabro/docs/internal/events.md", + "/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/lib.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/memory.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/session.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/skills.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/types.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-mcp/src/connection_manager.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/src/run_event/agent.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/src/run_event/mod.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/event/convert.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/event/names.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/handler/prompt.rs" + ] + }, + "simplify_gpt": { + "status": "succeeded", + "context_updates": { + "response.simplify_gpt": "Implemented review cleanup and validation.\n\nWhat I fixed:\n- Restored the durable `provider_profile` contract to `String` for:\n - `AgentMemoryLoadedProps`\n - `AgentSkillsDiscoveredProps`\n- Added explicit `loaded_bytes` to `MemoryDocument` instead of deriving it ad hoc from `content.len()`.\n- Reduced `fabro-agent` coupling to durable API props by adding internal agent summary types:\n - `MemoryFileSummary`\n - `SkillSummary`\n - `SkillActivationSource`\n - `McpToolSummary`\n- Restored explicit internal `tool_count` on `AgentEvent::McpServerReady`, while still carrying names-only tool summaries.\n- Updated workflow conversion to map internal agent summary types into `fabro-types` event props.\n- Updated tests to match the cleaned-up internal/durable boundaries.\n- Ran rustfmt.\n\nValidation passed:\n- `cargo nextest run -p fabro-agent -p fabro-workflow -p fabro-types -p fabro-mcp`\n - 1840 passed, 122 skipped\n- `cargo +nightly-2026-04-14 fmt --all`\n- `cargo +nightly-2026-04-14 clippy -q -p fabro-agent -p fabro-workflow -p fabro-types -p fabro-mcp --all-targets -- -D warnings`\n\nWorking tree now has cleanup modifications in 8 Rust files; no docs changes were needed during this review pass.", + "last_stage": "simplify_gpt", + "last_response": "Implemented review cleanup and validation.\n\nWhat I fixed:\n- Restored the durable `provider_profile` contract to `String` for:\n - `AgentMemoryLoadedProps`\n - `AgentSkillsDiscoveredProps`\n- Added expl" + }, + "notes": "Stage completed: simplify_gpt", + "usage": { + "input": { + "usage": { + "model": { + "provider": "openai", + "model_id": "gpt-5.5" + }, + "tokens": { + "input_tokens": 136605, + "output_tokens": 13950, + "reasoning_tokens": 4034, + "cache_read_tokens": 6260224, + "cache_write_tokens": 0 + } + }, + "facts": { + "algorithm": "openai" + } + }, + "total_usd_micros": 4352657 + } + }, "preflight_lint": { "status": "succeeded", "context_updates": { @@ -1000,14 +1272,6 @@ "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 - }, "preflight_compile": { "status": "succeeded", "context_updates": { @@ -1015,62 +1279,17 @@ }, "notes": "Script completed: cargo check -q --workspace 2>&1", "usage": null - }, - "implement": { - "status": "succeeded", - "context_updates": { - "last_response": "Let me do a quick sanity check on the plan's \"final sanity checks\":\n\n1. ✅ Memory events never contain file contents — verified by test `agent_memory_loaded_payload_excludes_file_contents` and stru", - "response.implement": "Let me do a quick sanity check on the plan's \"final sanity checks\":\n\n1. ✅ Memory events never contain file contents — verified by test `agent_memory_loaded_payload_excludes_file_contents` and structural design.\n2. ✅ Skills discovered and memory loaded emitted even for empty lists — verified by tests `initialize_emits_memory_loaded_event_with_empty_files_when_no_memory` and `initialize_emits_skills_discovered_event_when_no_skills`.\n3. ✅ Skill activation is persisted rather than filtered as streaming noise — `SkillActivated` is not in `is_streaming_noise()` and has conversion + name mapping.\n4. ✅ `agent.mcp.ready` backwards-compatible — verified by `agent_mcp_ready_deserializes_legacy_payload_without_tools` test and `#[serde(default, skip_serializing_if = \"Vec::is_empty\")]`.\n5. ✅ Docs match the serialized event names and payload shapes.\n\nSummary of changes:\n\n**Task 1 (fabro-types):** Added `AgentMemoryLoadedProps`, `AgentMemoryFileProps`, `AgentSkillsDiscoveredProps`, `AgentSkillSummary`, `AgentSkillActivationSource`, `AgentSkillActivatedProps`, `AgentMcpToolSummary`. Extended `AgentMcpReadyProps` with a backwards-compatible `tools` field. Added 3 new `EventBody` variants (`AgentMemoryLoaded`, `AgentSkillsDiscovered`, `AgentSkillActivated`) plus the legacy whitelist entries. Added 5 serde tests.\n\n**Task 2 (fabro-agent / fabro-workflow):** Added internal `AgentEvent::MemoryLoaded`, `SkillsDiscovered`, `SkillActivated` variants plus support structs (`MemoryFileSummary`, `SkillSummary`, `McpToolSummary`, `SkillActivationSource`). Extended `AgentEvent::McpServerReady` with `tools`. Added tracing for the new variants. Updated workflow `convert.rs` and `names.rs`. Added 4 conversion tests.\n\n**Task 3 (memory):** Changed `discover_memory` to return `Vec` with `{path, content, byte_count, loaded_bytes, truncated}`. `Session::initialize` emits `MemoryLoaded` immediately after discovery, always (even for empty results). The prompt assembly receives content-only strings. Memory test added for truncation metadata.\n\n**Task 4 (skills):** `Session::initialize` emits `SkillsDiscovered` (always). Slash expansion now emits `SkillActivated { Slash }` instead of `SkillExpanded`. `make_use_skill_tool` emits `SkillActivated { Tool }` only on successful lookup. Failed lookups do not emit. Added 4 session tests.\n\n**Task 5 (MCP):** Added `McpConnectionManager::tool_summaries_for_server` that filters/sorts by qualified name. `Session::initialize` includes the summaries in `McpServerReady`. Existing MCP e2e test now asserts the names-only summary content. Added an mcp unit test.\n\n**Task 6 (docs):** Updated `docs/internal/events.md` with the enriched `agent.mcp.ready` payload, three new sections for `agent.memory.loaded`, `agent.skills.discovered`, `agent.skill.activated`, and a note about the retired `agent.skill.expanded` notification.\n\n**Task 7:** All 1841 tests in `fabro-agent`, `fabro-workflow`, `fabro-types`, `fabro-mcp` pass; workspace clippy clean under nightly-2026-04-14; formatting checked.", - "last_stage": "implement" - }, - "notes": "Stage completed: implement", - "usage": { - "input": { - "usage": { - "model": { - "provider": "anthropic", - "model_id": "claude-opus-4-7" - }, - "tokens": { - "input_tokens": 220090, - "output_tokens": 51681, - "reasoning_tokens": 0, - "cache_read_tokens": 24871894, - "cache_write_tokens": 231090 - } - }, - "facts": { - "algorithm": "anthropic", - "cache_write_5m_tokens": 231090, - "cache_write_1h_tokens": 0 - } - }, - "total_usd_micros": 16272734 - }, - "files_touched": [ - "/home/daytona/workspace/fabro/docs/internal/events.md", - "/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/lib.rs", - "/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/memory.rs", - "/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/session.rs", - "/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/skills.rs", - "/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/types.rs", - "/home/daytona/workspace/fabro/lib/crates/fabro-mcp/src/connection_manager.rs", - "/home/daytona/workspace/fabro/lib/crates/fabro-types/src/run_event/agent.rs", - "/home/daytona/workspace/fabro/lib/crates/fabro-types/src/run_event/mod.rs", - "/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/event/convert.rs", - "/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/event/names.rs", - "/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/handler/prompt.rs" - ] } }, - "next_node_id": "simplify_gpt", + "next_node_id": "verify", "node_visits": { "start": 1, "toolchain": 1, "preflight_compile": 1, "preflight_lint": 1, "implement": 1, - "simplify_opus": 1 + "simplify_opus": 1, + "simplify_gpt": 1 } }, "diff": {} @@ -1096,6 +1315,68 @@ "pull_request": null, "superseded_by": null, "pending_interviews": {}, + "todos_by_list": { + "openai_plan:4bcccfdd-5592-40c9-abaf-71e73aebe0cc": { + "kind": "openai_plan", + "list_id": "openai_plan:4bcccfdd-5592-40c9-abaf-71e73aebe0cc", + "items": [ + { + "id": "13ff6755ec5cd2c7", + "status": "completed", + "order": 0, + "subject": "Inspect repository instructions and current diff" + }, + { + "id": "2687b8226189c15d", + "status": "completed", + "order": 1, + "subject": "Launch three parallel review agents with full diff" + }, + { + "id": "04ac6f344af4d8d2", + "status": "completed", + "order": 2, + "subject": "Aggregate review findings and apply fixes" + }, + { + "id": "9d604f6442392802", + "status": "completed", + "order": 3, + "subject": "Run focused validation" + }, + { + "id": "bb779e0966f4fdef", + "status": "in_progress", + "order": 4, + "subject": "Summarize results" + } + ] + }, + "openai_plan:5c91858f-3eed-4026-8680-e242debc846b": { + "kind": "openai_plan", + "list_id": "openai_plan:5c91858f-3eed-4026-8680-e242debc846b", + "items": [ + { + "id": "780107e30f232f7a", + "status": "in_progress", + "order": 0, + "subject": "Inspect implementation diff and changed files" + }, + { + "id": "d9ee7288b1e500f1", + "status": "pending", + "order": 1, + "subject": "Search repository for existing helpers matching new logic" + }, + { + "id": "2717703126e90244", + "status": "pending", + "order": 2, + "subject": "Compile concrete reuse findings with references" + } + ] + } + }, "stages": { "preflight_compile@1": { "first_event_seq": 30, @@ -1197,7 +1478,12 @@ "first_event_seq": 498, "prompt": null, "response": null, - "completion": null, + "completion": { + "outcome": "succeeded", + "notes": "Stage completed: simplify_opus", + "failure_reason": null, + "timestamp": "2026-05-22T19:38:59.336982Z" + }, "provider_used": { "mode": "agent", "provider": "anthropic", @@ -1210,6 +1496,12 @@ "output": null, "started_at": "2026-05-22T19:23:37.882517Z", "handler": "agent", + "timing": { + "wall_time_ms": 921449, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + }, "usage": { "input_tokens": 119287, "output_tokens": 38365, @@ -1223,7 +1515,7 @@ "provider": "anthropic", "model_id": "claude-opus-4-7" }, - "state": "running" + "state": "succeeded" }, "start@1": { "first_event_seq": 16, @@ -1302,6 +1594,38 @@ }, "state": "succeeded" }, + "simplify_gpt@1": { + "first_event_seq": 990, + "prompt": null, + "response": null, + "completion": null, + "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-22T19:39:03.374495Z", + "handler": "agent", + "usage": { + "input_tokens": 136605, + "output_tokens": 13950, + "total_tokens": 6414813, + "reasoning_tokens": 4034, + "cache_read_tokens": 6260224, + "cache_write_tokens": 0, + "total_usd_micros": 4352657 + }, + "model": { + "provider": "openai", + "model_id": "gpt-5.5" + }, + "state": "running" + }, "preflight_lint@1": { "first_event_seq": 40, "prompt": null, diff --git a/stages/006-simplify_opus@1/diff.patch b/stages/006-simplify_opus@1/diff.patch new file mode 100644 index 000000000..7ea0f7f1c --- /dev/null +++ b/stages/006-simplify_opus@1/diff.patch @@ -0,0 +1,807 @@ +diff --git a/lib/crates/fabro-agent/src/lib.rs b/lib/crates/fabro-agent/src/lib.rs +index 00be22526..13e6aa049 100644 +--- a/lib/crates/fabro-agent/src/lib.rs ++++ b/lib/crates/fabro-agent/src/lib.rs +@@ -70,10 +70,7 @@ pub use tools::{ + make_shell_tool, make_shell_tool_with_config, make_write_file_tool, register_core_tools, + }; + pub use truncation::{TruncationMode, truncate_lines, truncate_output, truncate_tool_output}; +-pub use types::{ +- AgentEvent, McpToolSummary, MemoryFileSummary, Message, SessionEvent, SessionState, +- SkillActivationSource, SkillSummary, +-}; ++pub use types::{AgentEvent, Message, SessionEvent, SessionState}; + + #[cfg(test)] + #[allow( +diff --git a/lib/crates/fabro-agent/src/memory.rs b/lib/crates/fabro-agent/src/memory.rs +index 07e59c6da..7d413556c 100644 +--- a/lib/crates/fabro-agent/src/memory.rs ++++ b/lib/crates/fabro-agent/src/memory.rs +@@ -11,14 +11,14 @@ pub const BUDGET_BYTES: usize = 32768; + + /// One discovered memory file. `content` is what gets inlined into the + /// system prompt. The remaining fields describe the file for +-/// observability and never carry the file's text. ++/// observability and never carry the file's text. The number of bytes ++/// actually loaded into the prompt is `content.len()`. + #[derive(Debug, Clone, PartialEq)] + pub struct MemoryDocument { +- pub path: String, +- pub content: String, +- pub byte_count: usize, +- pub loaded_bytes: usize, +- pub truncated: bool, ++ pub path: String, ++ pub content: String, ++ pub byte_count: usize, ++ pub truncated: bool, + } + + pub async fn discover_memory( +@@ -67,7 +67,6 @@ pub async fn discover_memory( + path, + content, + byte_count, +- loaded_bytes: byte_count, + truncated: false, + }); + } else if budget_remaining > 0 { +@@ -78,13 +77,11 @@ pub async fn discover_memory( + "Project doc truncated to fit budget" + ); + let truncated = truncate_to_budget(&content, budget_remaining); +- let loaded_bytes = truncated.len(); + budget_remaining = 0; + results.push(MemoryDocument { + path, + content: truncated, + byte_count, +- loaded_bytes, + truncated: true, + }); + } else { +@@ -94,7 +91,7 @@ pub async fn discover_memory( + } + } + +- let total_bytes: usize = results.iter().map(|doc| doc.loaded_bytes).sum(); ++ let total_bytes: usize = results.iter().map(|doc| doc.content.len()).sum(); + info!(files = results.len(), total_bytes, "Project docs loaded"); + + Ok(results) +@@ -173,7 +170,7 @@ mod tests { + assert_eq!(docs[0].content, "Agent instructions"); + assert_eq!(docs[0].path, "/repo/AGENTS.md"); + assert_eq!(docs[0].byte_count, "Agent instructions".len()); +- assert_eq!(docs[0].loaded_bytes, "Agent instructions".len()); ++ assert_eq!(docs[0].content.len(), docs[0].byte_count); + assert!(!docs[0].truncated); + } + +@@ -262,7 +259,7 @@ mod tests { + assert_eq!(docs.len(), 2); + assert_eq!(docs[0].content, large_content); + assert!(!docs[0].truncated); +- assert_eq!(docs[0].byte_count, docs[0].loaded_bytes); ++ assert_eq!(docs[0].byte_count, docs[0].content.len()); + // Second doc should be truncated to fit remaining budget + assert!( + docs[1] +@@ -270,7 +267,7 @@ mod tests { + .ends_with("[Project instructions truncated at 32KB]") + ); + assert!(docs[1].truncated); +- assert!(docs[1].byte_count > docs[1].loaded_bytes); ++ assert!(docs[1].byte_count > docs[1].content.len()); + assert!(docs[0].content.len() + docs[1].content.len() <= BUDGET_BYTES); + } + +@@ -342,8 +339,8 @@ mod tests { + assert_eq!(docs.len(), 1); + assert!(docs[0].truncated); + assert_eq!(docs[0].byte_count, large_content.len()); +- assert!(docs[0].loaded_bytes < docs[0].byte_count); +- assert!(docs[0].loaded_bytes <= BUDGET_BYTES); ++ assert!(docs[0].content.len() < docs[0].byte_count); ++ assert!(docs[0].content.len() <= BUDGET_BYTES); + } + + #[tokio::test] +diff --git a/lib/crates/fabro-agent/src/session.rs b/lib/crates/fabro-agent/src/session.rs +index 4163e70ed..30a1b1201 100644 +--- a/lib/crates/fabro-agent/src/session.rs ++++ b/lib/crates/fabro-agent/src/session.rs +@@ -39,10 +39,7 @@ use crate::skills::{ + }; + use crate::subagent::{SubAgentCallbackEvent, SubAgentEventCallback, SubAgentManager}; + use crate::tool_execution::execute_tool_calls; +-use crate::types::{ +- AgentEvent, McpToolSummary, MemoryFileSummary, Message, SessionEvent, SessionState, +- SkillActivationSource, SkillSummary, +-}; ++use crate::types::{AgentEvent, Message, SessionEvent, SessionState}; + + /// One queued external control item for a live session. + #[derive(Debug, Clone)] +@@ -492,26 +489,24 @@ impl Session { + ) + .await?; + +- let provider_profile_name = self.provider_profile.profile_kind().to_string(); ++ let provider_profile = self.provider_profile.profile_kind(); + + // Emit memory loaded event with file metadata. Contents are deliberately + // omitted so the durable event stream never carries file bytes. +- let memory_files: Vec = self ++ let memory_files: Vec = self + .memory + .iter() +- .map(|doc| MemoryFileSummary { ++ .map(|doc| fabro_types::AgentMemoryFileProps { + path: doc.path.clone(), + byte_count: doc.byte_count, +- loaded_bytes: doc.loaded_bytes, ++ loaded_bytes: doc.content.len(), + truncated: doc.truncated, + }) + .collect(); +- let total_loaded_bytes: usize = self.memory.iter().map(|doc| doc.loaded_bytes).sum(); + self.event_emitter + .emit(self.id.clone(), AgentEvent::MemoryLoaded { +- provider_profile: provider_profile_name.clone(), ++ provider_profile, + files: memory_files, +- total_loaded_bytes, + budget_bytes: BUDGET_BYTES, + }); + +@@ -526,19 +521,19 @@ impl Session { + self.skills = discover_skills(self.sandbox.as_ref(), &skill_dirs, &cancel_token).await?; + debug!(skill_count = self.skills.len(), "Skills discovered"); + +- let skill_summaries: Vec = self ++ let skill_summaries: Vec = self + .skills + .iter() +- .map(|skill| SkillSummary { ++ .map(|skill| fabro_types::AgentSkillSummary { + name: skill.name.clone(), + description: skill.description.clone(), + }) + .collect(); + self.event_emitter + .emit(self.id.clone(), AgentEvent::SkillsDiscovered { +- provider_profile: provider_profile_name.clone(), +- source_dirs: skill_dirs.clone(), +- skills: skill_summaries, ++ provider_profile, ++ source_dirs: skill_dirs.clone(), ++ skills: skill_summaries, + }); + + // Register use_skill tool when skills are available +@@ -562,11 +557,11 @@ impl Session { + + for (server_name, result) in &results { + match result { +- Ok(tool_count) => { ++ Ok(_) => { + let tools = manager + .tool_summaries_for_server(server_name) + .into_iter() +- .map(|(name, original_name)| McpToolSummary { ++ .map(|(name, original_name)| fabro_types::AgentMcpToolSummary { + name, + original_name, + }) +@@ -574,7 +569,6 @@ impl Session { + self.event_emitter + .emit(self.id.clone(), AgentEvent::McpServerReady { + server_name: server_name.clone(), +- tool_count: *tool_count, + tools, + }); + } +@@ -1194,7 +1188,7 @@ impl Session { + self.event_emitter + .emit(self.id.clone(), AgentEvent::SkillActivated { + skill_name: name.clone(), +- source: SkillActivationSource::Slash, ++ source: fabro_types::AgentSkillActivationSource::Slash, + }); + } + let expanded_input = expanded.text; +@@ -3709,14 +3703,8 @@ mod tests { + // summaries pulled from the connection manager. + let mut mcp_ready = false; + while let Ok(event) = rx.try_recv() { +- if let AgentEvent::McpServerReady { +- server_name, +- tool_count, +- tools, +- } = &event.event +- { ++ if let AgentEvent::McpServerReady { server_name, tools } = &event.event { + assert_eq!(server_name, "test-echo"); +- assert_eq!(*tool_count, 1); + assert_eq!(tools.len(), 1); + assert_eq!(tools[0].name, "mcp__test_echo__echo"); + assert_eq!(tools[0].original_name, "echo"); +@@ -3957,20 +3945,18 @@ mod tests { + while let Ok(envelope) = rx.try_recv() { + if let AgentEvent::MemoryLoaded { + files, +- total_loaded_bytes, + budget_bytes, + provider_profile, + } = envelope.event + { +- memory_event = Some((files, total_loaded_bytes, budget_bytes, provider_profile)); ++ memory_event = Some((files, budget_bytes, provider_profile)); + break; + } + } +- let (files, total_loaded_bytes, budget_bytes, provider_profile) = ++ let (files, budget_bytes, provider_profile) = + memory_event.expect("MemoryLoaded should be emitted"); +- assert_eq!(provider_profile, "anthropic"); ++ assert_eq!(provider_profile, fabro_model::AgentProfileKind::Anthropic); + assert_eq!(budget_bytes, 32768); +- assert_eq!(total_loaded_bytes, "Hello world".len()); + assert_eq!(files.len(), 1); + assert_eq!(files[0].path, "/home/test/AGENTS.md"); + assert_eq!(files[0].byte_count, "Hello world".len()); +@@ -4039,7 +4025,7 @@ mod tests { + } + let (provider_profile, source_dirs, skills) = + got.expect("SkillsDiscovered must be emitted"); +- assert_eq!(provider_profile, "anthropic"); ++ assert_eq!(provider_profile, fabro_model::AgentProfileKind::Anthropic); + assert_eq!(source_dirs, vec!["/skills".to_string()]); + assert_eq!(skills.len(), 1); + assert_eq!(skills[0].name, "commit"); +@@ -4098,16 +4084,15 @@ mod tests { + let mut rx = session.subscribe(); + session.process_input("/commit fix things").await.unwrap(); + +- let mut activations: Vec<(String, SkillActivationSource)> = Vec::new(); ++ let mut activations: Vec<(String, fabro_types::AgentSkillActivationSource)> = Vec::new(); + while let Ok(envelope) = rx.try_recv() { + if let AgentEvent::SkillActivated { skill_name, source } = envelope.event { + activations.push((skill_name, source)); + } + } + assert!( +- activations +- .iter() +- .any(|(name, source)| name == "commit" && *source == SkillActivationSource::Slash), ++ activations.iter().any(|(name, source)| name == "commit" ++ && *source == fabro_types::AgentSkillActivationSource::Slash), + "expected slash skill activation, got {activations:?}" + ); + } +@@ -4150,7 +4135,8 @@ mod tests { + let mut tool_activations = 0; + while let Ok(envelope) = rx.try_recv() { + if let AgentEvent::SkillActivated { source, skill_name } = envelope.event { +- if source == SkillActivationSource::Tool && skill_name == "commit" { ++ if source == fabro_types::AgentSkillActivationSource::Tool && skill_name == "commit" ++ { + tool_activations += 1; + } + } +diff --git a/lib/crates/fabro-agent/src/skills.rs b/lib/crates/fabro-agent/src/skills.rs +index f7b4e3eaf..34c6e126b 100644 +--- a/lib/crates/fabro-agent/src/skills.rs ++++ b/lib/crates/fabro-agent/src/skills.rs +@@ -7,7 +7,7 @@ use crate::error::{Error, InterruptReason}; + use crate::sandbox::Sandbox; + use crate::tool_registry::RegisteredTool; + use crate::tools::required_str; +-use crate::types::{AgentEvent, SkillActivationSource}; ++use crate::types::AgentEvent; + + #[derive(Debug, Clone)] + pub struct Skill { +@@ -187,7 +187,7 @@ pub fn make_use_skill_tool(skills: Arc>) -> RegisteredTool { + .ok_or_else(|| format!("Unknown skill: {name}"))?; + ctx.emit_agent_event(AgentEvent::SkillActivated { + skill_name: name.to_string(), +- source: SkillActivationSource::Tool, ++ source: fabro_types::AgentSkillActivationSource::Tool, + }); + Ok(skill.template.clone()) + }) +diff --git a/lib/crates/fabro-agent/src/types.rs b/lib/crates/fabro-agent/src/types.rs +index 95833a2d8..9e497bbd0 100644 +--- a/lib/crates/fabro-agent/src/types.rs ++++ b/lib/crates/fabro-agent/src/types.rs +@@ -3,7 +3,7 @@ use std::time::SystemTime; + use chrono::{DateTime, Utc}; + use fabro_llm::Error as LlmError; + use fabro_llm::types::{ContentPart, ThinkingData, TokenCounts, ToolCall, ToolResult}; +-use fabro_model::ModelRef; ++use fabro_model::{AgentProfileKind, ModelRef}; + use fabro_types::SessionMessage; + use serde::de::DeserializeOwned; + use serde::{Deserialize, Serialize}; +@@ -194,33 +194,6 @@ pub enum SessionState { + Closed, + } + +-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +-pub struct MemoryFileSummary { +- pub path: String, +- pub byte_count: usize, +- pub loaded_bytes: usize, +- pub truncated: bool, +-} +- +-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +-pub struct SkillSummary { +- pub name: String, +- pub description: String, +-} +- +-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +-pub struct McpToolSummary { +- pub name: String, +- pub original_name: String, +-} +- +-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +-#[serde(rename_all = "snake_case")] +-pub enum SkillActivationSource { +- Slash, +- Tool, +-} +- + #[derive(Debug, Clone, Serialize, Deserialize)] + pub enum AgentEvent { + SessionStarted { +@@ -279,9 +252,6 @@ pub enum AgentEvent { + TurnLimitReached { + max_turns: usize, + }, +- SkillExpanded { +- skill_name: String, +- }, + SteeringInjected { + text: String, + /// Principal that authored the steer. Lifted to top-level +@@ -329,27 +299,25 @@ pub enum AgentEvent { + }, + McpServerReady { + server_name: String, +- tool_count: usize, +- tools: Vec, ++ tools: Vec, + }, + McpServerFailed { + server_name: String, + error: String, + }, + MemoryLoaded { +- provider_profile: String, +- files: Vec, +- total_loaded_bytes: usize, +- budget_bytes: usize, ++ provider_profile: AgentProfileKind, ++ files: Vec, ++ budget_bytes: usize, + }, + SkillsDiscovered { +- provider_profile: String, ++ provider_profile: AgentProfileKind, + source_dirs: Vec, +- skills: Vec, ++ skills: Vec, + }, + SkillActivated { + skill_name: String, +- source: SkillActivationSource, ++ source: fabro_types::AgentSkillActivationSource, + }, + /// New todo / task was created. Carries the full row so the projection + /// can be reconstructed from `todo.created` alone. +@@ -373,7 +341,6 @@ impl AgentEvent { + | Self::TextDelta { .. } + | Self::ReasoningDelta { .. } + | Self::ToolCallOutputDelta { .. } +- | Self::SkillExpanded { .. } + ) + } + +@@ -463,9 +430,6 @@ impl AgentEvent { + Self::TurnLimitReached { max_turns } => { + warn!(session_id, max_turns, "Message limit reached"); + } +- Self::SkillExpanded { skill_name } => { +- debug!(session_id, skill = skill_name.as_str(), "Skill expanded"); +- } + Self::SteeringInjected { text, .. } => { + debug!(session_id, text_len = text.len(), "Steering injected"); + } +@@ -544,30 +508,24 @@ impl AgentEvent { + Self::SubAgentClosed { agent_id, depth } => { + debug!(session_id, agent_id, depth, "Sub-agent closed"); + } +- Self::McpServerReady { +- server_name, +- tool_count, +- tools, +- } => { ++ Self::McpServerReady { server_name, tools } => { + info!( + session_id, + server = server_name.as_str(), +- tool_count, +- summary_count = tools.len(), ++ tool_count = tools.len(), + "MCP server ready" + ); + } + Self::MemoryLoaded { + provider_profile, + files, +- total_loaded_bytes, + budget_bytes, + } => { + info!( + session_id, +- provider_profile = provider_profile.as_str(), ++ provider_profile = %provider_profile, + file_count = files.len(), +- total_loaded_bytes, ++ total_loaded_bytes = files.iter().map(|f| f.loaded_bytes).sum::(), + budget_bytes, + "Agent memory loaded" + ); +@@ -579,7 +537,7 @@ impl AgentEvent { + } => { + info!( + session_id, +- provider_profile = provider_profile.as_str(), ++ provider_profile = %provider_profile, + skill_count = skills.len(), + source_dir_count = source_dirs.len(), + "Agent skills discovered" +@@ -690,16 +648,6 @@ mod tests { + })); + } + +- #[test] +- fn skill_expanded_constructible() { +- let event = AgentEvent::SkillExpanded { +- skill_name: "commit".into(), +- }; +- assert!( +- matches!(event, AgentEvent::SkillExpanded { skill_name } if skill_name == "commit") +- ); +- } +- + #[test] + fn subagent_spawned_constructible() { + let event = AgentEvent::SubAgentSpawned { +@@ -832,13 +780,12 @@ mod tests { + fn mcp_server_ready_constructible() { + let event = AgentEvent::McpServerReady { + server_name: "filesystem".into(), +- tool_count: 3, + tools: Vec::new(), + }; +- assert!(matches!(event, AgentEvent::McpServerReady { +- tool_count: 3, +- .. +- })); ++ assert!(matches!( ++ event, ++ AgentEvent::McpServerReady { server_name, .. } if server_name == "filesystem" ++ )); + } + + #[test] +@@ -857,7 +804,6 @@ mod tests { + let events = vec![ + AgentEvent::McpServerReady { + server_name: "fs".into(), +- tool_count: 5, + tools: Vec::new(), + }, + AgentEvent::McpServerFailed { +@@ -868,10 +814,10 @@ mod tests { + let json = serde_json::to_string(&events).unwrap(); + let deserialized: Vec = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.len(), 2); +- assert!(matches!(&deserialized[0], AgentEvent::McpServerReady { +- tool_count: 5, +- .. +- })); ++ assert!(matches!( ++ &deserialized[0], ++ AgentEvent::McpServerReady { server_name, .. } if server_name == "fs" ++ )); + assert!(matches!( + &deserialized[1], + AgentEvent::McpServerFailed { .. } +diff --git a/lib/crates/fabro-types/src/lib.rs b/lib/crates/fabro-types/src/lib.rs +index 45f1d1f4d..2430a7cc3 100644 +--- a/lib/crates/fabro-types/src/lib.rs ++++ b/lib/crates/fabro-types/src/lib.rs +@@ -93,6 +93,7 @@ pub use run::{ + }; + pub use run_blob_id::RunBlobId; + pub use run_event::{ ++ AgentMcpToolSummary, AgentMemoryFileProps, AgentSkillActivationSource, AgentSkillSummary, + EventBody, ExecOutputTail, InterviewOption, MetadataSnapshotFailureKind, MetadataSnapshotPhase, + RunEvent, RunNoticeCode, RunNoticeLevel, RunPairEndedReason, RunPairFailedReason, + SessionCapability, TodoCreatedProps, TodoDeletedProps, TodoUpdatedProps, +diff --git a/lib/crates/fabro-types/src/run_event/agent.rs b/lib/crates/fabro-types/src/run_event/agent.rs +index 56106dc60..1666e6c43 100644 +--- a/lib/crates/fabro-types/src/run_event/agent.rs ++++ b/lib/crates/fabro-types/src/run_event/agent.rs +@@ -1,3 +1,4 @@ ++use fabro_model::AgentProfileKind; + use serde::{Deserialize, Serialize}; + use serde_json::Value; + +@@ -235,7 +236,7 @@ pub struct AgentMcpFailedProps { + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct AgentMemoryLoadedProps { +- pub provider_profile: String, ++ pub provider_profile: AgentProfileKind, + pub files: Vec, + pub total_loaded_bytes: usize, + pub budget_bytes: usize, +@@ -252,7 +253,7 @@ pub struct AgentMemoryFileProps { + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct AgentSkillsDiscoveredProps { +- pub provider_profile: String, ++ pub provider_profile: AgentProfileKind, + pub source_dirs: Vec, + pub skills: Vec, + pub visit: u32, +diff --git a/lib/crates/fabro-types/src/run_event/mod.rs b/lib/crates/fabro-types/src/run_event/mod.rs +index 564dea99a..91fba066d 100644 +--- a/lib/crates/fabro-types/src/run_event/mod.rs ++++ b/lib/crates/fabro-types/src/run_event/mod.rs +@@ -1999,7 +1999,7 @@ mod tests { + #[test] + fn agent_memory_loaded_serializes_with_canonical_name() { + let body = EventBody::AgentMemoryLoaded(AgentMemoryLoadedProps { +- provider_profile: "anthropic".to_string(), ++ provider_profile: fabro_model::AgentProfileKind::Anthropic, + files: vec![AgentMemoryFileProps { + path: "/repo/AGENTS.md".to_string(), + byte_count: 100, +@@ -2029,7 +2029,7 @@ mod tests { + #[test] + fn agent_skills_discovered_serializes_with_canonical_name() { + let body = EventBody::AgentSkillsDiscovered(AgentSkillsDiscoveredProps { +- provider_profile: "openai".to_string(), ++ provider_profile: fabro_model::AgentProfileKind::OpenAi, + source_dirs: vec!["/repo/.fabro/skills".to_string()], + skills: vec![AgentSkillSummary { + name: "commit".to_string(), +diff --git a/lib/crates/fabro-workflow/src/event/convert.rs b/lib/crates/fabro-workflow/src/event/convert.rs +index c1e008159..bc038d71c 100644 +--- a/lib/crates/fabro-workflow/src/event/convert.rs ++++ b/lib/crates/fabro-workflow/src/event/convert.rs +@@ -719,22 +719,14 @@ fn event_body_from_event(event: &Event) -> EventBody { + visit: *visit, + }) + } +- AgentEvent::McpServerReady { +- server_name, +- tool_count, +- tools, +- } => EventBody::AgentMcpReady(fabro_types::AgentMcpReadyProps { +- server_name: server_name.clone(), +- tool_count: *tool_count, +- tools: tools +- .iter() +- .map(|tool| fabro_types::AgentMcpToolSummary { +- name: tool.name.clone(), +- original_name: tool.original_name.clone(), +- }) +- .collect(), +- visit: *visit, +- }), ++ AgentEvent::McpServerReady { server_name, tools } => { ++ EventBody::AgentMcpReady(fabro_types::AgentMcpReadyProps { ++ server_name: server_name.clone(), ++ tool_count: tools.len(), ++ tools: tools.clone(), ++ visit: *visit, ++ }) ++ } + AgentEvent::McpServerFailed { server_name, error } => { + EventBody::AgentMcpFailed(fabro_types::AgentMcpFailedProps { + server_name: server_name.clone(), +@@ -745,20 +737,11 @@ fn event_body_from_event(event: &Event) -> EventBody { + AgentEvent::MemoryLoaded { + provider_profile, + files, +- total_loaded_bytes, + budget_bytes, + } => EventBody::AgentMemoryLoaded(fabro_types::AgentMemoryLoadedProps { +- provider_profile: provider_profile.clone(), +- files: files +- .iter() +- .map(|file| fabro_types::AgentMemoryFileProps { +- path: file.path.clone(), +- byte_count: file.byte_count, +- loaded_bytes: file.loaded_bytes, +- truncated: file.truncated, +- }) +- .collect(), +- total_loaded_bytes: *total_loaded_bytes, ++ provider_profile: *provider_profile, ++ total_loaded_bytes: files.iter().map(|f| f.loaded_bytes).sum(), ++ files: files.clone(), + budget_bytes: *budget_bytes, + visit: *visit, + }), +@@ -767,28 +750,15 @@ fn event_body_from_event(event: &Event) -> EventBody { + source_dirs, + skills, + } => EventBody::AgentSkillsDiscovered(fabro_types::AgentSkillsDiscoveredProps { +- provider_profile: provider_profile.clone(), ++ provider_profile: *provider_profile, + source_dirs: source_dirs.clone(), +- skills: skills +- .iter() +- .map(|skill| fabro_types::AgentSkillSummary { +- name: skill.name.clone(), +- description: skill.description.clone(), +- }) +- .collect(), ++ skills: skills.clone(), + visit: *visit, + }), + AgentEvent::SkillActivated { skill_name, source } => { + EventBody::AgentSkillActivated(fabro_types::AgentSkillActivatedProps { + skill_name: skill_name.clone(), +- source: match source { +- fabro_agent::SkillActivationSource::Slash => { +- fabro_types::AgentSkillActivationSource::Slash +- } +- fabro_agent::SkillActivationSource::Tool => { +- fabro_types::AgentSkillActivationSource::Tool +- } +- }, ++ source: *source, + visit: *visit, + }) + } +@@ -800,7 +770,6 @@ fn event_body_from_event(event: &Event) -> EventBody { + | AgentEvent::TextDelta { .. } + | AgentEvent::ReasoningDelta { .. } + | AgentEvent::ToolCallOutputDelta { .. } +- | AgentEvent::SkillExpanded { .. } + | AgentEvent::SessionStarted { .. } + | AgentEvent::SessionEnded => panic!( + "agent event should not be converted through the stage-scoped Event::Agent wrapper" +@@ -2327,15 +2296,14 @@ mod tests { + stage: "code".to_string(), + visit: 3, + event: AgentEvent::MemoryLoaded { +- provider_profile: "anthropic".to_string(), +- files: vec![fabro_agent::MemoryFileSummary { ++ provider_profile: fabro_model::AgentProfileKind::Anthropic, ++ files: vec![fabro_types::AgentMemoryFileProps { + path: "/repo/AGENTS.md".to_string(), + byte_count: 200, + loaded_bytes: 200, + truncated: false, + }], +- total_loaded_bytes: 200, +- budget_bytes: 32768, ++ budget_bytes: 32768, + }, + session_id: Some("ses_1".to_string()), + parent_session_id: None, +@@ -2345,7 +2313,10 @@ mod tests { + match stored.body { + EventBody::AgentMemoryLoaded(props) => { + assert_eq!(props.visit, 3); +- assert_eq!(props.provider_profile, "anthropic"); ++ assert_eq!( ++ props.provider_profile, ++ fabro_model::AgentProfileKind::Anthropic ++ ); + assert_eq!(props.budget_bytes, 32768); + assert_eq!(props.total_loaded_bytes, 200); + assert_eq!(props.files.len(), 1); +@@ -2364,15 +2335,14 @@ mod tests { + stage: "code".to_string(), + visit: 1, + event: AgentEvent::MemoryLoaded { +- provider_profile: "openai".to_string(), +- files: vec![fabro_agent::MemoryFileSummary { ++ provider_profile: fabro_model::AgentProfileKind::OpenAi, ++ files: vec![fabro_types::AgentMemoryFileProps { + path: "/repo/AGENTS.md".to_string(), + byte_count: 100, + loaded_bytes: 100, + truncated: false, + }], +- total_loaded_bytes: 100, +- budget_bytes: 32768, ++ budget_bytes: 32768, + }, + session_id: None, + parent_session_id: None, +@@ -2391,9 +2361,9 @@ mod tests { + stage: "code".to_string(), + visit: 2, + event: AgentEvent::SkillsDiscovered { +- provider_profile: "anthropic".to_string(), ++ provider_profile: fabro_model::AgentProfileKind::Anthropic, + source_dirs: vec!["/repo/.fabro/skills".to_string()], +- skills: vec![fabro_agent::SkillSummary { ++ skills: vec![fabro_types::AgentSkillSummary { + name: "commit".to_string(), + description: "Make a commit".to_string(), + }], +@@ -2406,7 +2376,10 @@ mod tests { + match stored.body { + EventBody::AgentSkillsDiscovered(props) => { + assert_eq!(props.visit, 2); +- assert_eq!(props.provider_profile, "anthropic"); ++ assert_eq!( ++ props.provider_profile, ++ fabro_model::AgentProfileKind::Anthropic ++ ); + assert_eq!(props.source_dirs, vec!["/repo/.fabro/skills".to_string()]); + assert_eq!(props.skills.len(), 1); + assert_eq!(props.skills[0].name, "commit"); +@@ -2423,7 +2396,7 @@ mod tests { + visit: 1, + event: AgentEvent::SkillActivated { + skill_name: "commit".to_string(), +- source: fabro_agent::SkillActivationSource::Slash, ++ source: fabro_types::AgentSkillActivationSource::Slash, + }, + session_id: Some("ses_1".to_string()), + parent_session_id: None, +@@ -2444,7 +2417,7 @@ mod tests { + visit: 4, + event: AgentEvent::SkillActivated { + skill_name: "review".to_string(), +- source: fabro_agent::SkillActivationSource::Tool, ++ source: fabro_types::AgentSkillActivationSource::Tool, + }, + session_id: None, + parent_session_id: None, +@@ -2467,13 +2440,12 @@ mod tests { + visit: 5, + event: AgentEvent::McpServerReady { + server_name: "github".to_string(), +- tool_count: 2, + tools: vec![ +- fabro_agent::McpToolSummary { ++ fabro_types::AgentMcpToolSummary { + name: "mcp__github__create_issue".to_string(), + original_name: "create_issue".to_string(), + }, +- fabro_agent::McpToolSummary { ++ fabro_types::AgentMcpToolSummary { + name: "mcp__github__list_issues".to_string(), + original_name: "list_issues".to_string(), + }, +diff --git a/lib/crates/fabro-workflow/src/event/names.rs b/lib/crates/fabro-workflow/src/event/names.rs +index d730c1872..51eea15c8 100644 +--- a/lib/crates/fabro-workflow/src/event/names.rs ++++ b/lib/crates/fabro-workflow/src/event/names.rs +@@ -78,7 +78,6 @@ pub fn event_name(event: &Event) -> &'static str { + AgentEvent::Warning { .. } => "agent.warning", + AgentEvent::LoopDetected => "agent.loop.detected", + AgentEvent::TurnLimitReached { .. } => "agent.turn.limit", +- AgentEvent::SkillExpanded { .. } => "agent.skill.expanded", + AgentEvent::SteeringInjected { .. } => "agent.steering.injected", + AgentEvent::CompactionStarted { .. } => "agent.compaction.started", + AgentEvent::CompactionCompleted { .. } => "agent.compaction.completed", diff --git a/stages/006-simplify_opus@1/status.json b/stages/006-simplify_opus@1/status.json new file mode 100644 index 000000000..459adf41c --- /dev/null +++ b/stages/006-simplify_opus@1/status.json @@ -0,0 +1,6 @@ +{ + "outcome": "succeeded", + "notes": "Stage completed: simplify_opus", + "failure_reason": null, + "timestamp": "2026-05-22T19:38:59.336982Z" +} \ No newline at end of file diff --git a/stages/007-simplify_gpt@1/prompt.md b/stages/007-simplify_gpt@1/prompt.md new file mode 100644 index 000000000..ad1e9b72c --- /dev/null +++ b/stages/007-simplify_gpt@1/prompt.md @@ -0,0 +1,511 @@ +Goal: # Agent Context Observability Events Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add durable API/backend events that report loaded memory files, discovered and activated skills, and per-server MCP tool names for agent runs. + +**Architecture:** Keep this API-backend scoped. Emit typed `AgentEvent` variants from the existing `fabro-agent` initialization and skill activation paths, convert them through `fabro-workflow` into durable `fabro-types` run events, and document the event contracts. Do not add run projection fields in this pass; consumers can read the event stream/history. + +**Tech Stack:** Rust, Serde, Fabro agent/session events, Fabro workflow event conversion, Fabro MCP connection manager, `cargo nextest`. + +--- + +## Scope + +Implement these event changes: + +- Add `agent.memory.loaded` with memory file paths, byte counts, loaded byte counts, truncation flags, provider profile, total loaded bytes, and budget bytes. +- Add `agent.skills.discovered` with source directories, provider profile, and sorted skill summaries. +- Add persisted `agent.skill.activated` for slash skill expansion and successful `use_skill` tool calls. +- Enrich `agent.mcp.ready` with names-only tool summaries: qualified tool name and original server tool name. + +Do not implement ACP-native equivalents in this pass. Do not include memory file contents in any event payload. Do not include MCP tool descriptions or schemas. + +## Existing Patterns To Follow + +- Read `docs/internal/events-strategy.md` before changing event variants, names, conversion, or progress JSONL behavior. +- Read `docs/internal/testing-strategy.md` before adding or reorganizing tests. +- Follow the current `AgentEvent` flow: + - `lib/crates/fabro-agent/src/types.rs` + - `lib/crates/fabro-agent/src/session.rs` + - `lib/crates/fabro-workflow/src/handler/llm/api.rs` + - `lib/crates/fabro-workflow/src/event/convert.rs` + - `lib/crates/fabro-workflow/src/event/names.rs` + - `lib/crates/fabro-types/src/run_event/agent.rs` + - `lib/crates/fabro-types/src/run_event/mod.rs` +- Follow Rust import style from `AGENTS.md`: import types by name, import functions through their parent module, and avoid glob imports in production code. + +## File Map + +- Modify `lib/crates/fabro-types/src/run_event/agent.rs`: add new prop structs and extend `AgentMcpReadyProps`. +- Modify `lib/crates/fabro-types/src/run_event/mod.rs`: add `EventBody` variants for the new event names. +- Modify `lib/crates/fabro-agent/src/types.rs`: add internal `AgentEvent` variants, trace output, and noise filtering decisions. +- Modify `lib/crates/fabro-agent/src/memory.rs`: return memory content plus metadata instead of bare strings. +- Modify `lib/crates/fabro-agent/src/session.rs`: emit memory, skills, skill activation, and enriched MCP events. +- Modify `lib/crates/fabro-agent/src/skills.rs`: emit tool-sourced skill activation from `use_skill`. +- Modify `lib/crates/fabro-mcp/src/connection_manager.rs`: expose or support deterministic names-only tool summaries per server. +- Modify `lib/crates/fabro-workflow/src/event/convert.rs`: convert new agent events to durable event bodies. +- Modify `lib/crates/fabro-workflow/src/event/names.rs`: add event names. +- Modify `lib/crates/fabro-workflow/src/event/events.rs` only if the agent event name mapping also lives there for these variants. +- Modify `lib/crates/fabro-workflow/src/event/stored_fields.rs` only if a new event needs non-standard stored fields; otherwise rely on existing `Event::Agent` handling. +- Modify `docs/internal/events.md`: document new event shapes and the richer MCP payload. +- Add or update tests in `lib/crates/fabro-agent`, `lib/crates/fabro-mcp`, `lib/crates/fabro-types`, and `lib/crates/fabro-workflow`. + +--- + +### Task 1: Add Typed Durable Event Contracts + +**Files:** +- Modify: `lib/crates/fabro-types/src/run_event/agent.rs` +- Modify: `lib/crates/fabro-types/src/run_event/mod.rs` +- Test: existing `fabro-types` run event serde tests, or add focused coverage near the existing run event tests. + +- [ ] **Step 1: Add agent memory props** + +Add event prop structs with this shape: + +```rust +pub struct AgentMemoryLoadedProps { + pub provider_profile: String, + pub files: Vec, + pub total_loaded_bytes: usize, + pub budget_bytes: usize, + pub visit: u32, +} + +pub struct AgentMemoryFileProps { + pub path: String, + pub byte_count: usize, + pub loaded_bytes: usize, + pub truncated: bool, +} +``` + +- [ ] **Step 2: Add skill props** + +Add skill discovery and activation props: + +```rust +pub struct AgentSkillsDiscoveredProps { + pub provider_profile: String, + pub source_dirs: Vec, + pub skills: Vec, + pub visit: u32, +} + +pub struct AgentSkillSummary { + pub name: String, + pub description: String, +} + +pub enum AgentSkillActivationSource { + Slash, + Tool, +} + +pub struct AgentSkillActivatedProps { + pub skill_name: String, + pub source: AgentSkillActivationSource, + pub visit: u32, +} +``` + +Use serde names `slash` and `tool` for `AgentSkillActivationSource`. If a local enum string pattern already exists, follow that pattern. + +- [ ] **Step 3: Extend MCP ready props** + +Extend `AgentMcpReadyProps` with a backwards-compatible field: + +```rust +#[serde(default, skip_serializing_if = "Vec::is_empty")] +pub tools: Vec, +``` + +Add: + +```rust +pub struct AgentMcpToolSummary { + pub name: String, + pub original_name: String, +} +``` + +- [ ] **Step 4: Add EventBody variants** + +Add `EventBody` variants using these serialized event names: + +- `agent.memory.loaded` +- `agent.skills.discovered` +- `agent.skill.activated` + +Keep existing `agent.mcp.ready` name unchanged and only enrich its props. + +- [ ] **Step 5: Add serde tests** + +Cover: + +- New event names serialize to the expected dot names. +- `AgentSkillActivationSource` serializes as `slash` and `tool`. +- Old `agent.mcp.ready` JSON without `tools` still deserializes with `tools == []`. + +--- + +### Task 2: Add Internal Agent Events And Conversion + +**Files:** +- Modify: `lib/crates/fabro-agent/src/types.rs` +- Modify: `lib/crates/fabro-workflow/src/event/convert.rs` +- Modify: `lib/crates/fabro-workflow/src/event/names.rs` +- Modify: `lib/crates/fabro-workflow/src/event/events.rs` if needed by the existing name mapping. +- Test: `lib/crates/fabro-workflow` event conversion tests. + +- [ ] **Step 1: Add internal AgentEvent variants** + +Add variants equivalent to: + +```rust +MemoryLoaded { + provider_profile: String, + files: Vec, + total_loaded_bytes: usize, + budget_bytes: usize, +} + +SkillsDiscovered { + provider_profile: String, + source_dirs: Vec, + skills: Vec, +} + +SkillActivated { + skill_name: String, + source: SkillActivationSource, +} + +McpServerReady { + server_name: String, + tool_count: usize, + tools: Vec, +} +``` + +Prefer small shared internal structs near `AgentEvent` if that matches the existing file organization. + +- [ ] **Step 2: Persist skill activation** + +Do not classify `SkillActivated` as streaming noise. The existing `SkillExpanded` event is currently filtered before persistence; replace slash expansion emissions with `SkillActivated { source: Slash }` or keep `SkillExpanded` internal-only if removing it would create unnecessary churn. + +- [ ] **Step 3: Add trace behavior** + +Update `AgentEvent::trace` so the new events emit concise tracing summaries: + +- memory loaded: profile, file count, total loaded bytes, budget bytes +- skills discovered: profile, skill count, source dir count +- skill activated: name and source +- MCP ready: server, count, and summary count + +- [ ] **Step 4: Convert to durable events** + +Update `fabro-workflow` event conversion so the new agent events map to the new `fabro-types` props and include `visit`. + +- [ ] **Step 5: Add conversion tests** + +Cover each new event with a focused conversion assertion that checks: + +- durable event name +- `visit` +- core fields +- no memory content in the converted payload + +--- + +### Task 3: Emit Memory Loaded Metadata + +**Files:** +- Modify: `lib/crates/fabro-agent/src/memory.rs` +- Modify: `lib/crates/fabro-agent/src/session.rs` +- Test: relevant `fabro-agent` memory/session tests. + +- [ ] **Step 1: Change memory discovery return type** + +Change memory discovery from bare `Vec` to a document type carrying both prompt content and event metadata: + +```rust +pub struct MemoryDocument { + pub path: String, + pub content: String, + pub byte_count: usize, + pub loaded_bytes: usize, + pub truncated: bool, +} +``` + +Keep existing behavior unchanged: + +- provider profile filename candidates stay the same +- root-to-working-dir walk stays the same +- content dedupe stays the same +- empty files are skipped +- total budget remains 32 KiB +- truncated content keeps the existing truncation marker + +- [ ] **Step 2: Preserve prompt assembly behavior** + +Adjust session/profile prompt assembly to pass only memory contents where prompt assembly expects memory text. The system prompt should be byte-for-byte equivalent except where existing tests allow non-semantic ordering differences. + +- [ ] **Step 3: Emit agent.memory.loaded** + +In `Session::initialize()`, emit `AgentEvent::MemoryLoaded` immediately after memory discovery, before skills and MCP initialization. + +Emit the event even when no memory files are loaded. That lets consumers distinguish "no memory" from "not reported." + +- [ ] **Step 4: Add memory tests** + +Cover: + +- loaded file path appears in event metadata +- `byte_count` is the original file byte count +- `loaded_bytes` reflects bytes actually loaded into the prompt budget +- `truncated` is true only for truncated files +- event payload never contains memory file contents +- empty discovery still emits a memory-loaded event with `files == []` + +--- + +### Task 4: Emit Skills Discovered And Skill Activated + +**Files:** +- Modify: `lib/crates/fabro-agent/src/session.rs` +- Modify: `lib/crates/fabro-agent/src/skills.rs` +- Test: relevant `fabro-agent` skill/session tests. + +- [ ] **Step 1: Emit skills discovered** + +After `discover_skills(...)`, emit `AgentEvent::SkillsDiscovered` with: + +- `provider_profile` +- `source_dirs` +- sorted `skills: [{ name, description }]` + +Emit the event even when no skills are discovered. + +- [ ] **Step 2: Emit slash activation** + +Where slash skill expansion currently emits or creates `SkillExpanded`, emit: + +```rust +AgentEvent::SkillActivated { + skill_name, + source: SkillActivationSource::Slash, +} +``` + +- [ ] **Step 3: Emit tool activation** + +In `make_use_skill_tool`, use `ToolContext::emit_agent_event(...)` after a requested skill is found and before returning the skill template. Emit: + +```rust +AgentEvent::SkillActivated { + skill_name: name.to_string(), + source: SkillActivationSource::Tool, +} +``` + +Do not emit activation for failed `use_skill` lookups. + +- [ ] **Step 4: Add skill tests** + +Cover: + +- discovery event includes all discovered skills sorted by name +- discovery event includes configured source directories +- empty discovery emits `skills == []` +- slash expansion emits `source == slash` +- successful `use_skill` emits `source == tool` +- failed `use_skill` does not emit activation + +--- + +### Task 5: Enrich agent.mcp.ready With Names-Only Tool Summaries + +**Files:** +- Modify: `lib/crates/fabro-mcp/src/connection_manager.rs` +- Modify: `lib/crates/fabro-agent/src/session.rs` +- Test: relevant `fabro-mcp` or `fabro-agent` MCP tests. + +- [ ] **Step 1: Add deterministic tool summaries** + +Expose a helper on `McpConnectionManager` or compute in `Session` from `all_tools()`: + +- filter tools by `server_name` +- return qualified tool name as `name` +- return server-provided tool name as `original_name` +- sort by qualified `name` + +- [ ] **Step 2: Enrich ready emissions** + +When emitting `AgentEvent::McpServerReady`, include the tool summaries for that server. Keep existing `server_name` and `tool_count`. + +- [ ] **Step 3: Add MCP tests** + +Cover: + +- ready event includes only tools from the ready server +- summaries are sorted by qualified name +- `name` is the Fabro-qualified MCP tool name +- `original_name` is the server-provided tool name +- descriptions and input schemas are not included + +--- + +### Task 6: Update Event Documentation + +**Files:** +- Modify: `docs/internal/events.md` + +- [ ] **Step 1: Document new events** + +Add sections for: + +- `agent.memory.loaded` +- `agent.skills.discovered` +- `agent.skill.activated` + +For `agent.memory.loaded`, explicitly state that file contents are excluded. + +- [ ] **Step 2: Update MCP ready docs** + +Update `agent.mcp.ready` to show: + +```json +{ + "server_name": "github", + "tool_count": 2, + "tools": [ + { + "name": "mcp__github__create_issue", + "original_name": "create_issue" + } + ], + "visit": 1 +} +``` + +- [ ] **Step 3: Record skill event replacement** + +If `agent.skill.expanded` remains in internal code or docs, mark it internal-only or replaced by `agent.skill.activated`. + +--- + +### Task 7: Verify + +**Files:** +- No new files unless test placement requires it. + +- [ ] **Step 1: Run focused tests** + +Run: + +```bash +cargo nextest run -p fabro-agent -p fabro-workflow -p fabro-types -p fabro-mcp +``` + +- [ ] **Step 2: Run formatting** + +Run: + +```bash +cargo +nightly-2026-04-14 fmt --all +``` + +- [ ] **Step 3: Run clippy for touched crates or workspace** + +Prefer the workspace command if time permits: + +```bash +cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings +``` + +- [ ] **Step 4: Final sanity checks** + +Confirm: + +- memory events never contain file contents +- skills discovered and memory loaded are emitted even for empty lists +- skill activation is persisted rather than filtered as streaming noise +- `agent.mcp.ready` remains backwards-compatible for old events without `tools` +- docs match the serialized event names and payload shapes + + + +## Completed stages +- **toolchain**: succeeded + - 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` + - Output: + ``` + cargo 1.95.0 (f2d3ce0bd 2026-03-21) + ``` +- **preflight_compile**: succeeded + - Script: `cargo check -q --workspace 2>&1` + - Output: (empty) +- **preflight_lint**: succeeded + - Script: `cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1` + - Output: (empty) +- **implement**: succeeded + - Model: claude-opus-4-7, 220.1k tokens in / 51.7k out + - Files: /home/daytona/workspace/fabro/docs/internal/events.md, /home/daytona/workspace/fabro/lib/crates/fabro-agent/src/lib.rs, /home/daytona/workspace/fabro/lib/crates/fabro-agent/src/memory.rs, /home/daytona/workspace/fabro/lib/crates/fabro-agent/src/session.rs, /home/daytona/workspace/fabro/lib/crates/fabro-agent/src/skills.rs, /home/daytona/workspace/fabro/lib/crates/fabro-agent/src/types.rs, /home/daytona/workspace/fabro/lib/crates/fabro-mcp/src/connection_manager.rs, /home/daytona/workspace/fabro/lib/crates/fabro-types/src/run_event/agent.rs, /home/daytona/workspace/fabro/lib/crates/fabro-types/src/run_event/mod.rs, /home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/event/convert.rs, /home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/event/names.rs, /home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/handler/prompt.rs +- **simplify_opus**: succeeded + - Model: claude-opus-4-7, 119.3k tokens in / 38.4k out + - Files: /home/daytona/workspace/fabro/lib/crates/fabro-agent/src/lib.rs, /home/daytona/workspace/fabro/lib/crates/fabro-agent/src/memory.rs, /home/daytona/workspace/fabro/lib/crates/fabro-agent/src/session.rs, /home/daytona/workspace/fabro/lib/crates/fabro-agent/src/skills.rs, /home/daytona/workspace/fabro/lib/crates/fabro-agent/src/types.rs, /home/daytona/workspace/fabro/lib/crates/fabro-types/src/lib.rs, /home/daytona/workspace/fabro/lib/crates/fabro-types/src/run_event/agent.rs, /home/daytona/workspace/fabro/lib/crates/fabro-types/src/run_event/mod.rs, /home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/event/convert.rs, /home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/event/names.rs + + +# Simplify: Code Review and Cleanup + +Review changes vs. origin for reuse, quality, and efficiency. Fix any issues found. + +## Phase 1: Identify Changes + +Run 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. + +## Phase 2: Launch Three Review Agents in Parallel + +Use 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. + +### Agent 1: Code Reuse Review + +For each change: + +1. 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. +2. Flag any new function that duplicates existing functionality. Suggest the existing function to use instead. +3. 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. + +Note: This is a greenfield app, so focus on maximizing simplicity and don't worry about changing things to achieve it. + +### Agent 2: Code Quality Review + +Review the same changes for hacky patterns: + +1. Redundant state: state that duplicates existing state, cached values that could be derived, observers/effects that could be direct calls +2. Parameter sprawl: adding new parameters to a function instead of generalizing or restructuring existing ones +3. Copy-paste with slight variation: near-duplicate code blocks that should be unified with a shared abstraction +4. Leaky abstractions: exposing internal details that should be encapsulated, or breaking existing abstraction boundaries +5. Stringly-typed code: using raw strings where constants, enums (string unions), or branded types already exist in the codebase + +Note: This is a greenfield app, so be aggressive in optimizing quality. + +### Agent 3: Efficiency Review + +Review the same changes for efficiency: + +1. Unnecessary work: redundant computations, repeated file reads, duplicate network/API calls, N+1 patterns +2. Missed concurrency: independent operations run sequentially when they could run in parallel +3. Hot-path bloat: new blocking work added to startup or per-request/per-render hot paths +4. Unnecessary existence checks: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error +5. Memory: unbounded data structures, missing cleanup, event listener leaks +6. Overly broad operations: reading entire files when only a portion is needed, loading all items when filtering for one + +## Phase 3: Fix Issues + +Wait 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. + +When done, briefly summarize what was fixed (or confirm the code was already clean). \ No newline at end of file diff --git a/stages/007-simplify_gpt@1/provider_used.json b/stages/007-simplify_gpt@1/provider_used.json new file mode 100644 index 000000000..a04162cbf --- /dev/null +++ b/stages/007-simplify_gpt@1/provider_used.json @@ -0,0 +1,5 @@ +{ + "mode": "agent", + "provider": "openai", + "model": "gpt-5.5" +} \ No newline at end of file diff --git a/stages/007-simplify_gpt@1/response.md b/stages/007-simplify_gpt@1/response.md new file mode 100644 index 000000000..33f22c0a7 --- /dev/null +++ b/stages/007-simplify_gpt@1/response.md @@ -0,0 +1,24 @@ +Implemented review cleanup and validation. + +What I fixed: +- Restored the durable `provider_profile` contract to `String` for: + - `AgentMemoryLoadedProps` + - `AgentSkillsDiscoveredProps` +- Added explicit `loaded_bytes` to `MemoryDocument` instead of deriving it ad hoc from `content.len()`. +- Reduced `fabro-agent` coupling to durable API props by adding internal agent summary types: + - `MemoryFileSummary` + - `SkillSummary` + - `SkillActivationSource` + - `McpToolSummary` +- Restored explicit internal `tool_count` on `AgentEvent::McpServerReady`, while still carrying names-only tool summaries. +- Updated workflow conversion to map internal agent summary types into `fabro-types` event props. +- Updated tests to match the cleaned-up internal/durable boundaries. +- Ran rustfmt. + +Validation passed: +- `cargo nextest run -p fabro-agent -p fabro-workflow -p fabro-types -p fabro-mcp` + - 1840 passed, 122 skipped +- `cargo +nightly-2026-04-14 fmt --all` +- `cargo +nightly-2026-04-14 clippy -q -p fabro-agent -p fabro-workflow -p fabro-types -p fabro-mcp --all-targets -- -D warnings` + +Working tree now has cleanup modifications in 8 Rust files; no docs changes were needed during this review pass. \ No newline at end of file