hindsight-ruby
hindsight-ruby is a standalone, framework-agnostic Ruby client for the Vectorize Hindsight API.
It provides a small Ruby-idiomatic API for:
- retaining text memories
- retaining files for async processing
- polling async operation status
- recalling facts
- reflecting over stored memory
Installation
Add to your Gemfile:
gem 'hindsight-ruby'Then run:
bundle installQuick Start
require 'hindsight-ruby'
client = Hindsight::Client.new('http://localhost:8888')
bank = client.bank('quick-start')
bank.retain('The team is shipping v2.0 by end of March')
bank.retain('Maria is on vacation March 20-31')
bank.retain('Maria is the only one certified for production deploys')
# Recall: parallel retrieval (semantic, keyword, graph, temporal) — no LLM
result = bank.recall('shipping schedule')
result.facts.each { |f| puts "- #{f.text}" }
# Reflect: agentic multi-step search across topics, then synthesizes — uses LLM
reflection = bank.reflect('What are the risks to the v2.0 launch?')
puts reflection.text
client.banks.delete('quick-start')API
Hindsight::Client
client = Hindsight::Client.new(
'https://hindsight.example.com',
api_key: ENV['HINDSIGHT_API_KEY'], # optional
headers: { 'X-Request-Source' => 'my-app' }, # optional
logger: Logger.new($stdout), # optional Faraday response logger
# connection: my_faraday, # optional; inject a pre-configured Faraday connection
# allow_insecure: true, # optional, only if you intentionally send credentials over non-localhost http://
open_timeout: 2,
timeout: 10
)If api_key is provided, the client sends:
Authorization: Bearer <api_key>connection: is mainly useful for tests or advanced Faraday customization. If provided, the client uses it directly instead of building an internal connection.
When credentials are configured (api_key or Authorization header), non-localhost http:// base URLs are rejected unless allow_insecure: true is explicitly set.
Methods:
client.bank(bank_id)-
client.banks(resource API for bank containers) -
client.chunks(resource API for chunk retrieval) client.versionclient.healthclient.featuresclient.file_upload_api_supported?
Hindsight::Bank
Retain text
bank.retain(
'User prefers vegetarian restaurants',
context: 'chat',
tags: ['diet'],
document_id: 'msg-123',
timestamp: '2026-03-01T10:00:00Z',
metadata: { 'source' => 'slack' },
entities: [{ text: 'Alice', type: 'person' }],
document_tags: ['onboarding'],
async: false
)The client sends retain payloads to POST /memories.
Return value:
- when
async: true, returnsHindsight::Types::OperationReceipt(poll with#fetch/#wait) - when
async: false(or omitted), returns the raw API response hash
Retain batch
receipt = bank.retain_batch(
[
{ content: 'Alice likes coffee', context: 'chat', tags: ['prefs'] },
{ content: 'Bob likes tea', metadata: { 'source' => 'email' } }
],
document_tags: ['import'],
async: true
)retain_batch follows the same return contract as retain: async: true returns OperationReceipt; async: false returns the raw API response hash.
Retain files
receipt = bank.retain_files(
['/tmp/profile.pdf'],
context: 'chat',
tags: ['onboarding'],
files_metadata: [{ context: 'report', document_id: 'doc-1' }],
allowed_paths: ['/tmp']
)
puts receipt.operation_ids.inspectretain_files returns Hindsight::Types::OperationReceipt.
retain_files raises Hindsight::FeatureNotSupported when the server reports file_upload_api: false.
Security: For path entries, retain_files requires allowed_paths: and resolves paths with File.realpath (resolving symlinks and requiring the target to exist). Do not pass user-controlled paths directly. If you need custom unrestricted file handles, pass upload hashes ({ io:, filename:, content_type: }) instead of paths.
Poll operation status
# Poll each operation id once
statuses = receipt.fetch
# Wait until all operations become terminal (completed/failed/cancelled/etc)
final_statuses = receipt.wait(interval: 1.0, timeout: 120.0)
final_statuses.each do |status|
puts "#{status.id} status=#{status.status} done=#{status.terminal?} ok=#{status.successful?}"
endreceipt.fetch(bank: other_bank) and receipt.wait(bank: other_bank, ...) are also supported if you want to override the bank context explicitly.
You can also call polling APIs directly:
status = bank.operations.get('op-123')
done = bank.operations.wait(%w[op-123 op-456], interval: 0.5, timeout: 60, backoff: :exponential, max_interval: 30)Recall
result = bank.recall(
'What are dietary preferences?',
budget: :mid,
max_tokens: 2048,
types: [:world] # optional: :world, :experience, :observation
)
result.facts.each do |fact|
puts [fact.text, fact.type, fact.entities].inspect
end
puts result.token_countReturns Hindsight::Types::RecallResult (Enumerable).
Reflect
reflection = bank.reflect(
'Summarize this user',
budget: :high
)
puts reflection.textReturns Hindsight::Types::Reflection.
Optional advanced controls:
reflection = bank.reflect(
'Summarize this user',
include: { facts: { max_count: 8 }, tool_calls: { input: false, output: false } },
response_schema: {
type: 'object',
properties: { summary: { type: 'string' } },
required: ['summary']
}
)
puts reflection.json.inspectAdditional endpoints
The gem exposes client-level and bank-level endpoints through resource objects:
# bank containers (client-level)
client.banks.list
client.banks.create(bank_id: 'user-42', name: 'Helpful support assistant')
client.banks.update(bank_id: 'user-42', mission: 'Support users clearly')
client.banks.get('user-42')
client.banks.stats('user-42')
client.banks.delete('user-42')
# chunk retrieval (client-level)
client.chunks.get('chunk-1')
# bank resources
bank.stats
bank.consolidate
bank.memories.list(type: :world, q: 'diet', limit: 50, offset: 0)
bank.memories.get('mem-1')
bank.memories.delete(type: :experience)
bank.mental_models.list(tags: ['team'], tags_match: :all)
bank.mental_models.create(name: 'Team model', source_query: 'How does team communicate?')
bank.mental_models.get('team-model')
bank.mental_models.update(mental_model_id: 'team-model', max_tokens: 4096)
bank.mental_models.refresh('team-model')
bank.mental_models.delete('team-model')
bank.directives.create(name: 'No competitors', content: 'Never recommend competitors.')
bank.directives.list(tags: ['policy'], tags_match: :exact, active_only: true)
bank.directives.get('dir-1')
bank.directives.update(directive_id: 'dir-1', is_active: false)
bank.directives.delete('dir-1')
bank.documents.list(q: 'proposal', limit: 20, offset: 0)
bank.documents.get('doc-1')
bank.documents.delete('doc-1')
bank.entities.list(limit: 20, offset: 0)
bank.entities.get('ent-1')
bank.operations.list(status: 'pending', limit: 20, offset: 0)
bank.operations.get('op-123')
bank.operations.wait(%w[op-123 op-456], interval: 0.5, timeout: 60, backoff: :exponential, max_interval: 30)
bank.operations.cancel('op-123')
bank.observations.delete
bank.observations.delete_for_memory('mem-1')
bank.tags.list(q: 'user:*', limit: 100, offset: 0)
bank.config.get
bank.config.patch(updates: { llm_model: 'gpt-4.1' })
bank.config.reset
bank.graph.get(type: 'world', q: 'alice', tags: ['team'], tags_match: 'all_strict', limit: 100)Error Handling
All gem errors inherit from Hindsight::Error.
Hindsight::ValidationErrorHindsight::ConnectionErrorHindsight::TimeoutError-
Hindsight::APIError(status,body,retriable?) Hindsight::FeatureNotSupported
APIError#message includes status context; use e.body for full server payload.
Example:
begin
bank.retain('hello')
rescue Hindsight::APIError => e
warn "status=#{e.status} retriable=#{e.retriable?} body=#{e.body.inspect}"
endType Objects
Hindsight::Types::FactHindsight::Types::RecallResultHindsight::Types::ReflectionHindsight::Types::OperationReceiptHindsight::Types::OperationStatus
Notes:
-
Fact#typeis optional; when the API payload omits type, it isnil. - Type parsers normalize hash keys internally and read API fields using string-key conventions.
Compatibility Notes
- Requires Ruby
>= 3.1 - Uses Faraday 2.x and
faraday-multipart
Development
bundle install
bundle exec rspec