Feeds
A feed is a prompt with an address. demo.uris.to/buy holds a sentence — find me things worth
buying from these stores — and running it produces items you can search like anything else in the
catalog.
The prompt is not a search. It is given the same tools an MCP client gets, so it can look through the catalog, act on resources, and add what it finds to itself.
Why this costs less than it looks
Section titled “Why this costs less than it looks”Three of the four pieces already exist.
| Already there | |
|---|---|
| tool definitions | Tool.all — each declares tool_name, description and input_schema |
| which tools may run | Grant#tools, filtered by the token’s scopes, per request |
| enforcement, budget, audit | Tool::Base.respond reads Current.grant and calls permit! itself |
| a bounded, cancellable job | Run, TrackedRun, halted?, Gate |
An MCP tool definition and an OpenAI function definition are the same object under two names, so the tools a feed may call are already computed by code that exists. What is missing is the loop.
The loop
Section titled “The loop”A model never runs anything. It emits a request to run something; the server runs it and appends the result as another message. That is the whole mechanism.
messages = [ system, { role: "user", content: feed.prompt } ]
TURNS.times do message = inference.converse(messages:, tools: grant.tools) calls = message["tool_calls"]
break if calls.blank?
messages << message calls.each { |call| messages << tool_result(call) }endtool_result dispatches to the same Tool.call that POST /mcp dispatches to. Nothing is
special-cased for feeds, which is what keeps a feed unable to do more than the token behind it.
What the models can actually do
Section titled “What the models can actually do”Tool calling is a property of the model, not the server — and Ollama’s declared tools capability
is not to be trusted: five of the installed models advertise it and one drives a loop. Measured
against the eleven real tool definitions, with a system message telling the model it has tools:
| Model | Declares tools | One call | A loop |
|---|---|---|---|
qwen3:8b |
yes | yes | yes |
llama3.1:8b |
yes | yes | no — emits the second call as text |
mistral:latest |
yes | no — writes the call into the content field | no |
glm4 (GLM-4-9B) |
yes | no — “I don’t have direct access to external tools” | no |
deepseek-r1 14b/32b |
yes | no — reasons about it, never calls | no |
gemma3:4b |
no | the server answers does not support tools |
no |
A single call proves nothing. llama3.1:8b answers one correctly and then breaks the moment there
is a tool result in the history, which is every turn after the first. qwen3:8b searched, took an
id out of the result, fetched it, and stopped with the answer.
gemma3:4b is currently both the fast and the vision role, so neither can drive a feed. Feeds
take a role of their own:
Analyzer::Base.summary_role # :smart, :fast, :visionFeed::ROLE # :agent -> qwen3:8bBecause Resource.for_role resolves a role against whichever inference resource names the model,
pointing agent at a bigger model later is a row, not a deploy. See
Analysis.
Provenance
Section titled “Provenance”A feed can add items it found and mint items it composed. Both are searchable; they are not the same claim.
origin |
Lives in | |
|---|---|---|
| synced | resource |
the resource it came from |
| minted by a feed | feed |
the feeds Resource::Database |
An item minted by a feed carries the feed that made it and the run that produced it. Items you own and items a model wrote about them stay distinguishable in search, in export, and to a person reading a list.
Nothing lets a feed rewrite a synced item’s origin.
Bounds
Section titled “Bounds”A feed is a Run of kind feed, so it inherits what every bulk operation already has: a status the
iteration re-reads through halted?, cancellation, gates, and the audit trail.
| turns | Feed::TURNS, hard cap; running out is a normal outcome |
| tools per turn | bounded by the grant, which the executor re-checks |
| run budget | URIS_RUN_BUDGET, the counter tools declaring starts_runs already use |
| one prompt row per turn | so the reasoning is auditable the way a summary is |
Every turn is a prompts row carrying the resource, role, model, request and response. A feed that
went wrong is readable afterwards rather than inferred.
Addressing
Section titled “Addressing”A logged-in request to a path nothing else claims resolves to a feed:
GET /buy -> feeds#show, the feed named buyGET /mcp -> the MCP endpoint, because the route is declared firstThe catch-all is declared last and the slug is validated against the names the application already
owns — mcp, graphql, graphiql, auth, enroll, references, jobs, up, settings,
resources, runs. A feed that could be named mcp would either shadow the endpoint or be
unreachable, and route order alone does not say which.