Class: Legate::Agentic::Loop

Inherits:
Object
  • Object
show all
Defined in:
lib/legate/agentic/loop.rb

Overview

Drives the observe -> think -> act loop: ask the planner for the next single action, run it via the executor, feed the result back as an observation, and repeat until the model gives a final answer or the iteration cap is hit.

Returns the same { details:, last_result: } shape as PlanExecutor#execute_plan, so Agent#run_task builds the final event identically for both execution strategies.

Constant Summary collapse

DEFAULT_MAX_ITERATIONS =
8
MAX_OBSERVATION_CHARS =

Long string tool results are truncated to this many characters before being fed back, so one big output doesn’t dominate the prompt each turn.

2_000

Instance Method Summary collapse

Constructor Details

#initialize(planner:, executor:, logger: nil, max_iterations: DEFAULT_MAX_ITERATIONS) ⇒ Loop

Returns a new instance of Loop.

Parameters:

  • planner (#reason_next_action)

    returns a Decision for the next step

  • executor (#execute_step)

    runs one tool step (e.g. a PlanExecutor)

  • logger (Logger, nil) (defaults to: nil)
  • max_iterations (Integer) (defaults to: DEFAULT_MAX_ITERATIONS)


26
27
28
29
30
31
# File 'lib/legate/agentic/loop.rb', line 26

def initialize(planner:, executor:, logger: nil, max_iterations: DEFAULT_MAX_ITERATIONS)
  @planner = planner
  @executor = executor
  @logger = logger || Legate.logger
  @max_iterations = max_iterations
end

Instance Method Details

#run(user_input:, session:, session_service:, invocation_id: nil) ⇒ Hash

Returns { details: <observations>, last_result: <result hash> }.

Returns:

  • (Hash)

    { details: <observations>, last_result: <result hash> }



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
62
63
64
65
# File 'lib/legate/agentic/loop.rb', line 34

def run(user_input:, session:, session_service:, invocation_id: nil)
  observations = []

  @max_iterations.times do |i|
    decision = @planner.reason_next_action(user_input, observations, invocation_id)

    return success(decision.answer, observations) if decision.final?

    if decision.invalid?
      @logger.warn("Agentic loop: model returned an unusable decision at step #{i + 1}; stopping.")
      return error('The agent could not decide on a valid next action.', observations)
    end

    result = execute(decision, session, session_service, invocation_id)
    observation = { tool: decision.tool, params: decision.params, result: sanitize(result) }
    spinning = observation == observations.last
    observations << observation

    # Loop-breaker: the model just repeated the exact same action and got
    # the exact same result — re-running won't make progress, so stop and
    # summarize rather than burn the rest of the iteration budget.
    next unless spinning

    @logger.warn("Agentic loop: repeated action '#{decision.tool}' with no change; stopping to avoid spinning.")
    return finish_without_final(user_input, observations, invocation_id,
                                fallback: "Stopped after repeating the same action ('#{decision.tool}') without progress.")
  end

  @logger.warn("Agentic loop: reached the #{@max_iterations}-iteration cap without a final answer.")
  finish_without_final(user_input, observations, invocation_id,
                       fallback: "Stopped after #{@max_iterations} steps without a final answer.")
end