Class: Yorishiro::SubAgent

Inherits:
Object
  • Object
show all
Defined in:
lib/yorishiro/sub_agent.rb

Overview

Runs a delegated task in its own fresh Conversation so exploratory tool output never enters the parent conversation — only the final assistant text is returned. The caller decides which tools the subagent may use; Tools::Task passes read-only tools only, so no permission prompts fire.

Constant Summary collapse

MAX_ITERATIONS =
15
SYSTEM_PROMPT =
<<~PROMPT
  You are a subagent handling a task delegated by a coding assistant.
  Investigate using the available read-only tools, then reply with your
  findings as plain text. Only your final message is returned to the
  caller, so make it complete and self-contained. Do not ask questions —
  nobody can answer them.
PROMPT

Instance Method Summary collapse

Constructor Details

#initialize(provider:, tools:, output: $stdout, hooks: Yorishiro.configuration.hooks, max_iterations: MAX_ITERATIONS) ⇒ SubAgent

Returns a new instance of SubAgent.



19
20
21
22
23
24
25
26
27
# File 'lib/yorishiro/sub_agent.rb', line 19

def initialize(provider:, tools:, output: $stdout,
               hooks: Yorishiro.configuration.hooks,
               max_iterations: MAX_ITERATIONS)
  @provider = provider
  @tools = tools
  @output = output
  @hooks = hooks
  @max_iterations = max_iterations
end

Instance Method Details

#run(prompt) ⇒ Object

Run the agent loop for prompt and return the final assistant text.



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/yorishiro/sub_agent.rb', line 30

def run(prompt)
  conversation = Conversation.new(system_prompt: SYSTEM_PROMPT)
  conversation.add_message(:user, prompt)
  last_content = nil

  @max_iterations.times do |iteration|
    manage_context!(conversation)

    begin
      result = @provider.chat(conversation, tools: @tools.map(&:definition))
    rescue StandardError => e
      # A failed completion must not throw away the investigation done so
      # far — salvage it the same way the iteration limit does.
      return provider_error_notice(last_content, e)
    end
    content = result[:content]
    tool_calls = result[:tool_calls]

    conversation.add_message(:assistant, content, tool_calls: tool_calls.empty? ? nil : tool_calls)
    last_content = content unless content.to_s.empty?

    return final_answer(content) if tool_calls.empty?

    # Pending tool calls on the last iteration are pointless to execute —
    # there is no follow-up completion to consume their results.
    break if iteration == @max_iterations - 1

    execute_tool_calls(conversation, tool_calls)
  end

  iteration_limit_notice(last_content)
end