Class: Kward::Agent
- Inherits:
-
Object
- Object
- Kward::Agent
- Defined in:
- lib/kward/agent.rb
Overview
Runs model turns, handles context compaction, dispatches tool calls, and streams high-level events back to CLI and RPC callers.
Agent is the main turn orchestrator. It should know what a turn means:
append the user's input, call the model, persist assistant/tool messages,
retry once after recoverable context overflow, apply in-flight steering, and
emit frontend-neutral Events::* objects. It should not know terminal or RPC
rendering details; callers translate events into their own UI protocol.
Tool implementations own local side effects. Client owns provider HTTP
details. Conversation owns transcript state. Keep future changes in the
lowest layer that owns the behavior, and use Agent only for cross-step turn
coordination.
Instance Attribute Summary collapse
-
#conversation ⇒ Object
readonly
Returns the value of attribute conversation.
Instance Method Summary collapse
-
#ask(input, display_input: nil, on_reasoning_delta: nil, on_retry: nil, cancellation: nil, steering: nil) {|event| ... } ⇒ String
Adds a user message, compacts context when needed, and runs the turn.
-
#initialize(client:, tool_registry: ToolRegistry.new, conversation: Conversation.new, telemetry_logger: TelemetryLogger.new) ⇒ Agent
constructor
A new instance of Agent.
-
#run_turn(on_reasoning_delta: nil, on_retry: nil, cancellation: nil, steering: nil) {|event| ... } ⇒ String
Runs model calls until the assistant returns an answer without pending tool calls, including tool dispatch and one context-overflow retry.
Constructor Details
#initialize(client:, tool_registry: ToolRegistry.new, conversation: Conversation.new, telemetry_logger: TelemetryLogger.new) ⇒ Agent
Returns a new instance of Agent.
27 28 29 30 31 32 |
# File 'lib/kward/agent.rb', line 27 def initialize(client:, tool_registry: ToolRegistry.new, conversation: Conversation.new, telemetry_logger: TelemetryLogger.new) @client = client @tool_registry = tool_registry @conversation = conversation @telemetry_logger = telemetry_logger end |
Instance Attribute Details
#conversation ⇒ Object (readonly)
Returns the value of attribute conversation.
34 35 36 |
# File 'lib/kward/agent.rb', line 34 def conversation @conversation end |
Instance Method Details
#ask(input, display_input: nil, on_reasoning_delta: nil, on_retry: nil, cancellation: nil, steering: nil) {|event| ... } ⇒ String
Adds a user message, compacts context when needed, and runs the turn.
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/kward/agent.rb', line 42 def ask(input, display_input: nil, on_reasoning_delta: nil, on_retry: nil, cancellation: nil, steering: nil, &block) started_at = @telemetry_logger.monotonic_now status = "completed" error = nil cancellation&.raise_if_cancelled! @conversation. @conversation.append_user(input, display_content: display_input) auto_compact_if_needed run_turn(on_reasoning_delta: on_reasoning_delta, on_retry: on_retry, cancellation: cancellation, steering: steering, &block) rescue StandardError => e status = "failed" error = e raise e ensure log_turn(duration_ms: @telemetry_logger.duration_ms(started_at), status: status, error: error) end |
#run_turn(on_reasoning_delta: nil, on_retry: nil, cancellation: nil, steering: nil) {|event| ... } ⇒ String
Runs model calls until the assistant returns an answer without pending tool calls, including tool dispatch and one context-overflow retry.
64 65 66 67 68 69 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 |
# File 'lib/kward/agent.rb', line 64 def run_turn(on_reasoning_delta: nil, on_retry: nil, cancellation: nil, steering: nil) overflow_retried = false steering_state = build_steering_state(steering) do |event| yield event if block_given? end loop do cancellation&.raise_if_cancelled! begin = chat(on_reasoning_delta: on_reasoning_delta, on_retry: on_retry, cancellation: cancellation, steering: steering) do |event| yield event if block_given? end rescue StandardError => e raise if cancellation&.cancelled? raise unless !overflow_retried && ContextOverflow.error?(e) && compact_after_context_overflow(e) overflow_retried = true next end update_conversation_runtime() yield Events::AssistantMessage.new(message: ) if block_given? @conversation.append_assistant() = append_steering_events(steering_state) yield Events::SteeringApplied.new(count: ) if block_given? && .positive? tool_calls = ["tool_calls"] || [:tool_calls] || [] if tool_calls.empty? next if .positive? answer = safe_answer(.fetch("content", [:content] || "")) yield Events::Answer.new(content: answer) if block_given? return answer end tool_calls.each do |tool_call| cancellation&.raise_if_cancelled! yield Events::ToolCall.new(tool_call: tool_call) if block_given? tool_started_at = @telemetry_logger.monotonic_now content = nil status = "completed" error = nil begin content = @tool_registry.dispatch(tool_call, @conversation, cancellation: cancellation) rescue StandardError => e status = "failed" error = e raise e ensure log_tool(tool_call, content: content, duration_ms: @telemetry_logger.duration_ms(tool_started_at), status: status, error: error) end cancellation&.raise_if_cancelled! yield Events::ToolResult.new(tool_call: tool_call, content: content) if block_given? end steered_after_tools = append_steering_events(steering_state) yield Events::SteeringApplied.new(count: steered_after_tools) if block_given? && steered_after_tools.positive? end ensure steering_state&.fetch(:unsubscribe)&.call end |