feat(agent): add Claude 5 profile - #643
Conversation
There was a problem hiding this comment.
Pull request overview
Adds first-class support for Claude 5 as a model-scoped agent profile across Fabro’s model catalog, LLM routing, and agent harness/tooling. This includes new Claude 5 vocabulary/tool adapters, background-agent notification batching, updated default Claude model selection to Sonnet 5, and extensive prompt/schema snapshot coverage.
Changes:
- Introduce
AgentProfileKind::Claude5and route Claude Fable 5 / Opus 5 / Sonnet 5 through the new profile (Anthropic/OpenRouter/Bedrock where supported). - Add a Claude 5 tool vocabulary + adapters (Read/Write/Edit/Bash/WebFetch/WebSearch + Claude-style subagent controls) and align question/skill tool schemas to the Claude 5 contract.
- Implement exactly-once, batched parent notifications for background subagents and share task state/IDs across root + child profiles.
Reviewed changes
Copilot reviewed 33 out of 33 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| lib/foundation/fabro-model/src/catalog/providers/openrouter.toml | Adds Claude Sonnet 5 on OpenRouter, sets Claude 5 models to use claude-5 agent profile, updates defaults/aliases. |
| lib/foundation/fabro-model/src/catalog/providers/bedrock.toml | Adds Bedrock Claude Sonnet 5, sets Claude 5 agent profile, updates Bedrock default Claude model. |
| lib/foundation/fabro-model/src/catalog/providers/anthropic.toml | Adds Anthropic Claude Sonnet 5, sets Claude 5 agent profile, updates Anthropic default Claude model. |
| lib/foundation/fabro-model/src/catalog.rs | Updates catalog tests and expectations for Sonnet 5 defaults, agent profile mapping, and alias resolution. |
| lib/foundation/fabro-model/src/adapter.rs | Adds AgentProfileKind::Claude5 enum variant with serde/strum mappings. |
| lib/components/fabro-workflow/tests/materialize_run.rs | Updates workflow materialization test default model to claude-sonnet-5. |
| lib/components/fabro-workflow/src/pipeline/transform.rs | Updates transform tests to default to claude-sonnet-5. |
| lib/components/fabro-workflow/src/operations/create.rs | Updates create/validate tests to use claude-sonnet-5 as the default model. |
| lib/components/fabro-workflow/src/handler/llm/api.rs | Selects Claude 5 agent profile (and configures WebFetch summarizer) when using Claude 5 models; adds test coverage. |
| lib/components/fabro-llm/src/adapter_registry.rs | Updates adapter routing expectations to use Claude 5 profile for Claude 5 models and include Sonnet 5. |
| lib/components/fabro-agent/src/tools.rs | Exposes make_web_search_tool_with_api_key for reuse by Claude 5 adapters. |
| lib/components/fabro-agent/src/todo_tools.rs | Moves Claude task ID allocation into shared TodoRuntime for root/child coordination. |
| lib/components/fabro-agent/src/todo_runtime.rs | Adds shared per-list task counters and a next_task_id allocator. |
| lib/components/fabro-agent/src/subagent.rs | Adds parent notification hub, XML-escaped batched notification formatting, and exactly-once delivery semantics. |
| lib/components/fabro-agent/src/skills.rs | Adds Claude 5 skill tool argument schema (skill + optional args). |
| lib/components/fabro-agent/src/session.rs | Delivers batched background-agent completion notifications at safe turn boundaries as followup inputs. |
| lib/components/fabro-agent/src/question_tools.rs | Adds Claude 5 AskUserQuestion schema/normalization and tool registration for Claude 5 vocabulary. |
| lib/components/fabro-agent/src/profiles/snapshots/fabro_agent__profiles__tests__claude5_web_search_prompt_snapshot.snap | Snapshot for Claude 5 prompt with WebSearch enabled. |
| lib/components/fabro-agent/src/profiles/snapshots/fabro_agent__profiles__tests__claude5_web_search_and_subagents_prompt_snapshot.snap | Snapshot for Claude 5 prompt with WebSearch + subagents enabled. |
| lib/components/fabro-agent/src/profiles/snapshots/fabro_agent__profiles__tests__claude5_web_search_and_question_prompt_snapshot.snap | Snapshot for Claude 5 prompt with WebSearch + AskUserQuestion enabled. |
| lib/components/fabro-agent/src/profiles/snapshots/fabro_agent__profiles__tests__claude5_subagents_prompt_snapshot.snap | Snapshot for Claude 5 prompt with subagents enabled. |
| lib/components/fabro-agent/src/profiles/snapshots/fabro_agent__profiles__tests__claude5_subagents_and_question_prompt_snapshot.snap | Snapshot for Claude 5 prompt with subagents + AskUserQuestion enabled. |
| lib/components/fabro-agent/src/profiles/snapshots/fabro_agent__profiles__tests__claude5_question_prompt_snapshot.snap | Snapshot for Claude 5 prompt with AskUserQuestion enabled. |
| lib/components/fabro-agent/src/profiles/snapshots/fabro_agent__profiles__tests__claude5_default_prompt_snapshot.snap | Snapshot for Claude 5 prompt baseline. |
| lib/components/fabro-agent/src/profiles/snapshots/fabro_agent__profiles__tests__claude5_all_conditionals_prompt_snapshot.snap | Snapshot for Claude 5 prompt with all conditional sections enabled. |
| lib/components/fabro-agent/src/profiles/prompts/claude5.md.j2 | Adds the Claude 5 system prompt template with conditional sections. |
| lib/components/fabro-agent/src/profiles/mod.rs | Wires in Claude 5 profile, shares TodoRuntime across builder builds, and adds prompt snapshot tests. |
| lib/components/fabro-agent/src/profiles/claude5.rs | New Claude 5 agent profile implementation and tool registration. |
| lib/components/fabro-agent/src/profiles/claude5_tools.rs | New Claude 5 harness adapters and lifecycle tools (Agent/TaskOutput/TaskStop/SendMessage). |
| lib/components/fabro-agent/src/native_tool.rs | Adds Claude 5 tool vocabulary and new Claude-style subagent tool identities/names. |
| lib/components/fabro-agent/src/memory.rs | Extends memory discovery to treat Claude 5 like Anthropic (AGENTS.md / CLAUDE.md). |
| lib/components/fabro-agent/src/lib.rs | Re-exports Claude5Profile. |
| lib/components/fabro-agent/src/config.rs | Aligns Claude 5 tool timeout defaults with Anthropic profile defaults. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Three correctness fixes in the Claude 5 background-agent path, plus
cleanups from a reuse/quality/efficiency review pass.
Fixes:
- Background-agent output was run through skill expansion. A child that
wrote a bare path ("cleaned up /tmp") failed the whole parent turn with
`Unknown skill: /tmp`, and a child whose output happened to name a real
skill had its report replaced by that skill's template. Synthesized
harness turns now skip expansion; only text the user typed can invoke a
skill.
- `begin_shutdown` suppressed the pending notification before deciding
whether a shutdown would happen. Stopping an agent that had just
finished rejected the stop *and* discarded the result the parent was
owed. Suppression now happens only once shutdown is committed.
- `spawn_inner` registered the notification after publishing the agent in
`state.agents`, so a concurrent `shutdown_all` in that window left a
pending entry the monitor never completes, and the parent's drain loop
would never see the queue as drained. Registration now precedes
publication.
- `TaskOutput.timeout` was declared `number` but parsed with `as_u64`, so
a schema-valid `30000.0` failed at runtime.
- Update the fabro-server alias test for the `sonnet` alias moving to
Claude Sonnet 5.
Cleanups:
- The supervisor renders the notification turn; `Session` no longer knows
the envelope format.
- Replace six near-identical prompt snapshots with a property test over
all eight conditional combinations, keeping the default and
all-conditionals snapshots for wording.
- Collapse `TodoRuntime`'s two mutexes into one.
- Read the prompt vocabulary from the registry instead of hardcoding it.
- Drop internal vocabulary from the `SendMessage` tool description.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review pass: 3 correctness bugs fixed, plus simplificationsRan a reuse / quality / efficiency review over the diff vs Fixed in d669a2d1. Background-agent output was run through skill expansion. This was the serious one. The synthesized notification turn went to 2. 3. 4. 5. Stale Simplifications
Catalog verifiedChecked the Claude 5 entries against Anthropic's current model data — all correct. 1M context and 128K max output for all three; Fable $10/$50, Opus $5/$25, Sonnet intro $2/$10; cache reads at 0.1× input throughout. Not changed — worth a decision
Remaining duplication worth a follow-up: |
`ParentNotificationHub` kept a second `Mutex` and `watch` channel holding a copy of each child's terminal result -- data `SubAgent.status` already owns as `SubAgentStatus::Finished`, and which is never evicted, since nothing removes entries from `SupervisorState.agents`. Two of the three bugs fixed in the previous commit were ordering bugs in the coupling between those two structures: suppress-vs-commit in `begin_shutdown`, and register-vs-publish in `spawn_inner`. Both were fixed by ordering the steps correctly. Keeping the registration beside the status it is delivered with makes that whole class unrepresentable instead: - Registration is now a field on the `SubAgent` literal `spawn_inner` already builds, under the lock that publishes it. There is no window between publishing an agent and registering its notification. - Suppression on shutdown happens inside the critical section that decides the shutdown, after the status transition commits, so a rejected shutdown cannot discard a result the parent is owed. - `next_parent_notification_batch` scans agents for a live registration whose status is `Finished`, and ignores `Closing`/`Closed` outright -- so a shutdown racing delivery can no longer park the parent on a result that will never arrive, even if suppression were missed. `spawn_result_monitor` no longer takes the hub; it bumps a single `watch` counter after committing the status it already commits. Batch order was the queue's insertion order, so `SubAgent` carries a `spawn_seq` to keep delivery oldest-first. Tests move from exercising the hub directly to the supervisor API, and cover spawn-order batching and the shutdown-races-delivery case that the old shape could not express. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up:
|
Task tools scope their list by `root_session_id` -- `Session` documents this as "a subagent session inherits its parent's `root_session_id` so todo tools that scope by root (Anthropic tasks) share one list across all subagents" -- so a root and its children address one logical list. `build()` runs once per session, though, and `AnthropicProfile` constructed its own `TodoRuntime` inside that call. Root and child therefore resolved the same `list_id` through different runtimes: both ID counters started at zero, so both emitted `todo.created` with id `1` for the same list, and `TodoListProjection::upsert` matches on id -- the child's task replaced the parent's in the persisted projection. `TaskGet` and `TaskList` read the local runtime, so neither session could see the other's tasks either. The previous commit's shared runtime fixed this for Claude 5 only, because `build()` passed dependencies positionally and adding a fourth argument would have meant touching all six call sites. It grew a second constructor for Claude 5 instead, leaving the other five on a signature that could not carry the runtime. Bundle them into `ProfileDeps` so every profile takes the same `(model, &deps)`. The duplicate constructor is gone, Anthropic shares the runtime by construction rather than by opting in, and a future dependency reaches all six profiles or none. The existing Claude 5 sharing test is generalized and now also runs for Anthropic; it fails against a per-profile runtime. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up: shared task runtime for every profile (27549c2)
That machinery is live — Concretely: both ID counters start at zero, so the parent's This is pre-existing, not introduced here — before this PR the counter lived in
The Claude 5 sharing test is generalized and now runs for Anthropic too. I verified it has teeth by restoring the per-profile runtime — it fails, then passes on revert. Verification: 7446/7446 tests, clippy and fmt clean, no pending snapshots. This does change Anthropic-profile behavior in a PR titled "add Claude 5 profile." Happy to split it onto its own branch if you'd rather review it as a standalone bug fix. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 33 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
lib/components/fabro-agent/src/session.rs:1336
- This comment says only results already ready at a boundary are delivered, but the loop below awaits
next_parent_notification_turn(...), which can wait for background agents to finish. Please align the comment with the actual (blocking) delivery semantics so future readers don’t assume it’s non-blocking.
// Process the initial input, then drain followups. Claude-compatible
// background-agent results join this same boundary queue: they never
// interrupt inference or a tool call, and all results already ready at
// a boundary are delivered in one additional parent turn.
`NativeTool` documents itself as "an identity, not a name" whose canonical form is fabro's own vocabulary, with harness names layered on as aliases: `to_string = "read_file", serialize = "Read"`. The four Claude 5 subagent tools inverted that. `ClaudeAgent` declared `to_string = "Agent"`, making the Anthropic wire name the identity and leaving `name(ToolVocabulary::Fabro)` returning `"Agent"` -- and pairing a provider-specific variant name with a generic wire name. It also meant the `Claude5` arm listed none of them: they fell through to `canonical_name()` and were correct only by accident. Rename to `BackgroundAgent` / `AgentOutput` / `StopAgent` / `MessageAgent` with fabro canonical names, keep the harness names as `serialize` aliases so `from_any_name` still resolves them, and name them explicitly in the `Claude5` vocabulary arm. Also map `Grep`/`Glob` there: that arm describes the vocabulary rather than the profile's registry, and if either were ever registered it would otherwise reach the harness lowercased. Records why these are separate identities from `spawn_agent`/`wait`/`close_agent`/`send_input` rather than aliases of them, since the capabilities genuinely differ. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
All six provider profiles embed a `BaseProfile` and hand-wrote the same six delegating accessors -- 24 identical lines each. What actually distinguishes them is `build_system_prompt`, and for Claude 5, `register_subagent_tools`. Replace the copies with one `impl_base_profile_accessors!()` invocation. A macro rather than trait defaults because three implementors have no `BaseProfile` to delegate to -- `TestProfile`, the workflow crate's `ShutdownTestProfile`, and the server's `AskFabroProfile` -- so a default would need a runtime fallback for a case the compiler can already rule out. Those three keep their hand-written accessors and are untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Claude5QuestionToolArgs`/`Claude5Question`/`Claude5Option` differed from the Anthropic trio only in required-ness -- `header: String` rather than `Option<String>`, same for each option's `description`. The JSON Schema already enforces that at the model boundary, so the lenient structs deserialize the strict payload unchanged. `normalize_claude5_questions` then reproduced `normalize_anthropic_questions` plus an inlined copy of `options_from_anthropic`, so `option_key`, `display_text`, and `bounded_display_field` were each applied in two places and could drift. Replace both with one normalizer taking a `QuestionLimits`. The genuine Claude 5 deltas -- at most four questions, two to four options, a twelve-character header cap, required header and option descriptions, and no previews on multi-select -- become data rather than a second code path. Two rules serde used to enforce are now the normalizer's: a missing header and a missing option description. Both are still rejected, with a clearer message than serde's "missing field". `multiSelect` now defaults to false instead of being a deserialization error; the schema still marks it required, which is where that contract belongs. Adds tests pinning the strict rules against the shared normalizer, and one asserting the lenient contract still accepts optional headers and descriptions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three follow-up refactors (3c755a7, 8b7d07b, 73051c9)1. Renamed to 2. 3. Question tools — the Claude 5 struct family differed from the Anthropic one only in required-ness, and the schema already enforces that at the model boundary, so the lenient structs deserialize the strict payload unchanged. Two behavior notes on (3), both deliberate: a missing header or option description used to be a serde error and is now a normalizer error — still rejected, clearer message. And Verification (each commit gated separately): 7449/7449 tests, clippy and fmt clean, no pending snapshots. That closes every follow-up from the review comments. The items still open are the ones I flagged as needing your decision rather than a fix: Sonnet 5's intro pricing having no expiry mechanism, the default-model flip, the |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 33 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
lib/components/fabro-agent/src/profiles/claude5_tools.rs:299
- The
TaskOutputtool schema marksblock/timeoutas required while also advertising defaults. Because required fields must always be present, the defaults are misleading and can cause the model to omit them (expecting defaults) and then hitMissing required …errors at runtime. Either make the fields optional and default them in the executor, or (simpler) remove thedefaultentries from the schema to match the required list.
"type": "boolean",
"default": true,
"description": "Whether to wait for completion."
},
"timeout": {
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 33 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
lib/components/fabro-agent/src/profiles/claude5_tools.rs:298
blockis marked as required in this tool's schema (and the executor treats it as required), so the JSON-Schemadefaultvalue is misleading/ineffective. Either makeblockoptional and honor the default in the executor, or drop thedefaultfield here to reflect the actual contract.
"block": {
"type": "boolean",
"default": true,
"description": "Whether to wait for completion."
},
lib/components/fabro-agent/src/profiles/claude5_tools.rs:305
timeoutis also required by the schema/executor, so the JSON-Schemadefaultentry here is misleading (it will never be applied). Consider removing thedefaultfield, or relaxing the required list and applying the default in code.
"timeout": {
"type": "integer",
"minimum": 0,
"maximum": 600_000,
"default": 30000,
"description": "Maximum wait time in milliseconds."
}
lib/components/fabro-agent/src/question_tools.rs:366
multiSelectis required in the Claude 5 question schema, so the JSON-Schemadefaultvalue is misleading/ineffective. Either make the field optional and rely on the default, or remove thedefaultannotation to match the required contract.
"multiSelect": {
"description": "Whether the user may select multiple options.",
"default": false,
"type": "boolean"
}
The schema advertises defaults for `block` and `timeout`, but the executor errored when either was absent. Apply the advertised defaults instead, and keep the type check for values that are present. The required list stays as the Claude 5 contract declares it. Constants now hold the defaults and the maximum so the schema and the executor cannot drift. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
claude-5agent profile and route Claude Fable 5, Opus 5, and Sonnet 5 to it on supported transportsVerification
cargo nextest run -p fabro-model --no-fail-fast— 167 passedcargo nextest run -p fabro-llm adapter_registry --no-fail-fast— 10 passedcargo nextest run -p fabro-agent -p fabro-workflow --no-fail-fast— 1,867 passedcargo build --workspacecargo +nightly-2026-04-14 fmt --check --allcargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warningscargo insta pending-snapshots— no pending snapshotsNotes