Every ingot is a set of real tables. Filter them with SQL, rank them by meaning, match names that were misspelled, and do all three in the same query.
Each ingot stores tool results as typed tables. The model writes the query it needs and gets back only the rows that answer it, so a count comes back as one number rather than forty records.
{ "sql": "SELECT company, arr FROM contacts WHERE stage = 'won' AND closed_at >= '2026-07-01' ORDER BY arr DESC LIMIT 3" }
{ "columns": ["company", "arr"], "rows": [ { "company": "Northwind", "arr": 184000 }, { "company": "Contoso", "arr": 96500 }, { "company": "Fabrikam", "arr": 71200 } ], "truncated": false, "next": null }
The wording in the question differs from the wording in the record. SQL matches values, not meaning. Use similarity search for that.
Declare a column with embed: true on /add and Ingot embeds it after the write. Send text with a query and it is embedded too, bound as $q, so a question about running out of database connections finds the record that says the pool was exhausted.
{ "text": "ran out of spare database connections", "sql": "SELECT id, summary, array_cosine_similarity(summary_vec, $q) AS score FROM incidents ORDER BY score DESC LIMIT 3" }
{ "columns": ["id", "summary", "score"], "rows": [ { "id": "INC-01", "score": 0.83, "summary": "Connection pool exhausted on catalog-db" }, { "id": "INC-09", "score": 0.71, "summary": "Replica lag after pool resize" }, { "id": "INC-04", "score": 0.64, "summary": "Timeouts from search under load" } ] }
You need every matching row, or a count of them. Similarity returns a ranking, not a complete set. Filter with SQL first.
Names get misspelled, in the data and in the question. The query runs in DuckDB, so its string-distance functions come with it: jaro_winkler_similarity finds John Kowalski from "Jon Kowalsky", and "north wind" finds Northwind. Nothing is embedded, so it works on any text column.
{ "sql": "SELECT id, name, company FROM contacts WHERE jaro_winkler_similarity( lower(name), 'jon kowalsky') > 0.85" }
{ "columns": ["id", "name", "company"], "rows": [ { "id": "c_2291", "name": "John Kowalski", "company": "Contoso" } ] }
The strings share no characters, like "outage" and "incident". Edit distance can’t link synonyms. Similarity search can.
A contract or a runbook is too long to embed as one value and too long to read back whole. Upload it to /file and Ingot parses, chunks and embeds it in the background. Each chunk keeps its page and section heading, so an answer can say where it came from.
-F "file=@q3-contracts.pdf" -F 'body={ "chunkTokens": 800, "overlapTokens": 120 }'
{ "fileId": "file_3f9c1a…", "mediaType": "application/pdf", "status": "pending", "chunksQuery": "SELECT … FROM ingot_file_chunks WHERE file_id = 'file_3f9c1a…' ORDER BY ordinal" }
The text is already short values in your own tables, like names or one-line summaries. Chunking is for uploaded documents; embed those columns instead.
Some tools return more than the context window can hold. /add stores all of it either way; the receipt is what goes back to the model in its place. schema hands back the table and the queries that find these rows, and full adds a summary and a search term, written in the background.
{ "table": "contacts", "rows": "$.contacts[*]", "key": ["id"], "receipt": "full", "result": toolResult }
{ "table": "contacts", "rowsAdded": 412, "receipt": { "status": "pending", "receiptQuery": "SELECT … FROM ingot_receipts WHERE source_batch = 'batch_1508c8…'" } } # seconds later, receiptQuery answers { "summary": "412 EMEA accounts, 17 at risk", "search_term": "EMEA renewal risk" }
The result is small enough to read whole. A receipt then costs an extra query to get back what the model could have read directly.
They are all one SELECT, so they compose. SQL narrows the rows exactly, fuzzy matching finds the name as someone typed it, and similarity puts what is left in order of meaning.
{ "text": "cannot finish checkout", "sql": "SELECT t.id, t.subject FROM tickets t JOIN contacts c ON c.id = t.contact_id WHERE t.status = 'open' AND jaro_winkler_similarity( lower(c.company), 'north wind') > 0.85 ORDER BY array_cosine_similarity( t.body_vec, $q) DESC LIMIT 3" }
{ "columns": ["id", "subject"], "rows": [ { "id": "T-4410", "subject": "Payment step spins forever" }, { "id": "T-4398", "subject": "Card declined after address change" }, { "id": "T-4371", "subject": "Order total shows zero" } ] }