Class: Mistri::SubAgent
- Inherits:
-
Object
- Object
- Mistri::SubAgent
- Defined in:
- lib/mistri/sub_agent.rb
Overview
Delegation with a clean context: a child agent runs on its own session (the caller's store, linked in the caller's transcript), and only its final answer returns to the parent — exploration never fills the parent's window. Compaction rescues a full context after the fact; spawning avoids filling it in the first place. A child session is its own single-provider session, so delegating to a cheaper model is the sanctioned way to mix models.
Two shapes on one mechanism. A named specialist the host curates:
researcher = Mistri::SubAgent.new(
name: "researcher", description: "Answers factual questions.",
provider: Mistri.provider("claude-haiku-4-5-20251001"),
system: "Research. Report findings only.", tools: [fetch_page],
)
agent = Mistri::Agent.new(provider:, tools: [researcher.tool])
and the open spawn tool, where the model composes each worker: names it, writes its instructions, picks a tool subset, and may pick a model:
spawn = Mistri::SubAgent.spawner(provider:, tools: [fetch_page, search])
Children never receive a spawn tool: delegation is one level deep by construction. Parallel fan-out costs nothing — several spawn calls in one turn run concurrently on the executor pool.
Child events forward into the parent's stream tagged with origin ("researcher#ab12cd34"; nesting of named specialists joins with ">"). Approval-gated tools cannot ride inside a child: a child answers its parent synchronously and cannot fire-and-forget suspend, so statically gated tools are refused at construction and a child that suspends at runtime is denied and reported in band. Gate the delegation itself instead (needs_approval: on the definition or spawner).
Constant Summary collapse
- SPAWNER_DESCRIPTION =
"Delegate a self-contained task to a focused child agent with a clean " \ "context. The child starts blank: give it complete instructions and " \ "every fact it needs. Use it to keep exploration out of your own " \ "context, and spawn several in one turn to fan out independent " \ "angles. Only the child's final answer comes back."
Instance Attribute Summary collapse
-
#description ⇒ Object
readonly
Returns the value of attribute description.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
Class Method Summary collapse
- .forbid_gated!(tools) ⇒ Object
- .run_child(label:, provider:, system:, tools:, task:, context:, schema: nil, **agent_options) ⇒ Object
-
.spawner(provider:, tools: [], models: [], needs_approval: false, **agent_options) ⇒ Object
The open spawn tool over a pool of tools the host allows children to use.
Instance Method Summary collapse
-
#initialize(name:, description:, provider:, system: nil, tools: [], schema: nil, **agent_options) ⇒ SubAgent
constructor
schema: makes the specialist answer in validated JSON (task mode underneath) — fan-out children then return a uniform shape the parent synthesizes instead of five styles of prose.
- #run_child(task, context) ⇒ Object
-
#tool ⇒ Object
The delegate tool: each call runs a fresh child and answers with its final text, plus session_id on the ui channel so a host can link the child's transcript.
Constructor Details
#initialize(name:, description:, provider:, system: nil, tools: [], schema: nil, **agent_options) ⇒ SubAgent
schema: makes the specialist answer in validated JSON (task mode underneath) — fan-out children then return a uniform shape the parent synthesizes instead of five styles of prose.
50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/mistri/sub_agent.rb', line 50 def initialize(name:, description:, provider:, system: nil, tools: [], schema: nil, **) SubAgent.forbid_gated!(tools) @name = name.to_s @description = description @provider = provider @system = system @tools = tools @schema = schema @agent_options = @gate = .delete(:needs_approval) || false end |
Instance Attribute Details
#description ⇒ Object (readonly)
Returns the value of attribute description.
45 46 47 |
# File 'lib/mistri/sub_agent.rb', line 45 def description @description end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
45 46 47 |
# File 'lib/mistri/sub_agent.rb', line 45 def name @name end |
Class Method Details
.forbid_gated!(tools) ⇒ Object
126 127 128 129 130 131 132 133 |
# File 'lib/mistri/sub_agent.rb', line 126 def forbid_gated!(tools) gated = tools.select { |tool| statically_gated?(tool) } return if gated.empty? raise ConfigurationError, "approval-gated tools cannot run inside a sub-agent " \ "(#{gated.map(&:name).join(", ")}); gate the delegation instead" end |
.run_child(label:, provider:, system:, tools:, task:, context:, schema: nil, **agent_options) ⇒ Object
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/mistri/sub_agent.rb', line 109 def run_child(label:, provider:, system:, tools:, task:, context:, schema: nil, **) store = context.session ? context.session.store : Stores::Memory.new child = Session.new(store: store) context.session&.append("subagent", "name" => label, "session_id" => child.id) agent = Agent.new(provider: provider, session: child, system: system, tools: tools, **) origin = "#{label}##{child.id[0, 8]}" emit = ->(event) { forward(event, origin, context) } result = if schema agent.task(task, schema: schema, signal: context.signal, &emit) else agent.run(task, signal: context.signal, &emit) end answer(result, label, child) end |
.spawner(provider:, tools: [], models: [], needs_approval: false, **agent_options) ⇒ Object
The open spawn tool over a pool of tools the host allows children to use. The model may name the worker (a display label riding origins and the transcript link) and grant a tool subset by name; models: is the host's allowlist of child model ids — without one, no model choice is offered at all, so a hallucinated id can never construct a provider or land children on an expensive model.
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/mistri/sub_agent.rb', line 92 def spawner(provider:, tools: [], models: [], needs_approval: false, **) forbid_gated!(tools) if tools.any? { |tool| tool.name == "spawn_agent" } raise ConfigurationError, "the spawn tool never goes in its own pool" end schema = spawner_schema(tools, models, default_model(provider)) Tool.define("spawn_agent", SPAWNER_DESCRIPTION, needs_approval: needs_approval, schema: schema) do |args, context| run_child(label: child_label(args["name"]), provider: child_provider(provider, args["model"], models), system: args.fetch("instructions"), tools: pick(tools, args["tools"]), task: args.fetch("task"), context: context, **) end end |
Instance Method Details
#run_child(task, context) ⇒ Object
79 80 81 82 83 |
# File 'lib/mistri/sub_agent.rb', line 79 def run_child(task, context) SubAgent.run_child(label: @name, provider: @provider, system: @system, tools: @tools, task: task, context: context, schema: @schema, **@agent_options) end |
#tool ⇒ Object
The delegate tool: each call runs a fresh child and answers with its final text, plus session_id on the ui channel so a host can link the child's transcript.
66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/mistri/sub_agent.rb', line 66 def tool sub = self blurb = "#{@description} Runs as a focused sub-agent with a clean " \ "context: give it complete instructions, it starts blank." Tool.define(@name, blurb, needs_approval: @gate, schema: lambda { string :task, "Complete instructions for the sub-agent", required: true }) do |args, context| sub.run_child(args.fetch("task"), context) end end |