Skip to content

feat(agent): add Claude 5 profile - #643

Merged
brynary merged 8 commits into
mainfrom
feat/claude-5-profile
Jul 28, 2026
Merged

feat(agent): add Claude 5 profile#643
brynary merged 8 commits into
mainfrom
feat/claude-5-profile

Conversation

@brynary

@brynary brynary commented Jul 25, 2026

Copy link
Copy Markdown
Member

Summary

  • add the model-scoped claude-5 agent profile and route Claude Fable 5, Opus 5, and Sonnet 5 to it on supported transports
  • align the core, question, skill, and root-agent tool schemas with the accepted Claude 5 contract while omitting Grep and Glob
  • add background-by-default agents with exactly-once, batched parent notifications and explicit TaskOutput, TaskStop, and SendMessage controls
  • derive a Fabro-accurate conditional system prompt and snapshot every tool-availability variant
  • share task state and task ID allocation between root and child profiles

Verification

  • cargo nextest run -p fabro-model --no-fail-fast — 167 passed
  • cargo nextest run -p fabro-llm adapter_registry --no-fail-fast — 10 passed
  • cargo nextest run -p fabro-agent -p fabro-workflow --no-fail-fast — 1,867 passed
  • cargo build --workspace
  • cargo +nightly-2026-04-14 fmt --check --all
  • cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings
  • cargo insta pending-snapshots — no pending snapshots

Notes

  • Bedrock routing covers Fable 5 and Sonnet 5. No unverified Opus 5 Bedrock route is introduced.

Copilot AI review requested due to automatic review settings July 25, 2026 17:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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::Claude5 and 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.

Comment thread lib/components/fabro-agent/src/profiles/claude5_tools.rs
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>
Copilot AI review requested due to automatic review settings July 25, 2026 19:15
@brynary

brynary commented Jul 25, 2026

Copy link
Copy Markdown
Member Author

Review pass: 3 correctness bugs fixed, plus simplifications

Ran a reuse / quality / efficiency review over the diff vs main. Everything below was verified against the source, and both notification bugs were confirmed by reintroducing them and watching the new tests fail.

Fixed in d669a2d

1. Background-agent output was run through skill expansion. This was the serious one. The synthesized notification turn went to run_single_input, which calls expand_skill whenever any skill is registered. find_skill_references matches /name at whitespace boundaries, and escape_notification_xml doesn't escape /. So a child agent reporting cleaned up /tmp produced Unknown skill: /tmp, which failed the entire parent turn. Worse, if the token matched a real skill, expand_skill replaces the whole input with that skill's template — the child's report was silently discarded and a skill body spliced into the parent's context, driven by environment-influenced text. Synthesized harness turns now skip expansion; only text the user typed can invoke a skill.

2. begin_shutdown suppressed the notification before committing to the shutdown. suppress was the first line, before the state lock and before any status check. With strict = true (TaskStopclose_agent), an agent that had just finished returns Err("... is not running") — but its pending result was already gone. Since the prompt explicitly tells the model not to poll TaskOutput, that result was simply lost. Suppression now happens only after shutdown is committed.

3. spawn_inner registered the notification after publishing the agent. Between state.agents.insert and register, the id is discoverable by shutdown_all. In that window: suppress finds nothing to suppress → status leaves Running → the monitor early-returns without calling complete → a pending entry exists that is never completed or suppressed, and next_batch never reports the queue drained again. Registration now precedes publication (safe: nothing can complete before start_tx.send).

4. TaskOutput.timeout schema/executor mismatch — declared number, parsed with as_u64, so a schema-valid 30000.0 failed at runtime. Now integer. (Matches the Copilot comment.)

5. Stale fabro-server test — this was the CI failure. test_model_explicit_provider_alias_returns_canonical_model_id_when_unavailable still expected claude-sonnet-4-6 for the sonnet alias. Full suite is now green: 7442/7442, clippy and fmt clean, no pending snapshots.

Simplifications

  • The supervisor now renders the notification turn; Session no longer imports the envelope format or SubAgentParentNotification.
  • Six near-identical prompt snapshots (~45 KB) replaced by a property test over all eight conditional combinations. The default and all_conditionals snapshots still pin wording. Previously any edit to claude5.md.j2 invalidated all eight at once — exactly when insta accept gets run unread.
  • TodoRuntime's two mutexes collapsed into one.
  • Prompt vocabulary read from the registry instead of hardcoded (matches kimi.rs).
  • Internal vocabulary ("Fabro agent ID") dropped from the SendMessage description.

Catalog verified

Checked 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. sampling_params = false and reasoning_effort = "levels" are right for Claude 5.

Not changed — worth a decision

  • Sonnet 5 intro pricing has no expiry. $2/$10 is correct today, but "through August 31, 2026" is only a comment; there's no date-based cost switching in catalog.rs. From Sept 1 the catalog under-reports Sonnet 5 by 33%. Worth a tracked issue.
  • This flips the default model from Sonnet 4.6 to Sonnet 5 on all three providers. Real product change, not mentioned in the PR summary.
  • web_fetch summarizer is Claude-5-only in workflow runs (handler/llm/api.rs). Not a regression — the workflow backend previously set no summarizer at all — but the CLI gives one to every profile. Making it unconditional would newly enable summarization (and extra LLM calls) for every other profile, so that's a product call, not a cleanup.
  • AnthropicProfile still builds its own TodoRuntime (anthropic.rs:43), so root and child get separate task lists — the exact bug the shared-runtime change fixes for Claude 5. Left alone to keep this PR scoped, but it's the same defect.
  • Permission surface widened slightly. Agent, TaskOutput, TaskStop, SendMessage are now globally resolvable by NativeTool::from_any_name and categorized Subagent, which is_auto_approved allows at every level. A tool with one of those bare names from any source goes from "unknown → Shell → needs approval" to auto-approved. Exposure is small (MCP names are mcp__-prefixed), and SpawnAgent/Wait/CloseAgent were already auto-approved, so this extends existing design rather than inventing it — but it's worth being deliberate about.
  • Session blocks until background agents finish. next_parent_notification_turn returns only when a result is ready or no notifiable agents remain, and there's no iteration budget or wall-clock bound (wall_clock_timeout defaults to None and neither the CLI nor the workflow backend sets it). A wedged child pins the parent stage until cancellation. That's the intended exactly-once design, but it deserves a bound.

Remaining duplication worth a follow-up: claude5_tools::make_bash_tool vs tools::make_shell_tool_with_options vs gpt56::make_shell_command_tool; the Claude 5 question structs/normalizer vs the Anthropic ones (~80 lines); a fifth hand-rolled XML escaper; and definition() duplicated with kimi_tools.rs.

@brynary
brynary marked this pull request as ready for review July 25, 2026 19:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated no new comments.

`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>
Copilot AI review requested due to automatic review settings July 26, 2026 03:44
@brynary

brynary commented Jul 26, 2026

Copy link
Copy Markdown
Member Author

Follow-up: ParentNotificationHub folded into subagent state (f293e3d)

Two of the three bugs in the previous commit were the same defect in different places — ordering bugs in the coupling between ParentNotificationHub's state and SupervisorState. Suppress-vs-commit and register-vs-publish, in ~40 lines of interaction surface. Fixing them by ordering the steps correctly left the shape that produced them intact, so this collapses it.

The hub 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. Moving the registration beside the status it's delivered with makes the whole class unrepresentable rather than merely correctly ordered:

  • Registration is now a field on the SubAgent literal spawn_inner already builds, under the lock that publishes it — there's 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. A rejected shutdown can't discard a result the parent is owed.
  • next_parent_notification_batch scans for a live registration whose status is Finished, and ignores Closing/Closed outright — so a shutdown racing delivery can't park the parent on a result that will never arrive, even if suppression were somehow missed. That's strictly stronger than the old hub, which depended on suppress being called.

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 moved from poking the hub directly to the supervisor API, and now cover spawn-order batching and the shutdown-races-delivery case the old shape couldn't express. I re-verified the suppress-ordering regression test still has teeth by reintroducing the bug against the new structure — it fails, then passes on revert.

Verification: 7445/7445 tests, clippy and fmt clean. Net +259/−187 in subagent.rs; one mutex and one watch channel gone.

Everything else from the earlier review comment stands as listed — ProfileDeps, the NativeTool canonical-name inversion, the BaseProfile delegation boilerplate, and the question-tool near-copy are still open follow-ups.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated 3 comments.

Comment thread lib/components/fabro-agent/src/profiles/claude5_tools.rs Outdated
Comment thread lib/components/fabro-agent/src/profiles/claude5_tools.rs
Comment thread lib/components/fabro-agent/src/session.rs
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>
Copilot AI review requested due to automatic review settings July 26, 2026 11:37
@brynary

brynary commented Jul 26, 2026

Copy link
Copy Markdown
Member Author

Follow-up: shared task runtime for every profile (27549c2)

Session::root_session_id documents the intent directly:

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

That machinery is live — anthropic_task_scope prefers root_session_id, and subagent.rs propagates it into every child. But build() runs once per session and AnthropicProfile constructed its own TodoRuntime inside that call, so root and child resolved the same list_id through different runtimes.

Concretely: both ID counters start at zero, so the parent's TaskCreate returns id 1 and the child's also returns 1, both emitted against the same list. TodoListProjection::upsert matches on id and replaces — the child's task silently overwrote the parent's in the persisted projection. Independently, TaskGet/TaskList read the local runtime, so neither session could see the other's tasks.

This is pre-existing, not introduced here — before this PR the counter lived in AnthropicTaskCounters, also per profile instance, and its comment scoped the guarantee to "inside one session." The earlier commit fixed it for Claude 5 only, because build() passed dependencies positionally and a fourth argument would have meant touching all six call sites. It grew a second constructor (with_native_tools_and_todo_runtime) instead, leaving the other five profiles on a signature that couldn't carry the runtime.

ProfileDeps bundles them so every profile takes (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 of them.

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copilot AI review requested due to automatic review settings July 26, 2026 11:57
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 33 out of 33 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings July 26, 2026 12:02
`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>
@brynary

brynary commented Jul 26, 2026

Copy link
Copy Markdown
Member Author

Three follow-up refactors (3c755a7, 8b7d07b, 73051c9)

1. NativeTool canonical namesNativeTool documents itself as "an identity, not a name," with fabro's vocabulary as canonical and 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", so the Anthropic wire name was the identity and name(ToolVocabulary::Fabro) returned "Agent" — a provider-specific variant name paired with a generic wire name. It also meant the Claude5 arm listed none of them; they fell through to canonical_name() and were right only by accident.

Renamed to BackgroundAgent / AgentOutput / StopAgent / MessageAgent with fabro canonical names, harness names kept as serialize aliases so from_any_name still resolves them, and named explicitly in the Claude5 arm. Also mapped Grep/Glob there — that arm describes the vocabulary, not the profile's registry, so if either were ever registered it would otherwise reach the harness lowercased. Added a test pinning the identity/name split, and a comment recording why these are separate identities from spawn_agent/wait/close_agent/send_input rather than aliases.

2. BaseProfile delegation — all six profiles hand-wrote the same 24 lines of accessor delegation. Replaced with one impl_base_profile_accessors!() invocation. A macro rather than trait defaults because three implementors have no BaseProfile to delegate to (TestProfile, ShutdownTestProfile, the server's AskFabroProfile), so a default would need a runtime fallback for a case the compiler already rules out. Those three are untouched. −144/+63.

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. normalize_claude5_questions reproduced the Anthropic normalizer plus an inlined copy of options_from_anthropic, so option_key, display_text, and bounded_display_field were applied in two places. Now one normalizer parameterized by QuestionLimits; the real Claude 5 deltas (≤4 questions, 2–4 options, 12-char header, required header/descriptions, no preview on multi-select) are data rather than a second code path.

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 multiSelect now defaults to false rather than failing deserialization; the schema still marks it required, which is where that contract belongs. Added tests pinning every strict rule against the shared normalizer, plus one asserting the lenient contract still accepts optional headers and descriptions.

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 web_fetch summarizer being Claude-5-only in workflow runs, the auto-approval surface for the four subagent tool names, and the unbounded parent wait on background agents.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 TaskOutput tool schema marks block/timeout as 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 hit Missing required … errors at runtime. Either make the fields optional and default them in the executor, or (simpler) remove the default entries from the schema to match the required list.
                        "type": "boolean",
                        "default": true,
                        "description": "Whether to wait for completion."
                    },
                    "timeout": {

Comment thread lib/components/fabro-agent/src/question_tools.rs
Copilot AI review requested due to automatic review settings July 26, 2026 12:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • block is marked as required in this tool's schema (and the executor treats it as required), so the JSON-Schema default value is misleading/ineffective. Either make block optional and honor the default in the executor, or drop the default field 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

  • timeout is also required by the schema/executor, so the JSON-Schema default entry here is misleading (it will never be applied). Consider removing the default field, 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

  • multiSelect is required in the Claude 5 question schema, so the JSON-Schema default value is misleading/ineffective. Either make the field optional and rely on the default, or remove the default annotation 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>
Copilot AI review requested due to automatic review settings July 28, 2026 22:21
@brynary
brynary merged commit fa85bc4 into main Jul 28, 2026
14 checks passed
@brynary
brynary deleted the feat/claude-5-profile branch July 28, 2026 22:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 33 out of 33 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants