Skip to content

[superseded] Agent-as-tool delegation — see activeagent#360 - #7

Closed
TonsOfFun wants to merge 1 commit into
mainfrom
claude/agent-as-tool-delegation
Closed

[superseded] Agent-as-tool delegation — see activeagent#360#7
TonsOfFun wants to merge 1 commit into
mainfrom
claude/agent-as-tool-delegation

Conversation

@TonsOfFun

Copy link
Copy Markdown
Contributor

I recommend closing this in favour of activeagent#360. Opening it as a draft so the decision is explicit rather than silent — the work is real and passing, it is just in the wrong repo.

Why I think it belongs in activeagent

docs/framework/v2-extraction-roadmap.md in activeagent draws the line:

  • activeagent — execution: agents, providers, tools, telemetry
  • solid_agent — persistence: contexts, generations, tool streams, memory, pricing
  • the platform — accounts, billing, quotas, hosted UI, multi-tenancy

Delegation is execution, not persistence. And the roadmap lists it explicitly as an activeagent v2 item:

4. Agent-to-agent delegation.tools_function only routes back to self. The platform built call_agent (sub-agent invocation with a Thread.current depth cap) as an app tool. v2: a first-class delegation primitive — invoke another agent class/instance as a tool, with depth limits and shared trace/context correlation.

I built the core version first, was asked to move it here, and only found the roadmap afterwards while researching the records extraction. Flagging it rather than quietly letting whichever PR merges first decide.

What this branch contains, if you want it anyway

The same feature adapted to this gem's conventions, and the adaptation surfaced two things worth keeping regardless of where delegation lands:

  • SolidAgent::ModelPricing is better than the Delegation::Pricing I wrote for core. It resolves rates from RubyLLM's registry with a static fallback and a blended default, so max_cost works with no configuration — and mock models correctly price at $0, so a cost budget never trips by accident in tests. If delegation stays in activeagent, that gem needs an equivalent or the two should share one.
  • HasTools sets the convention that tools are passed explicitly. So this version uses prompt(tools: delegated_tools) rather than auto-merging, which drops the core patch entirely — no monkeypatching, survives ActiveAgent upgrades. auto_delegate! is opt-in and documented as coupled to a private method.

Also here: an integration harness (test/integration/, rake test:integration) running against a real ActiveAgent::Base and driving the genuine provider tool loop, plus a test-only Fake provider giving backend-swap tests a second service without pulling in a vendor SDK. 55 integration tests, 0 failures; 220 unit unchanged. That harness is useful independent of delegation — if this PR closes, it is worth salvaging.

If you close this

#6 (the records extraction) was deliberately rebased off main rather than stacked on this branch, so closing this one costs nothing there. The one casualty is test/integration/, which lives only here — say the word and I will lift it onto a standalone branch.


Generated by Claude Code

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.
Cost budgets price themselves through the existing ModelPricing, which
already resolves rates from RubyLLM's registry with a static fallback, so
`max_cost` works with no configuration and mock models correctly cost $0.
Delegated tools are passed explicitly via `delegated_tools`, matching how
HasTools passes `tools` — no monkeypatching, and it survives ActiveAgent
upgrades. `auto_delegate!` opts into merging them onto every action; it is
opt-in because it prepends ActiveAgent's private prepare_prompt_parameters,
wrapping the method from the outside but still coupled to an internal.
Delegated generations inherit the parent's trace id via a before_generation
callback, so a delegation tree reads as one trace.
Tests run against a real ActiveAgent::Base and drive the genuine provider
tool loop. They need their own harness and rake task: the unit harness mocks
Rails, which makes `require "active_agent"` raise in the same process, so
`rake` now runs the two suites separately. A test-only Fake provider gives
the backend-swap tests a second service without pulling in a vendor SDK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RbmULyN7NyPwv62s7eWRmD
@superconductor-for-github

Copy link
Copy Markdown

Superconductor is workingView implementation


I'll get back to you soon!

@TonsOfFunClaude

Copy link
Copy Markdown
ContributorAuthor

Closing — superseded by activeagent#360.

Confirmed by diff that this is not a variant of that implementation, it is the same one:

FileLinesDifferences
schema.rb2201 — the module name
runner.rb2832 — module name, plus one line routing cost through ModelPricing
backend.rb109identical in both

Delegation is execution, and docs/framework/v2-extraction-roadmap.md files execution under activeagent — naming agent-to-agent delegation as v2 item 4 specifically. Two copies of the same ~900 lines drifting apart is the worse default.

Two things from this branch are worth keeping and are not carried by #360:

  • SolidAgent::ModelPricing is the better cost implementation. activeagent's Delegation::Pricing ships an empty rate table by design, so max_cost silently does nothing until an app registers rates. This one resolves from RubyLLM's registry with a static fallback, and prices mock models at $0 so cost budgets do not trip in tests. activeagent cannot depend on solid_agent, so that gem either grows an equivalent or keeps max_cost opt-in — tracked on #360.
  • test/integration/ — a harness driving a real ActiveAgent::Base through the genuine provider tool loop, plus a Fake provider giving backend-swap tests a second service without a vendor SDK. Useful to this gem independent of delegation, and easy to lift onto a standalone branch.

A docs page covering how activeagent's delegation meets this gem's persistence (delegated runs as AgentRun rows, the ledger against ModelPricing, sub-agent contexts under the parent trace) is deferred, not dropped.

#6 is unaffected — it was rebased off main rather than stacked on this branch, so closing this costs it nothing.


Generated by Claude Code

Sign up for freeto 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

@TonsOfFun@claude