Class: Protege::ToolUse
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Protege::ToolUse
- 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_fetchbody) 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
-
.clip(text, limit:) ⇒ String
Clip a string to
limitcharacters, appending TRUNCATION_MARKER when it had to be cut. -
.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_callsandtool_resultsin the same order).
Instance Method Summary collapse
-
#replay_result(limit:) ⇒ String
The stored result, truncated for replay so past results never dominate the context window.
-
#to_provider_tool_call ⇒ Protege::Inference::Provider::ToolCall
Rebuild the provider tool call for structured replay.
Class Method Details
.clip(text, limit:) ⇒ String
Clip a string to limit characters, appending TRUNCATION_MARKER when it had to be cut.
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.
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.
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_call ⇒ Protege::Inference::Provider::ToolCall
Rebuild the provider tool call for structured replay.
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 |