Agent-as-tool delegation as a core primitive - #360
Merged
Conversation
A tool is a Ruby method the model can call; a delegation is another agent
the model can call. The callee keeps its own instructions, templates, model
and budget, so a specialist agent stays specialist and the generalist
orchestrating it never inherits its prompt.
Three declarations, each where the knowledge lives:
- The contract lives on the sub-agent. `delegation :action, description:`
declares the description the calling model reads, a JSON Schema for the
inputs (block DSL, a plain hash, or any class responding to
`to_json_schema`), and optionally a `returns` schema. A declared `returns`
becomes the sub-agent's response_format; its answer is parsed and checked
against the required keys before the caller sees it, so callers receive
data rather than text to re-parse.
- The budget lives at the call site, because only the caller knows what the
work is worth. `max_calls`, `max_tokens`, `max_cost`, `max_duration` and a
per-call `timeout`, set per delegation and/or agent-wide with
`delegation_budget`. Exhausting one returns a structured result the model
can reason about instead of raising mid-conversation (`on_exceeded: :raise`
is available). Ledgers live on the agent instance, so a budget is scoped to
one generation with nothing to reset.
- The backend lives at the call site too. `backend: :ollama` or
`backend: { provider: :anthropic, model: "claude-haiku-4-5" }` moves a
delegation to different silicon without touching the sub-agent. Provider
swaps go through a cached subclass configured by `generate_with` rather
than merging a hash over stale provider config, and the subclass reports
its parent's name so template lookup still resolves to the original views.
`delegate_to AgentClass` exposes the sub-agent's contracts as tools, with
only:/except:/as: for scoping and renaming, `params:` for forwarding, and
`action:` for declaring a contract at the call site when you don't own the
sub-agent. Delegated tools merge into the action's own `tools:`; scope them
per action with the `delegations:` prompt option.
Cost budgets read from `Delegation::Pricing`, which ships empty on purpose --
vendor pricing moves faster than gem releases, and a stale table silently
under-reports spend. Apps register the rates they pay, or state them inline
on a budget; an unpriced model contributes 0.0 rather than a guess.
Delegated calls emit `delegate.active_agent` and `delegation_refused.active_agent`,
and inherit the parent's trace id so a delegation tree is one trace. To make
that inheritance reliable, `prepare_prompt_parameters` now writes the
generation's trace id back into prompt_options instead of minting a throwaway.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RbmULyN7NyPwv62s7eWRmD
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements item 4 of
docs/framework/v2-extraction-roadmap.md:It also covers the remainder of item 3 ("a token/cost budget alongside the turn cap"). The code this supersedes is
AgentExecutionService#call_agentin the platform app —MAX_CALL_DEPTH = 2, guarded by a thread-local counter.What this is
A tool is a Ruby method the model can call. A delegation is another agent the model can call. The callee keeps its own instructions, templates, model and budget, so a specialist agent stays specialist and the generalist orchestrating it never inherits its prompt.
Three declarations, each where the knowledge lives:
The contract lives on the sub-agent.
Inputs come from a block DSL, a plain JSON Schema hash, or any class responding to
to_json_schema. A declaredreturnsbecomes the sub-agent'sresponse_format; its answer is parsed and checked against the required keys before the caller sees it, so callers receive data rather than text to re-parse.The budget lives at the call site, because only the caller knows what the work is worth.
max_calls,max_tokens,max_cost,max_durationand a per-calltimeout. Exhausting one returns a structured result the model can reason about rather than raising mid-conversation:on_exceeded: :raiseis available. Ledgers live on the agent instance, so a budget is scoped to one generation with nothing to reset.The backend lives at the call site too. Provider swaps go through a cached subclass configured by
generate_withrather than merging a hash over stale provider config, and the subclass reports its parent's name so template lookup still resolves to the original agent's views.Notes for review
Delegation::Pricingships with an empty table on purpose. Vendor pricing moves faster than gem releases and a stale table silently under-reports spend, so apps register their own rates or state them inline on a budget. The tradeoff:max_costdoes nothing until someone registers rates. (solid_agent'sModelPricingalready solves this better — if the two gems should share it, that is worth discussing here.)prepare_prompt_parametersnow writes the trace id back intoprompt_options(||=instead of||) so delegated sub-agents inherit it and a delegation tree reads as one trace. The telemetry instrumentation already did this write-back when enabled.tools:; scope per action withdelegations: falseordelegations: [:name].Verification
Missing credentialsfrom integration tests needing API keys — confirmed identical on a stashed baseline before any of these changes.Open question
activeagents/solid_agent#2ports this same work into solid_agent. I believe that one should be closed in favour of this PR: the roadmap's three-layer split puts execution in activeagent and persistence in solid_agent, and delegation is execution. Flagging it so the decision is explicit rather than implied by whichever merges first.Generated by Claude Code