Class: Insika::ToolEnvelope
- Inherits:
-
SimpleDelegator
- Object
- SimpleDelegator
- Insika::ToolEnvelope
- Defined in:
- lib/insika/tool_envelope.rb
Overview
Wraps each allowed tool: per-call timeout
- recording of a non-idempotent side-effect BEFORE the result returns to the model. Delegates everything else (name/description/params) to the real tool.
The tool loop belongs to RubyLLM; this is a decorator over the instances — the Executor never drives roundtrips.
Instance Method Summary collapse
-
#call(args) ⇒ Object
Entry point that RubyLLM invokes (Tool#call in the pinned version).
-
#initialize(tool, state:, checkpoint_store:, tool_registry:, timeout:, skip_side_effects: [], trace_recorder: nil) ⇒ ToolEnvelope
constructor
A new instance of ToolEnvelope.
Constructor Details
#initialize(tool, state:, checkpoint_store:, tool_registry:, timeout:, skip_side_effects: [], trace_recorder: nil) ⇒ ToolEnvelope
Returns a new instance of ToolEnvelope.
23 24 25 26 27 28 29 30 31 32 |
# File 'lib/insika/tool_envelope.rb', line 23 def initialize(tool, state:, checkpoint_store:, tool_registry:, timeout:, skip_side_effects: [], trace_recorder: nil) super(tool) @state = state @checkpoint_store = checkpoint_store @tool_registry = tool_registry @timeout = timeout @skip_side_effects = Array(skip_side_effects) # ids already completed in the interrupted turn @trace_recorder = trace_recorder # duck-type: #record(session_id:, entry:). nil = no trace. end |
Instance Method Details
#call(args) ⇒ Object
Entry point that RubyLLM invokes (Tool#call in the pinned version). A timeout overflow returns to the MODEL as a serialized error — it does not bring down the turn.
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 66 |
# File 'lib/insika/tool_envelope.rb', line 37 def call(args) # A non-idempotent tool call ALREADY COMPLETED in the interrupted # turn -> respond with a marker, NEVER re-execute. The marker returns to # the model, keeping the tool-use protocol intact. call_id = correlation_id return { "skipped" => "already_executed" } if call_id && @skip_side_effects.include?(call_id) # Approval gate: a tool marked `approval` suspends the turn in # :waiting until the operator resolves it. Delegates to the coordinator (the # Executor), which creates/queries the PendingAction and blocks via the # mailbox. Rejection returns to the MODEL as an error (the turn continues), # it does not bring down the turn. CancelledError/TimeoutError from the wait # propagate (they are not ToolTimeout). if approval_required? decision = @state.approval_coordinator.request_approval( task: @state.task, turn: @state.turn, tool: real_name, args: args, actor: @state.actor ) return { error: "rejected by operator" } unless decision.to_s == "approved" end started = monotonic result = with_gate { Async::Task.current.with_timeout(@timeout, ToolTimeout) { __getobj__.call(args) } } record_side_effect!(call_id) if side_effect? trace(call_id, args, result, started) result rescue ToolTimeout err = { error: "TimeoutError: tool exceeded #{@timeout}s" } trace(call_id, args, err, started) err end |