Class: Protege::ToolUse

Inherits:
ApplicationRecord show all
Defined in:
app/models/protege/tool_use.rb

Overview

One tool call the agent made and the result it got back, recorded during a run so a run's tool activity is durable. During a single run the calls/results live only in the in-memory Provider::Request; persisting them here gives two things: a reply run's tool use can be replayed by ThreadHistoryResolver on later turns (as real +:assistant+/+:tool+ messages, so the agent remembers what it searched, fetched, or read), and a scheduled run's tool use is captured for audit beside the ResponsibilityRun status trail.

The owning record is polymorphic source: a Protege::Message for a reply run (the triggering inbound message) or a Protege::ResponsibilityRun for a scheduled run. A row pairs a call with its result (the tool loop produces them 1:1). run_id groups one harness run, turn_index the tool-calling round within it, and position the order within a round (an assistant turn may request several calls at once). tool_call_id correlates the call to its result when history is replayed. persona_id is denormalized for per-persona queries.

Constant Summary collapse

STORED_PAYLOAD_LIMIT =

Upper bound on a stored +arguments+/+result+ JSON payload, so a huge tool result (e.g. a large web_fetch body) can't bloat the row. Replay truncates further still (see #replay_result).

16_000
TRUNCATION_MARKER =

Marker appended to any payload clipped for length.

'…[truncated]'

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.clip(text, limit:) ⇒ String

Clip a string to limit characters, appending TRUNCATION_MARKER when it had to be cut.

Parameters:

  • text (String)

    the text to clip

  • limit (Integer)

    the maximum length before truncation

Returns:

  • (String)

    the original text, or a truncated copy with the marker



108
109
110
111
112
# File 'app/models/protege/tool_use.rb', line 108

def clip(text, limit:)
  return text if text.length <= limit

  "#{text[0, limit - TRUNCATION_MARKER.length]}#{TRUNCATION_MARKER}"
end

.record_round(source:, persona:, run_id:, turn_index:, tool_calls:, tool_results:) ⇒ Array<Protege::ToolUse>

Record one tool-calling round: one row per call, paired index-for-index with its result (the tool loop builds tool_calls and tool_results in the same order).

Atomic: the whole round is written in a single transaction, so a serialization or validation failure on one call rolls the whole round back rather than leaving a half-written round (which would replay as an :assistant with unmatched tool_calls and break the next provider request). The transaction wraps only these inserts — it is entered after the tools have run and exits before the next inference call, so no provider request is ever made inside an open transaction.

Idempotent: rows are keyed by [source, run_id, turn_index, position], so re-recording a round (e.g. when the job retries after a transient provider error, replaying the same run_id) finds the existing rows instead of duplicating them.

Parameters:

Returns:



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'app/models/protege/tool_use.rb', line 87

def record_round(source:, persona:, run_id:, turn_index:, tool_calls:, tool_results:)
  transaction do
    tool_calls.each_with_index.map do |call, position|
      result = tool_results[position]
      find_or_create_by!(source:, run_id:, turn_index:, position:) do |use|
        use.persona      = persona
        use.tool_call_id = call.id
        use.tool_name    = call.name
        use.arguments    = clip(call.input.to_json, limit: STORED_PAYLOAD_LIMIT)
        use.result       = clip(result.result.to_h.to_json, limit: STORED_PAYLOAD_LIMIT)
        use.succeeded    = result.result.success?
      end
    end
  end
end

Instance Method Details

#replay_result(limit:) ⇒ String

The stored result, truncated for replay so past results never dominate the context window.

Parameters:

  • limit (Integer)

    maximum characters of result to replay

Returns:

  • (String)

    the (possibly truncated) result JSON text



60
61
62
# File 'app/models/protege/tool_use.rb', line 60

def replay_result(limit:)
  self.class.clip(result.to_s, limit:)
end

#to_provider_tool_callProtege::Inference::Provider::ToolCall

Rebuild the provider tool call for structured replay.

Returns:



48
49
50
51
52
53
54
# File 'app/models/protege/tool_use.rb', line 48

def to_provider_tool_call
  Protege::Inference::Provider::ToolCall.new(
    id:    tool_call_id,
    name:  tool_name,
    input: arguments.present? ? JSON.parse(arguments) : {}
  )
end