Class: Ask::Agent::ArtifactStore

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

Overview

Session-scoped storage for tool-produced deliverables ("artifacts").

Artifacts come in two kinds, chosen by the producing tool:

  • content — small text deliverables (reports, CSVs, patches, generated code) stored inline in the state store.
  • uri — large or binary deliverables stored externally (object storage, a file service); the store keeps the reference and metadata only.

Metadata always lives in the state store (pure KV, same adapter as sessions/checkpoints/memory — works with every backend and with the in-process Memory fallback):

artifact:<session_id>:<artifact_id>   — one key per artifact
artifact:<session_id>:index           — JSON array of ids

An optional uploader callback lifts inline content to a URI before storage (e.g. upload to S3), so apps that prefer object storage never grow the database: the tool returns content, the session uploads, the store keeps the reference.

Constant Summary collapse

KEY_PREFIX =
"artifact:"
INDEX_SUFFIX =
":index"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(state:, max_content_size: 100_000, uploader: nil) ⇒ ArtifactStore

Returns a new instance of ArtifactStore.

Parameters:

  • state (Ask::State::Adapter)

    backing store

  • max_content_size (Integer) (defaults to: 100_000)

    inline content cap (chars)

  • uploader (Proc, nil) (defaults to: nil)

    called with (content:, filename:, mime_type:) when a tool provides inline content; must return a URI string. When set, inline content is uploaded and the URI stored.



38
39
40
41
42
43
# File 'lib/ask/agent/artifact_store.rb', line 38

def initialize(state:, max_content_size: 100_000, uploader: nil)
  @state = state
  @max_content_size = max_content_size
  @uploader = uploader
  @mutex = Monitor.new
end

Instance Attribute Details

#stateAsk::State::Adapter (readonly)

Returns the underlying adapter.

Returns:

  • (Ask::State::Adapter)

    the underlying adapter



46
47
48
# File 'lib/ask/agent/artifact_store.rb', line 46

def state
  @state
end

#uploaderProc? (readonly)

Returns the uploader callback, if any.

Returns:

  • (Proc, nil)

    the uploader callback, if any



49
50
51
# File 'lib/ask/agent/artifact_store.rb', line 49

def uploader
  @uploader
end

Instance Method Details

#delete(session_id) ⇒ void

This method returns an undefined value.

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

Parameters:

  • session_id (String)


120
121
122
123
124
125
126
# File 'lib/ask/agent/artifact_store.rb', line 120

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

#fetch(session_id, id) ⇒ Hash?

Returns the full record (content or uri).

Parameters:

  • session_id (String)
  • id (String)

Returns:

  • (Hash, nil)

    the full record (content or uri)



109
110
111
112
113
114
# File 'lib/ask/agent/artifact_store.rb', line 109

def fetch(session_id, id)
  data = @state.get(entry_key(session_id, id))
  return nil unless data

  symbolize(data)
end

#list(session_id) ⇒ Array<Hash>

Returns artifact summaries (id, filename, mime_type, size, uri) newest first — content is not included.

Parameters:

  • session_id (String)

Returns:

  • (Array<Hash>)

    artifact summaries (id, filename, mime_type, size, uri) newest first — content is not included



99
100
101
102
103
104
# File 'lib/ask/agent/artifact_store.rb', line 99

def list(session_id)
  load_index(session_id)
    .filter_map { |id| fetch(session_id, id) }
    .reverse
    .map { |r| r.slice(:id, :filename, :mime_type, :size, :uri) }
end

#store(session_id, filename:, mime_type: nil, content: nil, uri: nil) ⇒ Hash

Store an artifact for a session.

Parameters:

  • session_id (String)
  • filename (String)

    required

  • mime_type (String, nil) (defaults to: nil)
  • content (String, nil) (defaults to: nil)

    inline content (small text); xor uri

  • uri (String, nil) (defaults to: nil)

    external reference (large/binary); xor content

Returns:

  • (Hash)

    the stored record filename:, mime_type:, size:, created_at:, content: | uri:

Raises:

  • (ArgumentError)

    on invalid input



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/ask/agent/artifact_store.rb', line 62

def store(session_id, filename:, mime_type: nil, content: nil, uri: nil)
  raise ArgumentError, "filename is required" if filename.to_s.strip.empty?
  raise ArgumentError, "pass either content: or uri:, not both" if content && uri
  raise ArgumentError, "pass either content: or uri:" unless content || uri

  if content
    content = content.to_s
    if content.length > @max_content_size
      raise ArgumentError, "content exceeds #{@max_content_size} chars; use uri: for large artifacts"
    end
    if @uploader
      uri = @uploader.call(content: content, filename: filename.to_s, mime_type: mime_type)
      content = nil
    end
  end

  id = SecureRandom.uuid
  record = {
    id: id,
    filename: filename.to_s,
    mime_type: mime_type,
    size: content ? content.length : nil,
    created_at: Time.now.iso8601
  }
  record[:content] = content if content
  record[:uri] = uri if uri

  @mutex.synchronize do
    @state.set(entry_key(session_id, id), record)
    @state.set(index_key(session_id), (load_index(session_id) + [id]).to_json)
  end
  record
end