Module: Phronomy::Agent::Concerns::Suspendable Private
- Included in:
- Base
- Defined in:
- lib/phronomy/agent/concerns/suspendable.rb
Overview
This module is part of a private API. You should avoid using this module if possible, as it may be removed or be changed in the future.
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
-
#checkpoint_store=(store) ⇒ void
Sets the idempotency store used to guard against duplicate resumes.
-
#on_approval_required(&block) ⇒ self
private
Registers a callback that is invoked before executing any tool that has +requires_approval true+ set.
-
#resume(checkpoint, approved:, config: {}) ⇒ Hash
private
Resumes a previously suspended invocation from a Phronomy::Agent::Checkpoint.
-
#scope_policy=(policy) ⇒ void
Registers a scope policy callable for this agent instance.
Instance Method Details
#checkpoint_store=(store) ⇒ void
This method returns an undefined value.
Sets the idempotency store used to guard against duplicate resumes.
The store must respond to:
- +consumed?(checkpoint_id)+ ⇒ Boolean
- +consume!(checkpoint_id)+ ⇒ void; raises CheckpointAlreadyResumedError on duplicate
Defaults to a per-instance Phronomy::Agent::CheckpointStore (in-memory, not thread-safe). Assign a shared persistent store when resuming across processes (e.g. Redis-backed). Custom stores are responsible for ensuring thread-safety if shared across threads.
65 66 67 |
# File 'lib/phronomy/agent/concerns/suspendable.rb', line 65 def checkpoint_store=(store) @checkpoint_store = store end |
#on_approval_required(&block) ⇒ self
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
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.
32 33 34 35 |
# File 'lib/phronomy/agent/concerns/suspendable.rb', line 32 def on_approval_required(&block) @approval_handler = block self end |
#resume(checkpoint, approved:, config: {}) ⇒ Hash
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
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.
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 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/phronomy/agent/concerns/suspendable.rb', line 86 def resume(checkpoint, approved:, config: {}) # Guard against duplicate resumes using the idempotency store. _checkpoint_store.consume!(checkpoint.checkpoint_id) # Build a fresh chat with all tools registered. chat = build_chat # Re-apply system instructions and register tools so the LLM has the # same persona/context as the original invocation. build_context # includes all tool classes (static + handoff) via add_capability. context = build_context(checkpoint.original_input, messages: []) apply_instructions(chat, context[:system]) if context[:system] (context[:tool_classes] || []).each { |tc| chat.with_tool(prepare_tool_class(tc)) } # Restore the full conversation (history + user + assistant with tool call). checkpoint..each { |msg| chat. << 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.( role: :tool, content: tool_result.to_s, tool_call_id: checkpoint.pending_tool_call_id ) # Re-register the suspension hook so that any further requires_approval # tools encountered during continuation are intercepted rather than # executed without approval (cascading / chained approval scenario). _register_suspension_hook!(chat) # Continue the LLM loop. Rescue SuspendSignal so that a second # approval-required tool produces a new checkpoint instead of running # without consent. begin response = chat.complete rescue SuspendSignal => signal new_checkpoint = Checkpoint.new( checkpoint_id: SecureRandom.uuid, agent_class: self.class.name, requested_at: Time.now.utc, thread_id: checkpoint.thread_id, original_input: checkpoint.original_input, messages: chat..dup, pending_tool_name: signal.tool_name, pending_tool_args: signal.args, pending_tool_call_id: signal.tool_call_id ) return {output: nil, suspended: true, checkpoint: new_checkpoint, messages: chat.} end output = response.content usage = Phronomy::TokenUsage.from_tokens(response.tokens) run_output_guardrails!(output) {output: output, suspended: false, messages: chat., usage: usage} end |
#scope_policy=(policy) ⇒ void
This method returns an undefined value.
Registers a scope policy callable for this agent instance.
The callable receives +(tool_class, scope, agent)+ and must return +:allow+, +:reject+, or +:approve+.
48 49 50 |
# File 'lib/phronomy/agent/concerns/suspendable.rb', line 48 def scope_policy=(policy) @scope_policy = policy end |