Registered runner¶
A registered runner is a long-lived daemon you run inside your own boundary (your network, with your tools / keys / KB). It connects out to the platform, pulls the few jobs that must execute live — starting with the agent playground — runs the real agent locally, and reports results back. The platform never runs your agent code or holds your secrets; it only commands the runner.
fastaiagent runner \
--connect https://app.fastaiagent.net \
--key $FASTAIAGENT_API_KEY \
--labels region=eu --labels gpu=false \
--max-concurrency 4
How it works¶
- Register with your API key (
X-API-Key) and advertisecapabilities:["live_playground","eval_run"](plustool_execwhen you pass--tools— see Connectors). The platform returns arunner_idand a short-livedrunner_token(kept in memory only — never written to disk). On startup the runner also callsconnect()with the same key, which wires the trace exporter so the jobs it runs push their traces to the platform. A rejected key fails fast with a clear error; an unreachable platform is tolerated (the runner still starts and traces buffer locally, draining when it returns). - Heartbeat every
ttl_seconds / 3, reportingstatusand the in-flight job count. - Long-poll for commands (≤30s). A
live_playgroundcommand carries the agent config (prompt, model, tool specs, KB refs) plus an input; aneval_runcommand carries the config plus a set of cases. Neither ever carries tools or keys. - Execute each job as its own asyncio task, bounded by
--max-concurrency, inside ajob_scopeso it binds your local tools / keys / KB. Each job's trace is pushed to the platform via the exporter wired at startup; the platform routes it to your API key's project (aneval_runruns the agent once per case and emits one trace per case). - Report the result (
completed/failed, with thetrace_idthat links to the pushed trace). Aneval_runreports the per-case outputs ({"outputs":[{case_id, output, trace_id}, …]}); the platform scores them.
All calls after register authenticate with Authorization: Bearer <runner_token>.
Verifying traces reach the platform¶
Every job a runner executes pushes its trace to the platform, routed by your API
key. The example examples/83_runner_trace_to_platform.py runs one
live_playground job exactly the way the daemon does, then reads the trace back
from the plane. A real run against a local plane on :20001:
============================================================
Runner trace -> platform (Task A)
============================================================
Connected: True domain=8ccd14b5-… project=ab4d5161-…
Executing live_playground 'demo-1' (real gpt-4o-mini)…
status=completed output='OK' trace_id=19c1f4083a0409d075d8d18d7a7c3871
Flushed exporter -> POST /public/v1/traces/ingest
Verifying on the platform (GET /public/v1/traces/{id})…
✓ trace 19c1f4083a0409d075d8d18d7a7c3871 source=sdk status=completed spans=[agent.demo, llm.openai.gpt-4o-mini]
============================================================
The trace_id the runner reports in POST /runners/{id}/results is the same
id the trace lands under (source=sdk), so the console links each job result to
its full trace — here the agent span and its LLM call. An eval_run emits one such
trace per case.
Resilience & shutdown¶
- Re-register automatically on auth loss (heartbeat miss /
401/404), minting a fresh token — no credential at rest. - Backoff (exponential, capped) on transient transport errors.
- Graceful shutdown on
Ctrl-C/SIGTERM: stop pulling new work, drain in-flight jobs, then send a finalstatus="stopping"heartbeat (the platform marks the runner offline and re-queues anything still in flight). There is no separate deregister endpoint.
Scope¶
The runner handles the live_playground, eval_run, and tool_exec
job types; guarded_live_rerun is the remaining fast-follow (it will gate on a
tool's replay_class and never
execute a side_effecting tool). For an eval_run the runner only executes
each case and returns the outputs — the platform applies the suite's scorers /
criteria centrally.
The runner runs concurrent jobs for one tenant per process; per-job state is
request-scoped via job_scope, so each job must
run in its own asyncio task (the daemon guarantees this). Because a runner is
single-tenant and the platform routes traces by your API key, all its job traces
land in that key's project — the local trace store is just a buffer.
Connectors (tool_exec)¶
When the platform can't reach a connector itself (a SaaS deployment whose target
is customer-private), it dispatches a tool_exec command and the runner runs
that connector locally, with your own creds. The command carries the connector
spec (instance_id, action, fixed_params), the exposed_name, and the
arguments — never a secret.
This is opt-in: register your local tools via --tools module:callable, which
also advertises the tool_exec capability. The runner resolves the dispatched
exposed_name in its ToolRegistry, runs it with {**arguments, **fixed_params},
and reports {"success": bool, "result": <output>} (the platform reads
result["success"]). The call is traced like any other job and linked by the
reported trace_id.
fastaiagent runner --connect https://app.fastaiagent.net --key $FASTAIAGENT_API_KEY \
--tools myconnectors:register
# myconnectors.py — runs in the runner's boundary, with your LOCAL creds
import os
from fastaiagent import FunctionTool
def register() -> None:
def lookup_account(account_id: str) -> dict:
token = os.environ["INTERNAL_API_TOKEN"] # a LOCAL secret, never shipped
... # call your customer-private service
return {"account_id": account_id, "status": "active"}
FunctionTool(name="lookup_account", fn=lookup_account) # exposed_name == "lookup_account"
A real tool_exec dispatched to the runner (here the connector makes a local
model call), recorded on the plane: