Engineering Platform › Shared module · See it on the architecture map

AI Tool Loop (agentkit)

A queued, tool-using AI loop - a worker claims a job, asks to run one of a fixed list of tools, gets a result back, and keeps going until it has an answer. Originally built just for Tourney's own AI feature, then pulled out into its own portable package so a second app could reuse the real, hardened logic instead of rebuilding it from a written description. Nothing in the package imports the app around it - copying the folder in is the entire integration.

Why it matters
An AI that can run tools is only as safe as the thing deciding which tools it may run. This package makes that decision the application's, never the model's - and records every refusal.
What I designed
A queue, a tool registry and a trust boundary between an app and a worker it doesn't fully control, built once for Tourney and extracted so GetSeen could reuse the hardened logic instead of a description of it.
Used by
Tourney and GetSeen.
2 real callers
Tourney's own AI feature, and GetSeen - a direct port of the same pattern, not a rewrite from scratch
Two independent checks
a tool call must carry the worker_id that actually claimed the job, AND the tool must be registered for that job's own type - either one failing refuses it
The model never touches your data
it can only ask for a named tool to run; the app decides whether to allow it and runs it itself
Every refusal is recorded
a rejected tool call is written to the same permanent event log as a successful one, not silently dropped

What it actually does, in plain terms

An app creates a job and drops it in a queue. A worker - a separate process, possibly on hardware the app doesn't fully control - checks in, claims the job, and starts talking to a model. When the model wants information it doesn't already have, the worker sends back a tool_call: the name of a specific action and its arguments, never a request to just "go look something up" in the open. The app checks whether that call is actually allowed, runs it if so, and sends the result back as a tool_result. That exchange repeats until the model is ready to answer, and only the app ever writes the tool_result - a worker cannot post one for itself, which is what makes the event log a record of what actually happened rather than what a worker claims happened.

app                          agentkit                     model host
---                          --------                     ----------
creates a job  ------------> queue
                                   claim
executes tools <-- tool_call -- worker loop --prompt--> model
   (app owns the data)                          <--calls--
                          \--- tool_result -->

The rule that must not be weakened

Authorization comes from the job's own recorded type - never from the job's input, and never from the list of tools that got sent to the worker. That list is advisory: it tells the worker which tools to offer the model, and it grants nothing. A worker that edits or forges it gains nothing either way - an unregistered tool name is refused outright, and a registered one still runs under the app's own handler and the app's own argument schema, regardless of what the worker asked for.

Two checks run before any tool executes, in a fixed order, and neither one substitutes for the other:

A worker's own identity is checked but not treated as a secret - it says which worker is asking, it doesn't prove the request is legitimate. That proof comes from a separate bearer token, checked before any of this logic even runs.

The limits that stop a runaway job

Every one of these fails toward "stop the job," never toward "let it keep going and hope":

One tool call's arguments8 KB, or refused
One tool's result32 KB, then truncated and logged as such
Tool calls in one job20, then refused
One tool handler's real running time10 seconds, then it's treated as failed

A separate watchdog expires a job that stops checking in at all, so a worker that crashes mid-loop doesn't leave a job stuck "running" forever with nothing watching it.

Where it runs, and how it differs from Spark Worker Kit

Tourney's own AI feature runs on this. GetSeen's does too, now - a real second caller, not a hypothetical one, which is the actual proof a "reusable" package is reusable rather than just generalized-sounding. It's easy to conflate this with AI Worker Boundary, since both trace back to the same original Tourney work: Spark Worker Kit is the transport that talks to FOMX.ai's own GPU hardware - the polling, the claim logic, the usage tracking for that one specific machine. agentkit is one layer up from that: the job queue and the tool-calling security boundary itself, independent of which model or which hardware actually answers the prompt.