Class: Pikuri::Agent
- Inherits:
-
Object
- Object
- Pikuri::Agent
- Defined in:
- lib/pikuri/agent.rb,
lib/pikuri/agent/event.rb,
lib/pikuri/agent/control.rb,
lib/pikuri/agent/listener.rb,
lib/pikuri/agent/extension.rb,
lib/pikuri/agent/synthesizer.rb,
lib/pikuri/agent/configurator.rb,
lib/pikuri/agent/listener_list.rb,
lib/pikuri/agent/chat_transport.rb,
lib/pikuri/agent/listener/terminal.rb,
lib/pikuri/agent/control/interloper.rb,
lib/pikuri/agent/control/step_limit.rb,
lib/pikuri/agent/listener/token_log.rb,
lib/pikuri/agent/control/cancellable.rb,
lib/pikuri/agent/listener/rate_limited.rb,
lib/pikuri/agent/context_window_detector.rb,
lib/pikuri/agent/listener/in_memory_event_list.rb
Overview
Thin wrapper around RubyLLM::Chat: pikuri owns the *extension surface* (the event-stream listeners that consume normalized chat events, plus the controls that signal back into the loop) while ruby_llm owns the loop itself. The Thought / Tool-call / Observation iteration lives in Chat#complete; pikuri’s job is wiring ruby_llm’s three callbacks at construction time, emitting Event variants from each, and forwarding control signals (step-budget tick, cancellation check, mid-loop input drain) to the appropriate Control.
Roles in this file
Two seams are visible:
-
Listeners (ListenerList + Listener::Base subclasses) — pure consumers of the event stream. The
Agentis the sole emitter; listeners never write back. New rendering or capture targets (a web sink, a structured log) are added here without touching Agent. -
Controls (Control::StepLimit, Control::Cancellable, Control::Interloper) — host-facing signal holders. The
Agentreads from them at well-defined boundaries: Control::StepLimit#tick! on everybefore_tool_call(and Control::StepLimit#reset! at turn start), Control::Cancellable#check! on everybefore_tool_call(and Control::Cancellable#reset! at turn start), Control::Interloper#drain! on everyafter_tool_result.
The two roles are named separately so “what fires when” is a single grep for @listeners.emit in this file.
Step-exhaustion rescue
If the step_limit: Control::StepLimit trips during Chat#ask, #run_loop catches the Exceeded exception, emits an Event::FallbackNotice to the listener stream, and hands off to Synthesizer.run on a fresh RubyLLM::Chat. The synth reuses the parent’s listener stream via ListenerList#for_sub_agent (Terminal padded, TokenLog zeroed, recorder shared by reference) with a name: derived from the parent’s. The synth shares the parent’s cancellable so a user cancel during synthesis still works, and gets a fresh step_limit at max: 1 (defensive — the synth has no tools and shouldn’t trip it). The synth’s answer becomes the value reported by #last_assistant_content, so callers (notably the agent tool from pikuri-subagents) still get a usable reply.
Cancellation rescue
If the cancellable: Control::Cancellable trips during Chat#ask, #run_loop catches the Cancelled exception, emits an Event::Cancelled to the listener stream, and re-raises. No synthesizer fallback runs: cancellation means the user asked the agent to drop everything, so salvaging a partial answer would be the wrong move. The caller (typically a REPL) rescues the re-raised exception and returns control to the user; because #run_loop calls Control::Cancellable#reset! at the start of every turn, the same agent instance can take a fresh turn immediately afterwards.
Defined Under Namespace
Modules: Control, Event, Extension, Listener, Synthesizer Classes: ChatTransport, Configurator, ContextWindowDetector, ListenerList
Instance Attribute Summary collapse
-
#cancellable ⇒ Control::Cancellable?
readonly
The cancellation control this agent was constructed with, or
nilwhen none. -
#chat ⇒ RubyLLM::Chat
readonly
Underlying chat; the extension seam.
-
#context_window_cap ⇒ Integer?
readonly
Context-window cap resolved by ContextWindowDetector at construction time.
-
#extensions ⇒ Array<Extension>
readonly
Extension instances bound to this agent — added via Configurator#add_extension inside the
Agent.newblock. -
#id ⇒ String
readonly
This agent’s unique identifier — empty for the main agent; for sub-agents, the persona-rooted id assigned by the
agenttool frompikuri-subagents(e.g. “researcher 0”, “researcher 1”, “file_miner 0”). -
#interloper ⇒ Control::Interloper?
readonly
The mid-loop user-input control this agent was constructed with, or
nilwhen none. -
#listeners ⇒ ListenerList
readonly
The listener list attached to this agent’s chat.
-
#step_limit ⇒ Control::StepLimit?
readonly
The step-budget control this agent was constructed with, or
nilwhen none. -
#streaming ⇒ Boolean
readonly
truewhen this agent opted into chunk-level streaming (see thestreaming:kwarg on #initialize);falseotherwise. -
#sub_agent_tools ⇒ Array<Tool>
readonly
Tools registered via Configurator#add_sub_agent_tool, in declaration order.
-
#system_prompt ⇒ String
readonly
System prompt actually sent to the chat — equal to the constructor’s
system_prompt:argument plus any snippets appended via Configurator#append_system_prompt (extensions’ <available_skills> / <available_mcps> / <available_agents>, …). -
#tools ⇒ Array<Tool>
readonly
This agent’s tool list in declaration order.
-
#transport ⇒ ChatTransport
readonly
The resolved transport bundle this agent was constructed with — same model id / provider / assume-model-exists flag passed to every
RubyLLM.chatcall originating from this agent (the main chat, the synthesizer rescue, theagenttool frompikuri-subagents).
Class Method Summary collapse
-
.streaming_block(listeners:, cancellable: nil) ⇒ Proc
Build the per-chunk streaming block passed to Chat#ask.
-
.think(transport:, prompt:, cancellable: nil) ⇒ String
One-shot inference.
-
.wire_chat(chat, listeners:, step_limit: nil, cancellable: nil, interloper: nil) ⇒ void
Wire one
RubyLLM::Chatfor pikuri’s event stream and controls.
Instance Method Summary collapse
-
#close ⇒ void
Release agent-owned resources.
- #initialize(transport:, system_prompt:, step_limit: nil, cancellable: nil, interloper: nil, context_window: nil, llama_probe_url: nil, id: '', streaming: false) {|Configurator| ... } ⇒ Agent constructor
-
#internal_add_tool(ruby_llm_tool) ⇒ void
Register a raw
RubyLLM::Toolsubclass on this agent’s underlying chat, bypassing the Tool strict-validation seam. -
#last_assistant_content ⇒ String?
Final assistant message content for the most recent #run_loop.
-
#model ⇒ String
Resolved model id from #transport.
-
#on_close { ... } ⇒ void
Register a handler called by #close.
-
#run_loop(user_message:) ⇒ nil
Run the agent loop for a single user turn.
-
#to_s ⇒ String
Short, single-line config dump suitable for a startup banner or a debug print.
Constructor Details
#initialize(transport:, system_prompt:, step_limit: nil, cancellable: nil, interloper: nil, context_window: nil, llama_probe_url: nil, id: '', streaming: false) {|Configurator| ... } ⇒ Agent
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 |
# File 'lib/pikuri/agent.rb', line 368 def initialize(transport:, system_prompt:, step_limit: nil, cancellable: nil, interloper: nil, context_window: nil, llama_probe_url: nil, id: '', streaming: false, &block) @transport = transport.model ? transport : transport.with(model: RubyLLM.config.default_model) @cancellable = cancellable @closed = false @system_prompt = system_prompt @step_limit = step_limit @interloper = interloper @id = id @streaming = streaming @synth_answer = nil @on_close_handlers = [] # Single Configurator funnel for everything the block adds — # tools, listeners, system-prompt snippets, extensions, and # on_close handlers. See {Configurator} for the per-method # contract. configurator = Configurator.new( transport: @transport, system_prompt_base: system_prompt, id: @id, streaming: @streaming, step_limit: @step_limit, cancellable: @cancellable, interloper: @interloper ) block&.call(configurator) @tools = configurator.tools.dup @sub_agent_tools = configurator.sub_agent_tools.dup @listeners = ListenerList.new(configurator.listeners) configurator.system_prompt_additions.each do |snippet| @system_prompt = "#{@system_prompt}\n\n#{snippet}" end @on_close_handlers.concat(configurator.on_close_handlers) @extensions = configurator.extensions.dup @chat = RubyLLM.chat(**@transport.to_h) @chat.with_instructions(@system_prompt) @tools.each { |t| @chat.with_tool(t.to_ruby_llm_tool) } @context_window_cap = ContextWindowDetector.new( override: context_window, ruby_llm_reported: @chat.model.context_window, llama_probe_url: llama_probe_url ).detect self.class.wire_chat( @chat, listeners: @listeners, step_limit: @step_limit, cancellable: @cancellable, interloper: @interloper ) # One-shot context-window cap: lets every listener that # cares (notably TokenLog) pick the value off the stream # before any Tokens event arrives. @listeners.emit(Event::ContextCap.new(cap: @context_window_cap)) # Bind sweep — each extension gets its chance to install # per-agent state (dynamic tools via #internal_add_tool, # per-agent close hooks via #on_close, etc.) now that the # chat is fully wired. See IDEAS.md §"Extension protocol # design" for what #configure vs #bind are each for. @extensions.each { |ext| ext.bind(self) } # Fallback cleanup: if the host forgets to call #close, the # at_exit hook fires it on process exit. Idempotent, so an # explicit close earlier makes this a no-op. The closure # captures self, which keeps the agent reachable until # process exit — fine for the handful of agents a typical # host creates; if pikuri grows a long-running host that # constructs many short-lived agents, switch to a single # process-global registry that close-then-removes. at_exit { close } end |
Instance Attribute Details
#cancellable ⇒ Control::Cancellable? (readonly)
Returns the cancellation control this agent was constructed with, or nil when none. Read by extensions that propagate cancellation to their own LLM calls (e.g. the agent tool from pikuri-subagents shares it with spawned sub-agents so one Ctrl+C stops the tree).
511 512 513 |
# File 'lib/pikuri/agent.rb', line 511 def cancellable @cancellable end |
#chat ⇒ RubyLLM::Chat (readonly)
Returns underlying chat; the extension seam.
452 453 454 |
# File 'lib/pikuri/agent.rb', line 452 def chat @chat end |
#context_window_cap ⇒ Integer? (readonly)
Returns context-window cap resolved by ContextWindowDetector at construction time. nil when no source produced a value (custom local model with no override and no reachable llama.cpp /props). Read by extensions that spawn their own ruby_llm calls (notably the agent tool from pikuri-subagents, so spawned sub-agents inherit the same cap without re-probing).
552 553 554 |
# File 'lib/pikuri/agent.rb', line 552 def context_window_cap @context_window_cap end |
#extensions ⇒ Array<Extension> (readonly)
Returns extension instances bound to this agent — added via Pikuri::Agent::Configurator#add_extension inside the Agent.new block. Each instance’s configure runs during the block and its bind runs at the end of #initialize, once per registration (so once per parent agent in the typical setup; sub-agents do not inherit extensions).
543 544 545 |
# File 'lib/pikuri/agent.rb', line 543 def extensions @extensions end |
#id ⇒ String (readonly)
Returns this agent’s unique identifier — empty for the main agent; for sub-agents, the persona-rooted id assigned by the agent tool from pikuri-subagents (e.g. “researcher 0”, “researcher 1”, “file_miner 0”). Propagated to listeners via ListenerList#for_sub_agent(id:) so id-aware ones can tag output. Distinct from the persona’s name (the value the LLM picks in the agent tool’s name: argument).
526 527 528 |
# File 'lib/pikuri/agent.rb', line 526 def id @id end |
#interloper ⇒ Control::Interloper? (readonly)
Returns the mid-loop user-input control this agent was constructed with, or nil when none.
516 517 518 |
# File 'lib/pikuri/agent.rb', line 516 def interloper @interloper end |
#listeners ⇒ ListenerList (readonly)
Returns the listener list attached to this agent’s chat.
499 500 501 |
# File 'lib/pikuri/agent.rb', line 499 def listeners @listeners end |
#step_limit ⇒ Control::StepLimit? (readonly)
Returns the step-budget control this agent was constructed with, or nil when none.
503 504 505 |
# File 'lib/pikuri/agent.rb', line 503 def step_limit @step_limit end |
#streaming ⇒ Boolean (readonly)
Returns true when this agent opted into chunk-level streaming (see the streaming: kwarg on #initialize); false otherwise. Read by extensions that spawn their own ruby_llm calls (notably the agent tool from pikuri-subagents, so spawned sub-agents inherit the same mode).
534 535 536 |
# File 'lib/pikuri/agent.rb', line 534 def streaming @streaming end |
#sub_agent_tools ⇒ Array<Tool> (readonly)
Returns tools registered via Pikuri::Agent::Configurator#add_sub_agent_tool, in declaration order. Invisible to the parent LLM (never sent to ruby_llm); available only to sub-agents whose persona tool_names match. See Configurator‘s “Two tool pools” header for the trifecta-defense rationale.
480 481 482 |
# File 'lib/pikuri/agent.rb', line 480 def sub_agent_tools @sub_agent_tools end |
#system_prompt ⇒ String (readonly)
Returns system prompt actually sent to the chat —equal to the constructor’s system_prompt: argument plus any snippets appended via Pikuri::Agent::Configurator#append_system_prompt (extensions’ <available_skills> / <available_mcps> / <available_agents>, …). Not inherited by sub-agents —each persona owns its own system prompt verbatim.
495 496 497 |
# File 'lib/pikuri/agent.rb', line 495 def system_prompt @system_prompt end |
#tools ⇒ Array<Tool> (readonly)
Returns this agent’s tool list in declaration order. Read by extensions that filter against it (notably the agent tool from pikuri-subagents, which picks the sub-agent’s toolset from the parent’s instances so any already-bound workspace/confirmer wiring travels along). Tools listed here are also the ones registered with ruby_llm — the parent LLM can call any of them. Compare with #sub_agent_tools.
472 473 474 |
# File 'lib/pikuri/agent.rb', line 472 def tools @tools end |
#transport ⇒ ChatTransport (readonly)
Returns the resolved transport bundle this agent was constructed with — same model id / provider / assume-model-exists flag passed to every RubyLLM.chat call originating from this agent (the main chat, the synthesizer rescue, the agent tool from pikuri-subagents). Read by extensions that need to spawn their own ruby_llm calls (e.g. MCP description synthesis, sub-agent delegation).
462 463 464 |
# File 'lib/pikuri/agent.rb', line 462 def transport @transport end |
Class Method Details
.streaming_block(listeners:, cancellable: nil) ⇒ Proc
Build the per-chunk streaming block passed to Chat#ask. Each invocation of the returned proc converts one RubyLLM::Chunk into zero, one, or two delta events (Pikuri::Agent::Event::ThinkingDelta / Pikuri::Agent::Event::AssistantDelta) on listeners. Tool-call chunks are intentionally ignored —partial JSON has no useful rendering; the assembled tool_calls surface through Pikuri::Agent::Event::ToolCall once the message completes.
Lives parallel to wire_chat (instead of being folded into it) because Chat#ask takes the streaming block as an argument rather than a registered callback, so both #run_loop and Pikuri::Agent::Synthesizer.run pass it inline at the call site with &Agent.streaming_block(listeners: …, cancellable: …).
Cancellation polling
When cancellable is non-nil, Pikuri::Agent::Control::Cancellable#check! fires before each chunk’s emit. The before_tool_call wiring in wire_chat only fires when the model requests a tool, which leaves a no-tool turn (e.g. a plain greeting) with zero cancellation points — Ctrl+C trips the flag but nothing reads it. Polling on every streamed chunk closes that gap: an in-flight Cancellation+check! raises on the next chunk delivered after the flag flips, the exception propagates out through ruby_llm’s streaming path (+Chat#ask+ doesn’t rescue), and #run_loop catches it, emits Pikuri::Agent::Event::Cancelled, and re-raises. The pre-emit ordering is deliberate: a chunk that arrives after a cancel request shouldn’t render — the user has said stop.
151 152 153 154 155 156 |
# File 'lib/pikuri/agent.rb', line 151 def self.streaming_block(listeners:, cancellable: nil) ->(chunk) { cancellable&.check! emit_chunk(chunk, listeners) } end |
.think(transport:, prompt:, cancellable: nil) ⇒ String
One-shot inference. Builds a fresh RubyLLM::Chat with no tools, no MCP, no listeners, no step budget, asks prompt as the single user turn, and returns the assistant’s reply as a plain String. Lives parallel to #initialize / #run_loop because the use case (e.g. summarizing an MCP server’s tool set into a short description block before any agent turn runs) is genuinely one-shot — there is no loop, no tool iteration, no listener stream.
prompt is sent as the user message. For a one-shot call there is no behavioral difference between the system slot and the user slot, so we use one parameter; pack any “instructions + data” framing into prompt directly.
Cancellation
Pikuri::Agent::Control::Cancellable#check! fires once before the call and once after, so a flag flipped right around the request raises Pikuri::Agent::Control::Cancellable::Cancelled promptly. The in-flight HTTP call itself is not interrupted — same “gentle cancel” semantic the main loop offers (see Pikuri::Agent::Control::Cancellable‘s class header). For 30s synthesis passes at boot this is still a useful escape hatch: the next check raises and the call returns.
Failure
Errors from the provider (HTTP failure, malformed response, RubyLLM raising) propagate to the caller verbatim — there is no recovery layer here. Callers that want “fail soft on synthesis errors” (e.g. Mcp::Servers) rescue at their level and fall back to a default; this method stays loud.
281 282 283 284 285 286 287 288 289 290 291 |
# File 'lib/pikuri/agent.rb', line 281 def self.think(transport:, prompt:, cancellable: nil) raise ArgumentError, "prompt must not be blank, got #{prompt.inspect}" \ if prompt.nil? || prompt.to_s.strip.empty? transport = transport.with(model: RubyLLM.config.default_model) unless transport.model cancellable&.check! chat = RubyLLM.chat(**transport.to_h) chat.ask(prompt) last = chat..reverse.find { |m| m.role == :assistant } last&.content.to_s end |
.wire_chat(chat, listeners:, step_limit: nil, cancellable: nil, interloper: nil) ⇒ void
This method returns an undefined value.
Wire one RubyLLM::Chat for pikuri’s event stream and controls. Used by both #initialize (on the main chat) and Pikuri::Agent::Synthesizer.run (on the synth chat) so the two share one source of truth for “which callback emits which event.”
Handles the three message-level registered callbacks (after_message, before_tool_call, after_tool_result); the per-chunk streaming callback is separate because ruby_llm takes it as a block to Chat#ask rather than a registered hook — see streaming_block.
98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/pikuri/agent.rb', line 98 def self.wire_chat(chat, listeners:, step_limit: nil, cancellable: nil, interloper: nil) chat. do |msg| (msg, listeners) end chat.before_tool_call do |tc| listeners.emit(Event::ToolCall.new(name: tc.name, arguments: tc.arguments)) step_limit&.tick! cancellable&.check! end chat.after_tool_result do |result| listeners.emit(Event::ToolResult.new(content: result)) drain_interloper(interloper, chat, listeners) if interloper end end |
Instance Method Details
#close ⇒ void
This method returns an undefined value.
Release agent-owned resources. Fires every handler registered via Pikuri::Agent::Configurator#on_close (during the Agent.new block) and #on_close (during Pikuri::Agent::Extension#bind or any post-construction call), in LIFO order — matches Ruby ensure-block semantics so handlers registered later (which may depend on handlers registered earlier) tear down first. Each handler runs inside its own rescue; an exception is logged via Pikuri.logger_for but doesn’t abort the rest. Idempotent —subsequent calls are no-ops.
660 661 662 663 664 665 666 667 668 669 |
# File 'lib/pikuri/agent.rb', line 660 def close return if @closed @closed = true @on_close_handlers.reverse_each do |handler| handler.call rescue StandardError => e LOGGER.warn("on_close handler raised #{e.class}: #{e.}") end end |
#internal_add_tool(ruby_llm_tool) ⇒ void
This method returns an undefined value.
Register a raw RubyLLM::Tool subclass on this agent’s underlying chat, bypassing the Tool strict-validation seam. Sole intended caller: Mcp::Servers::Connect, which uses this to lazy-add MCP-exposed tools after the LLM invokes mcp_connect in a turn.
The internal_ prefix is the warning: native pikuri tools should go through Tool so they get Tool::Parameters validation and the LLM-actionable “Error: …” contract. MCP tools deliberately don’t — see IDEAS.md §“v1 implementation shape” / “MCP tools bypass Pikuri::Tool entirely.”
The added tool does NOT enter @tools, only @chat‘s tool list. Sub-agents (the agent tool from pikuri-subagents) therefore cannot snapshot it — which is the whole point: activation is strictly per-agent, see IDEAS.md §“Per-agent activation, no propagation”.
707 708 709 |
# File 'lib/pikuri/agent.rb', line 707 def internal_add_tool(ruby_llm_tool) @chat.with_tool(ruby_llm_tool) end |
#last_assistant_content ⇒ String?
Final assistant message content for the most recent #run_loop. When the synthesizer rescue fired, returns its answer; otherwise walks the underlying chat’s history. Returns nil if neither source has produced an assistant turn yet.
561 562 563 564 565 566 |
# File 'lib/pikuri/agent.rb', line 561 def last_assistant_content return @synth_answer if @synth_answer last = @chat..reverse.find { |m| m.role == :assistant } last&.content end |
#model ⇒ String
Returns resolved model id from #transport. Convenience delegator for callers that don’t need the full transport bundle.
485 486 487 |
# File 'lib/pikuri/agent.rb', line 485 def model @transport.model end |
#on_close { ... } ⇒ void
This method returns an undefined value.
Register a handler called by #close. Symmetric to Pikuri::Agent::Configurator#on_close — same LIFO + per-handler-rescue + idempotent semantics — but available post-construction, so an Extension‘s bind(agent) can install per-agent cleanup that’s keyed to this specific agent rather than the parent.
679 680 681 682 683 684 |
# File 'lib/pikuri/agent.rb', line 679 def on_close(&blk) raise ArgumentError, 'on_close requires a block' unless block_given? @on_close_handlers << blk nil end |
#run_loop(user_message:) ⇒ nil
Run the agent loop for a single user turn. Emits an Pikuri::Agent::Event::UserTurn with mid_loop: false, resets the step-budget and cancellation controls (so a stale state from a prior turn doesn’t poison this one), and forwards user_message to #chat via ask. Returns nil; rendering and any other observable output is the listeners’ responsibility.
If the step_limit control trips during ask, the rescue branch emits an Pikuri::Agent::Event::FallbackNotice and runs Pikuri::Agent::Synthesizer.run on a fresh RubyLLM::Chat. The synth’s answer is captured for #last_assistant_content; the exception does not bubble out.
If the cancellable control trips during ask, the rescue branch emits an Pikuri::Agent::Event::Cancelled and re-raises the Cancelled exception. No synthesizer fallback runs — see the “Cancellation rescue” section in the class header.
Subsequent calls keep building on the same chat history, so the model sees full multi-turn context.
599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 |
# File 'lib/pikuri/agent.rb', line 599 def run_loop(user_message:) raise ArgumentError, "user_message must not be blank, got #{.inspect}" \ if .nil? || .to_s.strip.empty? @synth_answer = nil @listeners.emit(Event::UserTurn.new(content: , mid_loop: false)) @step_limit&.reset! @cancellable&.reset! if @streaming @chat.ask(, &self.class.streaming_block(listeners: @listeners, cancellable: @cancellable)) else @chat.ask() end nil rescue Control::Cancellable::Cancelled @listeners.emit(Event::Cancelled.new) raise rescue Control::StepLimit::Exceeded => e @listeners.emit(Event::FallbackNotice.new( reason: "agent exhausted #{e.max_steps} steps; synthesizing answer from gathered evidence" )) # Synth runs under this agent's identity but on a fresh # chat with a different system prompt, so it gets a # distinct +_synthesizer+ suffix on the id — same +_+ # separator the sub-agent generator uses, so main becomes # +"synthesizer"+ and a sub-agent +"researcher 0"+ becomes # +"researcher 0_synthesizer"+. Any +TokenLog+ in the list # tags the synth's prompt under that bracket so it's # obvious from the log which turns were the rescue rather # than the original loop. synth_id = @id.empty? ? 'synthesizer' : "#{@id}_synthesizer" synth_chat = RubyLLM.chat(**@transport.to_h) # Defensive step limit on the synth: the synth has no # tools so it should never trip +before_tool_call+, but # guarding the budget anyway means a buggy provider that # somehow returns a tool call doesn't loop forever. synth_step_limit = @step_limit && Control::StepLimit.new(max: 1) @synth_answer = Synthesizer.run( chat: synth_chat, parent_messages: @chat., user_message: , listeners: @listeners.for_sub_agent(id: synth_id), step_limit: synth_step_limit, cancellable: @cancellable, streaming: @streaming ) nil end |
#to_s ⇒ String
Short, single-line config dump suitable for a startup banner or a debug print.
719 720 721 |
# File 'lib/pikuri/agent.rb', line 719 def to_s "Agent(id=#{@id}, model=#{model}, tools=#{@tools.size}, listeners=#{@listeners})" end |