Class: Mistri::Agent
- Inherits:
-
Object
- Object
- Mistri::Agent
- Defined in:
- lib/mistri/agent.rb
Overview
The agent loop: prompt the provider, run any tools it calls, feed the results back, and repeat until it answers without calling tools. Every streamed event reaches the caller's block as it arrives, and every run returns a Result.
Each message persists to the session the moment it completes, so a crash or an abort leaves a replay-valid transcript with no repair step. A tool marked needs_approval suspends the run instead of executing: the run returns at once (no thread ever waits on a human), the decision arrives later as a session entry from any process, and resume settles it and carries on. Session#steer queues a user message from any process while the loop runs; it folds into the transcript at the next turn boundary.
Instance Attribute Summary collapse
-
#session ⇒ Object
readonly
Returns the value of attribute session.
Instance Method Summary collapse
-
#compact ⇒ Object
Compact now (a UI button, a pre-flight trim before a big task).
-
#context_usage ⇒ Object
How full the context is: window:, fraction:.
-
#initialize(provider:, session: nil, system: nil, tools: [], budget: nil, max_concurrency: 4, transform_context: nil, compaction: Compaction.new, retries: RetryPolicy.new, skills: [], before_tool: nil, after_tool: nil, context: nil) ⇒ Agent
constructor
compaction defaults on so long sessions survive their context window; pass false to disable, or a tuned Compaction.
-
#resume(signal: nil, &emit) ⇒ Object
Continue a suspended run.
-
#run(input, images: [], signal: nil, output_schema: nil, &emit) ⇒ Object
Run one exchange: append the user turn, then loop until the model answers without tools, a gated tool suspends the run, the run aborts, or a budget stops it.
-
#task(input, schema:, images: [], signal: nil, fixes: 1, &emit) ⇒ Object
Run an exchange that must end in a JSON value matching schema.
Constructor Details
#initialize(provider:, session: nil, system: nil, tools: [], budget: nil, max_concurrency: 4, transform_context: nil, compaction: Compaction.new, retries: RetryPolicy.new, skills: [], before_tool: nil, after_tool: nil, context: nil) ⇒ Agent
compaction defaults on so long sessions survive their context window; pass false to disable, or a tuned Compaction. It only ever triggers when the model's window is known (catalog or Compaction#window). skills: an array of Skill (or a directory path for Skills.load). Their descriptions join the system prompt and a read_skill tool serves full bodies on demand. before_tool and after_tool are the programmatic gates around every execution: before_tool(call, context) blocks a call by returning the reason as a String, which answers the model in band (and it runs again when an approved call finally executes, so a decision that aged days still passes current policy); after_tool(call, result, context) may return a replacement result, or nil to keep the original.
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/mistri/agent.rb', line 31 def initialize(provider:, session: nil, system: nil, tools: [], budget: nil, max_concurrency: 4, transform_context: nil, compaction: Compaction.new, retries: RetryPolicy.new, skills: [], before_tool: nil, after_tool: nil, context: nil) @provider = provider @session = session || Session.new(store: Stores::Memory.new) skills = skills.is_a?(String) ? Skills.load(skills) : Array(skills) @system = Skills.amend(system, skills) @tools = skills.empty? ? tools : tools + [Skills.reader(skills)] @tools_by_name = @tools.to_h { |tool| [tool.name, tool] } raise ConfigurationError, "duplicate tool names" if @tools_by_name.length != @tools.length @budget = budget || Budget.new @max_concurrency = max_concurrency @transform_context = Array(transform_context) @compaction = compaction || nil @retries = retries || nil @before_tool = before_tool @after_tool = after_tool @context = context end |
Instance Attribute Details
#session ⇒ Object (readonly)
Returns the value of attribute session.
53 54 55 |
# File 'lib/mistri/agent.rb', line 53 def session @session end |
Instance Method Details
#compact ⇒ Object
Compact now (a UI button, a pre-flight trim before a big task). Returns the Compactor result, or nil when there is nothing worth compacting.
132 133 134 135 |
# File 'lib/mistri/agent.rb', line 132 def compact(&) Compactor.call(session: @session, provider: @provider, settings: @compaction || Compaction.new, &) end |
#context_usage ⇒ Object
How full the context is: window:, fraction:. Hosts render meters and near-limit warnings from this; window is nil for models the catalog does not know unless Compaction#window supplies one.
123 124 125 126 127 128 |
# File 'lib/mistri/agent.rb', line 123 def context_usage tokens = Compaction.context_tokens(@session.) window = context_window { tokens: tokens, window: window, fraction: window && (tokens.to_f / window).round(3) } end |
#resume(signal: nil, &emit) ⇒ Object
Continue a suspended run. Undecided approvals return immediately, still suspended. Decided ones settle first: approved calls execute, denied calls answer in band so the model knows and can react. Then the loop carries on as if it never stopped.
80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/mistri/agent.rb', line 80 def resume(signal: nil, &emit) open = @session.open_approvals pending = open.select { |approval| approval[:decision].nil? } if pending.any? return Result.new(message: nil, status: :awaiting_approval, pending: pending.map { |approval| approval[:call] }, usage: Usage.zero) end settle(open, signal, &emit) loop_turns(signal, nil, &emit) end |
#run(input, images: [], signal: nil, output_schema: nil, &emit) ⇒ Object
Run one exchange: append the user turn, then loop until the model answers without tools, a gated tool suspends the run, the run aborts, or a budget stops it. output_schema constrains every non-tool answer to JSON matching the schema, natively where the provider supports it. task adds validation on top; run alone does not validate.
61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/mistri/agent.rb', line 61 def run(input, images: [], signal: nil, output_schema: nil, &emit) if @session.open_approvals.any? raise ConfigurationError, "session is awaiting approvals; call resume" end if input.to_s.empty? && Array(images).empty? raise ArgumentError, "run needs input text or images" end fold_steers # steers queued while idle arrived first; keep that order @session.(Message.user_with_images(input, images)) loop_turns(signal, output_schema, &emit) end |
#task(input, schema:, images: [], signal: nil, fixes: 1, &emit) ⇒ Object
Run an exchange that must end in a JSON value matching schema. Tools run as usual; providers constrain the final answer natively where they can, and the answer is validated here regardless. A violation goes back to the model (fixes more times), then raises SchemaError. The Result carries the validated value as output.
A run that suspends for approval returns as-is: validation applies to completed runs only, so resume the session and re-ask if that happens mid-task.
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/mistri/agent.rb', line 102 def task(input, schema:, images: [], signal: nil, fixes: 1, &emit) result = run(task_input(input, schema), images: images, signal: signal, output_schema: schema, &emit) spent = result.usage fixes.downto(0) do |remaining| result = result.with(usage: spent) return result unless result.completed? value = parse_output(result.text) errors = task_errors(value, schema) return result.with(output: value) if errors.empty? raise SchemaError, "task output failed validation: #{errors.join("; ")}" if remaining.zero? result = run(fix_prompt(errors), signal: signal, output_schema: schema, &emit) spent += result.usage end end |