Module: Phronomy::Agent::Concerns::Suspendable

Included in:
Base
Defined in:
lib/phronomy/agent/concerns/suspendable.rb

Overview

Adds suspend/resume and tool-approval support to an agent.

Included in Base. When a tool decorated with +requires_approval true+ is called and no synchronous approval handler has been registered, the invocation is suspended and a Phronomy::Agent::Checkpoint is returned so the caller can resume later.

Instance Method Summary collapse

Instance Method Details

#on_approval_required(&block) ⇒ self

Registers a callback that is invoked before executing any tool that has +requires_approval true+ set. The block receives the tool name (String) and the arguments Hash, and must return a truthy value to allow execution. Returning a falsy value causes the tool to return a denial message instead of executing.

When no handler is registered and a tool with +requires_approval+ is called, #invoke returns a suspended result hash containing a Phronomy::Agent::Checkpoint. Call #resume to continue execution after obtaining an approval decision from the user or an external system.

Examples:

Synchronous handler

agent = MyAgent.new
agent.on_approval_required { |tool_name, args| prompt_user(tool_name, args) }

Returns:

  • (self)


28
29
30
31
# File 'lib/phronomy/agent/concerns/suspendable.rb', line 28

def on_approval_required(&block)
  @approval_handler = block
  self
end

#resume(checkpoint, approved:, config: {}) ⇒ Hash

Resumes a previously suspended invocation from a Phronomy::Agent::Checkpoint.

This method reconstructs the conversation state captured at suspension time, injects the tool result (executed or denied), and continues the LLM loop until it produces a final answer.

Parameters:

  • checkpoint (Phronomy::Agent::Checkpoint)

    the checkpoint returned by the suspended #invoke call

  • approved (Boolean)

    +true+ to execute the pending tool; +false+ to inject a denial message and let the LLM handle it gracefully

  • config (Hash) (defaults to: {})

    same runtime options as #invoke

Returns:

  • (Hash)

    +{ output: String, suspended: false, messages: Array, usage: Phronomy::TokenUsage }+

Raises:



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/phronomy/agent/concerns/suspendable.rb', line 46

def resume(checkpoint, approved:, config: {})
  # Build a fresh chat with all tools registered.
  chat = build_chat

  # Re-apply system instructions so the LLM has the same persona/context
  # as the original invocation. build_cached_system_text is memoised, so
  # a Proc- or PromptTemplate-based instructions block is re-evaluated
  # against the original input rather than using a stale cached value.
  system_text = build_cached_system_text(checkpoint.original_input)
  apply_instructions(chat, system_text) if system_text

  # Restore the full conversation (history + user + assistant with tool call).
  checkpoint.messages.each { |msg| chat.messages << msg }

  # Determine the tool result: execute it or inject a denial string.
  tool_result =
    if approved
      tool_instance = chat.tools[checkpoint.pending_tool_name.to_sym]
      tool_instance ? tool_instance.call(checkpoint.pending_tool_args) : "Tool not found."
    else
      "Tool execution denied."
    end

  # Inject the tool result so the LLM can continue.
  chat.add_message(
    role: :tool,
    content: tool_result.to_s,
    tool_call_id: checkpoint.pending_tool_call_id
  )

  # Continue the React loop.
  response = chat.complete

  output = response.content
  usage = Phronomy::TokenUsage.from_tokens(response.tokens)

  run_output_guardrails!(output)

  {output: output, suspended: false, messages: chat.messages, usage: usage}
end