Class: Ask::Agent::ToolOutputStore

Inherits:
Object
  • Object
show all
Defined in:
lib/ask/agent/tool_output_store.rb

Overview

State-backed storage for large tool outputs, keeping them out of the conversation transcript.

When a tool result exceeds the session's offload threshold, the executor stores the full output here and the transcript keeps a short preview plus a reference the model can retrieve with the output_read tool (and the web UI can fetch from the same store).

Storage shape (pure KV — works with every Ask::State::Adapter backend including custom get/set/delete adapters):

output:<session_id>:<call_id>   — one key per offloaded output
output:<session_id>:index       — JSON array of call ids (write order)

store = Ask::Agent::ToolOutputStore.new(state: adapter)
store.store(session_id, "call_1", huge_output)
store.fetch(session_id, "call_1")   # => huge_output
store.delete(session_id)            # session cleanup

Constant Summary collapse

KEY_PREFIX =
"output:"
INDEX_SUFFIX =
":index"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(state:, max_size: 50_000) ⇒ ToolOutputStore

Returns a new instance of ToolOutputStore.

Parameters:

  • state (Ask::State::Adapter)

    backing store

  • max_size (Integer) (defaults to: 50_000)

    stored outputs are truncated to this many characters (with a truncation marker)



31
32
33
34
35
# File 'lib/ask/agent/tool_output_store.rb', line 31

def initialize(state:, max_size: 50_000)
  @state = state
  @max_size = max_size
  @mutex = Monitor.new
end

Instance Attribute Details

#stateAsk::State::Adapter (readonly)

Returns the underlying adapter.

Returns:

  • (Ask::State::Adapter)

    the underlying adapter



38
39
40
# File 'lib/ask/agent/tool_output_store.rb', line 38

def state
  @state
end

Instance Method Details

#delete(session_id) ⇒ void

This method returns an undefined value.

Remove every output for a session (called by Session#delete).

Parameters:

  • session_id (String)


70
71
72
73
74
75
76
# File 'lib/ask/agent/tool_output_store.rb', line 70

def delete(session_id)
  @mutex.synchronize do
    load_index(session_id).each { |call_id| @state.delete(entry_key(session_id, call_id)) }
    @state.delete(index_key(session_id))
  end
  nil
end

#fetch(session_id, call_id) ⇒ String?

Returns the stored output, or nil when absent.

Parameters:

  • session_id (String)
  • call_id (String)

Returns:

  • (String, nil)

    the stored output, or nil when absent



62
63
64
# File 'lib/ask/agent/tool_output_store.rb', line 62

def fetch(session_id, call_id)
  @state.get(entry_key(session_id, call_id))
end

#store(session_id, call_id, content) ⇒ String

Store an output for a tool call (idempotent per call id — a later store with the same call id replaces the earlier one).

Parameters:

  • session_id (String)
  • call_id (String)
  • content (String)

Returns:

  • (String)

    the stored content (possibly truncated)



47
48
49
50
51
52
53
54
55
56
57
# File 'lib/ask/agent/tool_output_store.rb', line 47

def store(session_id, call_id, content)
  stored = content.to_s
  stored = "#{stored[0, @max_size]}\n...(output truncated)" if stored.length > @max_size

  @mutex.synchronize do
    @state.set(entry_key(session_id, call_id), stored)
    index = load_index(session_id)
    @state.set(index_key(session_id), (index + [call_id]).uniq.to_json)
  end
  stored
end