Class: Pikuri::Tool::SubAgent
- Inherits:
-
Pikuri::Tool
- Object
- Pikuri::Tool
- Pikuri::Tool::SubAgent
- Defined in:
- lib/pikuri/tool/sub_agent.rb
Overview
The sub_agent tool, expressed as a Pikuri::Tool subclass: instantiating Tool::SubAgent.new(parent_agent) produces a tool whose #to_ruby_llm_tool wiring is identical to any bundled tool’s, so ruby_llm sees nothing special about it. When the model calls it, the closure inside execute spawns a fresh Agent that runs its own Thought / Tool-call / Observation loop on a clean message history, then returns only the sub-agent’s final assistant message back as the parent’s next observation.
The sub-agent reuses the parent’s transport, system_prompt, context_window_cap, and name (as its hierarchical prefix), so it shares the same persona, hits the same server, and inherits the same context-window cap without re-probing. Its tool list is a snapshot of the parent’s Agent#tools taken at construction —Agent#allow_sub_agent only appends the sub-agent tool to its own @tools after this snapshot, so the sub-agent’s tool list never contains itself (recursion guard).
Its listener list comes from the parent’s Agent#listeners via Agent::ListenerList#for_sub_agent, which forwards to each listener’s own for_sub_agent hook: Terminal swaps to a padded fresh instance, TokenLog resets its snapshot, and listeners without the hook (Agent::Listener::InMemoryEventList, …) are shared by reference so structured capture flows continuously.
Controls are derived per the per-control rule: a fresh Agent::Control::StepLimit at the new cap (mutable counter is per-chat), the same Agent::Control::Cancellable shared by reference (one cancel! stops the whole tree), and no Agent::Control::Interloper (the host has no handle to sub-agents).
All parent state is captured by value at construction — the closure does not chase parent_agent mutations later. The one piece of mutable state is a monotonic counter used to generate sub-agent ids: “sub_agent 0”, “sub_agent 1”, … at the top level; nested children of “sub_agent 0” are “sub_agent 0_0”, “sub_agent 0_1”, … — the “sub_agent ” prefix appears once at the top and the underscore-separated counter chain records depth.
Constant Summary collapse
- DESCRIPTION =
Description shown to the LLM. Follows the opencode-shape (summary +
Usage:bullets) prescribed by the project’s tool-description convention. <<~DESC Delegate a self-contained task to a fresh sub-agent that runs its own Thought / Tool-call / Observation loop on a clean conversation, returning only its final assistant message. Usage: - Use to isolate side-quests — research, multi-step lookups, exploratory tool use — so intermediate observations do not clutter your own context. - The sub-agent has your tools minus `sub_agent` itself, so it cannot recurse. - It shares your system prompt — persona, tool-use conventions, and output format carry over. Do NOT re-explain who you are or how to use tools. - It cannot see your conversation. Put ALL task-specific context inside `task`; the sub-agent has zero memory of what came before. DESC
Constants inherited from Pikuri::Tool
CALCULATOR, FETCH, WEB_SCRAPE, WEB_SEARCH
Instance Attribute Summary
Attributes inherited from Pikuri::Tool
#description, #execute, #name, #parameters
Instance Method Summary collapse
Methods inherited from Pikuri::Tool
Constructor Details
#initialize(parent_agent, max_steps: 10) ⇒ SubAgent
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/pikuri/tool/sub_agent.rb', line 70 def initialize(parent_agent, max_steps: 10) transport = parent_agent.transport system_prompt = parent_agent.system_prompt sub_tools = parent_agent.tools.dup listeners = parent_agent.listeners parent_step_limit = parent_agent.step_limit parent_cancel = parent_agent.cancellable context_window = parent_agent.context_window_cap parent_name = parent_agent.name streaming = parent_agent.streaming # Parent's extension list, captured at SubAgent construction # so spawned sub-agents share the *same* extension instances # (configure has already run on the parent — the resulting # tools / snippets / listeners are inherited verbatim via # the kwargs above). Each inherited extension's +bind+ fires # inside the sub-agent's +Agent#initialize+ — that's how # MCP's per-agent connect tool ends up keyed to the # sub-agent rather than the parent, while still sharing the # parent's live MCP clients through the extension instance. # See IDEAS.md §"Sub-agent inheritance — configure-once, # bind-per-agent". inherited_exts = parent_agent.extensions sub_counter = 0 super( name: 'sub_agent', description: DESCRIPTION, parameters: Parameters.build { |p| p.required_string :task, 'Self-contained instructions for the sub-agent, ' \ 'e.g. "Find the populations of Reykjavik and ' \ 'Helsinki in 2024 and report both numbers." ' \ 'It has no access to the parent conversation, ' \ 'so include all necessary context.' }, execute: lambda { |task:| idx = sub_counter sub_counter += 1 sub_name = parent_name.empty? ? "sub_agent #{idx}" : "#{parent_name}_#{idx}" sub_listeners = listeners.for_sub_agent(name: sub_name) # All inherited state is seeded through the Configurator # block — tools and listeners via add_tools / add_listeners, # extensions via inherit_extensions which retains them for # the bind sweep without re-running configure (the parent # already drove that and the resulting system-prompt # snippets are inherited verbatim through +system_prompt+). sub = Agent.new( transport: transport, system_prompt: system_prompt, step_limit: parent_step_limit&.for_sub_agent(max_steps: max_steps), cancellable: parent_cancel&.for_sub_agent, context_window: context_window, name: sub_name, streaming: streaming ) do |c| c.add_tools(sub_tools) c.add_listeners(sub_listeners) c.inherit_extensions(inherited_exts) end begin sub.run_loop(user_message: task) sub.last_assistant_content ensure # The sub-agent borrows the parent's MCP clients via # the shared {Mcp::Extension} instance; it doesn't own # them. {#close} still fires its own +on_close+ list # (empty for a sub-agent — no extensions registered # any handlers via the inherited path, since they only # re-bind here, not re-configure), so this is a no-op # today. Calling +#close+ anyway means any future # sub-agent-owned resource gets released without # revisiting this site. See {Agent#close}. sub.close end } ) end |