Skip to content

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.

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.

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) }
end

tool_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.

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, :vision
Feed::ROLE # :agent -> qwen3:8b

Because 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.

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.

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.

A logged-in request to a path nothing else claims resolves to a feed:

GET /buy -> feeds#show, the feed named buy
GET /mcp -> the MCP endpoint, because the route is declared first

The 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.