Get started
Connect your agent, wire a trusted outcome signal, turn on learning, then watch, pause or roll back. Or copy one prompt into Claude Code or Codex and let it do the setup.
Preview: evidence status
Plug-and-play learning is a preview. The whole loop runs on the local stack today: outcome receipts, learned candidates evaluated against the version you run, and pause, disable and rollback. Whether learning improves a given agent is not yet demonstrated. Read the evidence status before you rely on it.
ultrarouter sits between your agent and its models. You tell it whether each finished task succeeded, from a source you trust. It turns those outcomes into candidate adaptations, evaluates each candidate against the version you run today, deploys it only when it wins, and keeps learning from the new outcomes. That cycle is what "recursive" means here: outcome, adaptation, evaluation, deployment, new outcome. It changes the context ultrarouter serves your agent, never the model's weights.
Outcome
A source you trust says whether a finished task succeeded.
Adaptation
ultrarouter drafts a change to the context it serves your agent, from that evidence.
Evaluation
The candidate runs against the current version on live tasks, under a rule fixed in advance.
Deployment
Only a candidate that wins becomes active. Otherwise the current version stays.
New outcome
The adapted agent's results become the evidence for the next round.
The fast path: hand it to your coding agent
Pick your coding agent, your language and how you want to connect, then copy the prompt. It tells the agent to inspect your project, reuse its configuration, install the SDK from a local build, wire a trusted outcome, run one verified episode and confirm its receipt. It asks you only for sign-in, consent and keys, and it never contains a key.
Set it up with your coding agent
Copy the prompt into Claude Code or Codex. It walks your agent through the whole setup and asks you only for sign-in, consent and keys. It never contains a key.
Prompt for Claude Code: Add learning to an existing app
Recipe existing-app, revision 4, TypeScript
Set up ultrarouter learning in this project. You are Claude Code.
ultrarouter is an OpenAI-compatible gateway. The project keeps its agent; it reports a trusted outcome for each finished task, and ultrarouter turns those outcomes into candidate adaptations, evaluates each one against the version running today, deploys it only if it wins, and keeps learning from the new outcomes. Learning changes the context ultrarouter serves the agent, not the model's weights.
This setup
- Recipe: Add learning to an existing app (existing-app, revision 4, schema ur.setup-recipes/1). Machine-readable copy: http://localhost:3002/setup-recipes.json
- Project language: TypeScript. If the project keeps agent instructions in CLAUDE.md, add a short note there on where the gateway is configured and where outcomes are reported.
- Learning is not on the hosted service yet (the hosted gateway at https://api.ultrarouter.io is invite-only and serves routing and compression), so this setup uses the local stack: the gateway at http://localhost:8080 and the dashboard at http://localhost:3000.
- Learning is a preview. The loop runs on the local stack; if an outcome-receipt or learning endpoint returns 404, the checkout is older than it, so tell me and stop at that step. A working setup proves the plumbing, not that learning helps: http://localhost:3002/docs/evidence-status.md
Rules
- Never write a key into source files, tests, fixtures, logs, commit messages or your replies. Read it from UR_API_KEY in the environment. Before adding it to a .env file, confirm .env is git-ignored, and never print that file.
- Ask me only for what needs my identity, my consent or credentials you cannot get: signing up or in, email verification, two-factor enrolment, accepting terms, the workspace Learning opt-in (corrections and knowledge consent), provider keys, minting the ultrarouter API key, the path of my ultrarouter checkout, and a yes before you turn learning on or use its controls. Do everything else yourself. Never create an account, accept terms or change consent for me.
- Claude Code may ask me before it runs a shell command. When you request one that calls the gateway, say why it is needed.
- Reuse the project's existing configuration: read UR_BASE_URL and UR_API_KEY where the project already reads its settings, and keep its client, its retry and timeout settings, and its tests.
- Never claim that learning improved anything. Report what the receipt and the learning status say.
Steps
1. Inspect the project. Identify its language and package manager, the existing LLM client (the openai package, another OpenAI-compatible SDK, or raw HTTP calls) and its tests. Find where the client's base URL, key and model are configured, and how a finished task is judged today: that judgment is the outcome signal to reuse.
2. Read the current ultrarouter docs before you change anything:
- Get started: http://localhost:3002/docs/get-started.md
- Record outcomes: http://localhost:3002/docs/guides/outcomes.md
- Evidence status: http://localhost:3002/docs/evidence-status.md
- Set up with Claude Code or Codex: http://localhost:3002/docs/guides/agent-integrations.md
- Setup recipes (JSON): http://localhost:3002/setup-recipes.json
3. Check that the local gateway answers.
Learning is not on the hosted service yet (https://api.ultrarouter.io is invite-only and serves routing and compression), so this setup runs on the local stack from the ultrarouter repository: the gateway on http://localhost:8080 and the dashboard on http://localhost:3000. If the readiness check does not answer, the stack has to be started in the ultrarouter checkout, which the coding agent may not have access to.
Commands:
curl -sS http://localhost:8080/readyz
pnpm dev:e2e (in the ultrarouter checkout)
Expected output (illustrative; ids differ):
{"status":"ready","service":"gateway","checks":{…}}
4. Create or sign in to the account.
Sign up at http://localhost:3000/signup (or sign in at http://localhost:3000/login) and create the workspace. Email is not delivered on the local stack: the verification link is in the test outbox at http://localhost:8080/auth/test/outbox?to=<email>. Enrol two-factor authentication when asked; an owner needs it to mint keys and to change learning. As the workspace owner, choose Allow learning on the dashboard Learning page for the call type, or in the Learn onboarding flow. This workspace opt-in grants both corrections consent (store task content) and knowledge consent (use what it learns); corrections consent alone is insufficient. The equivalent owner-session request is PUT /v1/workspaces/<workspace id>/self-learning with {"enabled":true}; a bearer API key cannot grant these permissions. Under Providers & BYOK, add a key (Add key) for the provider whose models the agent and the learning reflection will use, or add a custom provider with its own key: learning only ever uses a model the workspace owns, never a platform key. A new workspace starts on prepaid UR credits, which never pay the workspace's own keys, so choose BYOK funding once from the signed-in session: POST /v1/workspaces/<workspace id>/billing/funding with {"mode":"byok","allowPlatformFallback":false,"requestKey":"<16 to 128 letters, digits, - or _>"}. Until then a request only your own key can serve is refused with HTTP 403 permission. Then mint an API key in Settings, API keys, with the gateway:invoke, outcomes:write and config:read scopes; add config:write only if the agent should turn learning on, pause it or roll it back through the API.
This step is mine: ask me to do it, then wait until I confirm it is done.
Docs: http://localhost:3000/signup | http://localhost:3000/login | Provider keys: http://localhost:3002/docs/guides/providers-byok.md | Security and data handling: http://localhost:3002/docs/security.md
5. Put the endpoint and the key in the environment.
read -rs reads the key without echoing it, so it stays out of the terminal and the shell history. Claude Code passes the environment it was started from to its shell. Export UR_BASE_URL and UR_API_KEY before starting it; if it is already running, restart it with the resume command so this conversation continues. If the project loads a .env file, add UR_API_KEY there only after confirming .env is git-ignored. To use the management MCP, export ULTRAROUTER_API_KEY (a separate key) the same way before starting the agent. The last command checks the key is visible without printing it.
This step is mine: ask me to do it, then wait until I confirm it is done.
Commands:
export UR_BASE_URL=http://localhost:8080
read -rs UR_API_KEY && export UR_API_KEY
claude --continue
test -n "$UR_API_KEY" && echo "UR_API_KEY is set"
Expected output (illustrative; ids differ):
UR_API_KEY is set
Docs: Set up with Claude Code or Codex: http://localhost:3002/docs/guides/agent-integrations.md
6. Install the TypeScript SDK from a local build (optional).
Only when the project has no LLM client yet, or I ask for the native SDK: the existing OpenAI-compatible client is enough for the connection. @ultrarouter/sdk is not on the npm registry, so never install it from there. Build it from the feat/learning-loop branch of the public source repository, cloned next to this project. Install the tarball with the project's package manager (pnpm add and yarn add take the same path). Version 0.1.0 includes the learning and outcome-receipt helpers (see the SDK guide); the commands in this recipe use the same REST calls, so they also work without them.
Commands:
git clone --branch feat/learning-loop --single-branch https://github.com/lucastonon/ultrarouter-sdk-typescript ../ultrarouter-sdk-typescript
npm ci && npm run build && npm pack (in ../ultrarouter-sdk-typescript)
npm install ../ultrarouter-sdk-typescript/ultrarouter-sdk-0.1.0.tgz
Expected output (illustrative; ids differ):
ultrarouter-sdk-0.1.0.tgz (the last line npm pack prints)
Docs: https://github.com/lucastonon/ultrarouter-sdk-typescript | TypeScript SDK: http://localhost:3002/docs/guides/sdk-typescript.md
7. Point the existing client at the gateway.
Keep the project's OpenAI-compatible client and change only its configuration: the base URL becomes "$UR_BASE_URL/v1", the key comes from UR_API_KEY, and an X-UR-Call-Type header names this agent, because learning is per call type. If one task makes several model calls, send the same X-UR-Artifact header (the task or run id) on each of them. Keep the x-ur-request-id response header of the task's last call: the outcome is reported on it. With the openai package, .withResponse() exposes the headers.
your existing client module:
```ts
import OpenAI from "openai";
// Only the configuration changes: base URL, key and the call-type header.
const client = new OpenAI({
baseURL: `${process.env.UR_BASE_URL}/v1`,
apiKey: process.env.UR_API_KEY,
defaultHeaders: { "X-UR-Call-Type": "my-agent" },
});
// Every model call of one task carries the same task id; keep the last request id.
const { data: completion, response } = await client.chat.completions
.create({ model, messages }, { headers: { "X-UR-Artifact": taskId } })
.withResponse();
const lastRequestId = response.headers.get("x-ur-request-id");
// When the task is finished, report your verifier's verdict once.
await fetch(`${process.env.UR_BASE_URL}/v1/requests/${lastRequestId}/outcomes`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.UR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
channel: "computable",
kind: passed ? "oracle_pass" : "oracle_fail",
source_event_id: taskId,
}),
});
```
Docs: Quickstart: http://localhost:3002/docs/quickstart.md
8. Report a trusted outcome for each finished task.
Use the most trustworthy signal the project already has that a finished task succeeded, and report it once per task on the task's last request id. A verifier (tests, a schema or exact-match check) or a business rule sends channel computable with kind oracle_pass or oracle_fail. A delayed webhook sends channel resolution with kind resolved_true or resolved_false. A human review sends channel correction with kind accepted_unchanged or rejected. Self-rating (the agent or a model grading its own answer, or a thumbs-up) is weaker: it is recorded as a proxy and never teaches. An HTTP 200 or a tool call that returned is not task success. If the outcome is unknown, send nothing. A retry reuses the same source_event_id, so it never counts twice.
Commands:
curl -sS -X POST "$UR_BASE_URL/v1/requests/$REQUEST_ID/outcomes" -H "Authorization: Bearer $UR_API_KEY" -H "Content-Type: application/json" -d '{"channel":"computable","kind":"oracle_pass","source_event_id":"<task id>"}'
Expected output (illustrative; ids differ):
HTTP 202 with "status" accepted, duplicate or pending, and a "receipt" (ocr_…) that stays pending until the gateway has recorded the request, then becomes applied.
Docs: Record outcomes: http://localhost:3002/docs/guides/outcomes.md
9. Install the ultrarouter MCP servers (optional).
Neither MCP server is on a package registry yet. In the ultrarouter checkout, build and pack the management MCP and install that tarball, build the documentation MCP, then let the installer register both for this agent; it keeps every other host setting and never writes a key. The management server reads ULTRAROUTER_API_KEY (a separate key with config:read; config:write only if the agent may pause, disable or roll back learning) and ULTRAROUTER_BASE_URL from the environment the agent was started in. It reads learning status, evaluations and outcome receipts and can pause, disable or roll back; it cannot report an outcome, so it can never mark a task as succeeded, and it does not route this agent's own traffic through ultrarouter.
Ask me before you run it.
Commands:
pnpm --filter @ultrarouter/mcp build (in the ultrarouter checkout)
(cd packages/mcp && pnpm pack && npm install -g ./ultrarouter-mcp-0.1.0.tgz) (in the ultrarouter checkout)
pnpm --filter "@ultrarouter/docs-mcp..." build (in the ultrarouter checkout)
ultrarouter-mcp install --host claude --with-docs --docs-repo "$PWD" (in the ultrarouter checkout)
claude mcp get ultrarouter
ultrarouter-mcp check
Expected output (illustrative; ids differ):
Connected to ultrarouter at http://localhost:8080.
Workspace id: … (from ultrarouter-mcp check)
Docs: MCP servers for Claude Code and Codex: http://localhost:3002/docs/guides/mcp-servers.md
10. Run one verified episode.
Add or extend a test or script that runs one real task end to end through the gateway, applies the real verifier and reports the outcome. Run it with the project's own test command and keep its request id.
11. Confirm the outcome receipt and the learning state.
The outcome's receipt (ocr_…) must reach applied. Pending is normal until the gateway has recorded the request; rejected, failed or expired carries a reason to fix. The learning routes take the call type's id (ct_…), not its slug: the gateway created the call type on the first request that named it, so look the id up by its slug my-agent (with jq, or read it from the JSON) and use it as UR_CALL_TYPE_ID below. Then read the learning state and report it as returned; it is off until learning is turned on.
Commands:
curl -sS "$UR_BASE_URL/v1/requests/$REQUEST_ID/outcome-receipts" -H "Authorization: Bearer $UR_API_KEY"
curl -sS "$UR_BASE_URL/v1/call-types" -H "Authorization: Bearer $UR_API_KEY" | jq -r '.data[] | select(.slug == "my-agent") | .id'
curl -sS "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning" -H "Authorization: Bearer $UR_API_KEY"
Expected output (illustrative; ids differ):
{"object":"list","data":[{"object":"outcome_receipt","id":"ocr_…","status":"applied",…}]}
ct_…
{"object":"learning","call_type_id":"ct_…","state":"off",…}
Docs: Evidence status: http://localhost:3002/docs/evidence-status.md
12. Turn on learning for the call type.
Learning stays off until it is turned on for the call type, by its id. Three things must hold first, and the route names the one that is missing. Both corrections consent and knowledge consent are on through the workspace Allow learning opt-in (409 corrections_consent_required if either is missing; the owner uses the dashboard Learning page or PUT /v1/workspaces/<workspace id>/self-learning with {"enabled":true} from a signed-in owner session, never a bearer API key). reflection_model is a model the workspace owns, through a provider key it stored or one of its custom providers (400 reflection_model_not_workspace_owned with the reason; there is no platform or paid fallback, and a workspace on prepaid billing must fund with its own keys, byok). The caller is an owner or admin, or a key with config:write. With promotion mode "evaluate", the default, a learned candidate is evaluated before replacing the active version. During the comparison it serves only its randomly assigned share of eligible episodes; negative or inconclusive results keep the current active version. Afterwards, reflection.provider in the learning status says whether the reflection model is usable and, if not, why.
Ask me before you run it.
Commands:
curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/enable" -H "Authorization: Bearer $UR_API_KEY" -H "Content-Type: application/json" -d '{"reflection_model":"<a model the workspace owns>","promotion":{"mode":"evaluate"}}'
Expected output (illustrative; ids differ):
{"object":"learning_control","action":"enable","changed":true,…,"learning":{"state":"collecting",…}}
Docs: Provider keys: http://localhost:3002/docs/guides/providers-byok.md
13. Pause, disable or roll back.
These are the controls for later and the core of the rollback instructions; do not run them during setup. Pause stops reflecting and evaluating but keeps serving the active version; resume undoes it. Disable stops serving learned lessons and stops a running evaluation. Rollback points the call type at an earlier version from the version history (version_id, epb_…; omitted, the version that was active before the current one). Every control is audited and needs an owner or admin, or a key with config:write.
Ask me before you run it.
Commands:
curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/pause" -H "Authorization: Bearer $UR_API_KEY"
curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/resume" -H "Authorization: Bearer $UR_API_KEY"
curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/disable" -H "Authorization: Bearer $UR_API_KEY"
curl -sS "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/versions" -H "Authorization: Bearer $UR_API_KEY"
curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/rollback" -H "Authorization: Bearer $UR_API_KEY" -H "Content-Type: application/json" -d '{"version_id":"<an earlier version id>"}'
14. Summarize. List every file you changed and why, the commands you ran with their results, the request id, the receipt status and the learning state. Then give exact rollback instructions: revert those files, point the client back at its previous base URL, disable learning or roll back to an earlier version with the calls above, remove the MCP servers with ultrarouter-mcp uninstall --host claude --with-docs (or the plugin with claude plugin uninstall ultrarouter@ultrarouter-local) if you added them, and remind me to revoke the API keys in the dashboard Settings.
The change to your existing client
The lines to change in an existing project. model, messages, taskId and passed come from your own code; passed is your verifier, not a model grading itself.
import OpenAI from "openai";
// Only the configuration changes: base URL, key and the call-type header.
const client = new OpenAI({
baseURL: `${process.env.UR_BASE_URL}/v1`,
apiKey: process.env.UR_API_KEY,
defaultHeaders: { "X-UR-Call-Type": "my-agent" },
});
// Every model call of one task carries the same task id; keep the last request id.
const { data: completion, response } = await client.chat.completions
.create({ model, messages }, { headers: { "X-UR-Artifact": taskId } })
.withResponse();
const lastRequestId = response.headers.get("x-ur-request-id");
// When the task is finished, report your verifier's verdict once.
await fetch(`${process.env.UR_BASE_URL}/v1/requests/${lastRequestId}/outcomes`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.UR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
channel: "computable",
kind: passed ? "oracle_pass" : "oracle_fail",
source_event_id: taskId,
}),
});
Every step the prompt walks through
- Check that the local gateway answers
curl -sS http://localhost:8080/readyzpnpm dev:e2e
- Create or sign in to the account (you do this one)
- Put the endpoint and the key in the environment (you do this one)
export UR_BASE_URL=http://localhost:8080read -rs UR_API_KEY && export UR_API_KEYclaude --continuetest -n "$UR_API_KEY" && echo "UR_API_KEY is set"
- Install the TypeScript SDK from a local build (optional)
git clone --branch feat/learning-loop --single-branch https://github.com/lucastonon/ultrarouter-sdk-typescript ../ultrarouter-sdk-typescriptnpm ci && npm run build && npm packnpm install ../ultrarouter-sdk-typescript/ultrarouter-sdk-0.1.0.tgz
- Point the existing client at the gateway
- Report a trusted outcome for each finished task
curl -sS -X POST "$UR_BASE_URL/v1/requests/$REQUEST_ID/outcomes" -H "Authorization: Bearer $UR_API_KEY" -H "Content-Type: application/json" -d '{"channel":"computable","kind":"oracle_pass","source_event_id":"<task id>"}'
- Install the ultrarouter MCP servers (optional)
pnpm --filter @ultrarouter/mcp build(cd packages/mcp && pnpm pack && npm install -g ./ultrarouter-mcp-0.1.0.tgz)pnpm --filter "@ultrarouter/docs-mcp..." buildultrarouter-mcp install --host claude --with-docs --docs-repo "$PWD"claude mcp get ultrarouterultrarouter-mcp check
- Run one verified episode
- Confirm the outcome receipt and the learning state
curl -sS "$UR_BASE_URL/v1/requests/$REQUEST_ID/outcome-receipts" -H "Authorization: Bearer $UR_API_KEY"curl -sS "$UR_BASE_URL/v1/call-types" -H "Authorization: Bearer $UR_API_KEY" | jq -r '.data[] | select(.slug == "my-agent") | .id'curl -sS "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning" -H "Authorization: Bearer $UR_API_KEY"
- Turn on learning for the call type
curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/enable" -H "Authorization: Bearer $UR_API_KEY" -H "Content-Type: application/json" -d '{"reflection_model":"<a model the workspace owns>","promotion":{"mode":"evaluate"}}'
- Pause, disable or roll back
curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/pause" -H "Authorization: Bearer $UR_API_KEY"curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/resume" -H "Authorization: Bearer $UR_API_KEY"curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/disable" -H "Authorization: Bearer $UR_API_KEY"curl -sS "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/versions" -H "Authorization: Bearer $UR_API_KEY"curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/rollback" -H "Authorization: Bearer $UR_API_KEY" -H "Content-Type: application/json" -d '{"version_id":"<an earlier version id>"}'
The prompts are generated from versioned setup recipes. Agents can read every recipe, with exact
commands, environment variable names and expected output, at /setup-recipes.json
(schema ur.setup-recipes/1). The differences between Claude Code and Codex are on
Set up with Claude Code or Codex, and the MCP servers and plugins
on MCP servers for Claude Code and Codex.
Before you start
- The local stack. Learning is not on the hosted service yet (the hosted gateway at
https://api.ultrarouter.iois invite-only and serves routing and compression), so this guide runs on the local stack. In your checkout of the ultrarouter repository,pnpm dev:e2estarts the gateway onhttp://localhost:8080and the dashboard onhttp://localhost:3000.curl -sS http://localhost:8080/readyzanswers"status":"ready"when it is up. - Your account. Sign up at
http://localhost:3000/signupand create a workspace. Email is not delivered on the local stack: the verification link is in the test outbox athttp://localhost:8080/auth/test/outbox?to=<email>. Enrol two-factor authentication when asked; an owner needs it to mint keys and to change learning. - Workspace Learning opt-in. As the owner, choose Allow learning on the call type's dashboard
Learning page or in the Learn onboarding flow. It grants both corrections consent to store
the task and the agent's steps and knowledge consent to use what it learns; corrections consent
alone is insufficient. The equivalent request is
PUT /v1/workspaces/{id}/self-learningwith{"enabled":true}from a signed-in owner session, not a bearer API key. Revoking corrections consent stops learning and deletes what was stored. - A model your workspace owns. Under Providers & BYOK, choose Add key for the provider whose models your agent and the learning reflection will use, or add a custom provider with its own key (provider keys). Learning only ever calls a model the workspace owns, never a platform key, and there is no paid fallback.
- BYOK funding. A new workspace starts on prepaid UR credits, which never pay your own keys. Choose
BYOK funding once from your signed-in session with
POST /v1/workspaces/<workspace id>/billing/fundingand{"mode":"byok","allowPlatformFallback":false,"requestKey":"<16 to 128 letters, digits, - or _>"}. Until then a request only your own key can serve is refused with HTTP 403permission. - An API key. In Settings, under API keys, mint a key with the
gateway:invoke,outcomes:writeandconfig:readscopes. Addconfig:writeonly if a script or your coding agent should turn learning on, pause it or roll it back.
Put the gateway URL and the key in the environment of the shell that runs your app and your coding
agent, and never in source control. read -rs keeps the key out of your terminal and shell history:
export UR_BASE_URL="http://localhost:8080"
read -rs UR_API_KEY && export UR_API_KEYStart your coding agent after this, so it inherits both. Codex removes variables whose names contain
KEY, SECRET or TOKEN from the commands it runs, so start it with this override (it lets every
such variable through, so start it from a shell that holds only the secrets this project needs):
codex -c shell_environment_policy.ignore_default_excludes=trueConnect your agent
Learning is per call type, so name your agent's traffic: send an X-UR-Call-Type header (or a
call_type field) on every request. The gateway creates the call type the first time a request names
it. If one task takes several model calls, send the same X-UR-Artifact header (your task or run id)
on each, so they form one episode. Every response carries an x-ur-request-id header; keep the one
from the task's last call, because the outcome is reported on it.
With your existing OpenAI client, change only its configuration:
import OpenAI from "openai";
const client = new OpenAI({
baseURL: `${process.env.UR_BASE_URL}/v1`,
apiKey: process.env.UR_API_KEY,
defaultHeaders: { "X-UR-Call-Type": "my-agent" },
});
// Every model call of one task carries the same task id; keep the last request id.
const { data: completion, response } = await client.chat.completions
.create({ model, messages }, { headers: { "X-UR-Artifact": taskId } })
.withResponse();
const lastRequestId = response.headers.get("x-ur-request-id");With your existing OpenAI client, change only its configuration:
import os
from openai import OpenAI
client = OpenAI(
base_url=f"{os.environ['UR_BASE_URL']}/v1",
api_key=os.environ["UR_API_KEY"],
default_headers={"X-UR-Call-Type": "my-agent"},
)
# Every model call of one task carries the same task id; keep the last request id.
raw = client.chat.completions.with_raw_response.create(
model=model, messages=messages, extra_headers={"X-UR-Artifact": task_id}
)
last_request_id = raw.headers.get("x-ur-request-id")
completion = raw.parse()Neither SDK is on a package registry. Clone the public feat/learning-loop branch to get the
learning preview (0.1.0), build it, and install the resulting tarball or wheel. The default main
branch does not carry this learning preview. Exact file names are in
/setup-recipes.json.
# TypeScript: clone the learning branch and build its tarball, then install the file
git clone --branch feat/learning-loop --single-branch https://github.com/lucastonon/ultrarouter-sdk-typescript ../ultrarouter-sdk-typescript
(cd ../ultrarouter-sdk-typescript && npm ci && npm run build && npm pack)
npm install ../ultrarouter-sdk-typescript/ultrarouter-sdk-0.1.0.tgz
# Python: clone the learning branch and build its wheel, then install the file
git clone --branch feat/learning-loop --single-branch https://github.com/lucastonon/ultrarouter-sdk-python ../ultrarouter-sdk-python
(cd ../ultrarouter-sdk-python && uv build)
pip install ../ultrarouter-sdk-python/dist/ultrarouter-0.1.0-py3-none-any.whlThe SDK reads UR_BASE_URL and UR_API_KEY and takes call_type on every request. See the
TypeScript SDK and Python SDK guides. The
copy-to-agent prompt above has a complete runnable example for each language.
Wire a trusted outcome signal
When a task is finished, report once whether it succeeded, on the task's last request id. The source matters more than anything else you set up:
| Your source | What to send (channel / kind) | Teaches? |
|---|---|---|
| A verifier: tests, a schema or exact-match check | computable / oracle_pass or oracle_fail | Yes |
| A business rule your code checks | computable / oracle_pass or oracle_fail | Yes |
| A delayed webhook (a ticket resolved, a payment cleared) | resolution / resolved_true or resolved_false | Yes |
| A human review | correction / accepted_unchanged or rejected | Yes |
| Self-rating: the agent or a model grading its own answer, or a thumbs-up | proxy / thumbs | Never |
| You do not know | send nothing | — |
Self-rating is a weaker signal. It is recorded and can help rank, but it never teaches. An HTTP 200, or a tool call that returned, is not task success. Missing feedback stays unknown.
curl -sS -X POST "$UR_BASE_URL/v1/requests/$REQUEST_ID/outcomes" \
-H "Authorization: Bearer $UR_API_KEY" -H "Content-Type: application/json" \
-d '{"channel":"computable","kind":"oracle_pass","source_event_id":"<task id>"}'The answer (HTTP 202) has a status and a durable receipt:
accepted: recorded now.duplicate: a retry of an outcome already recorded, so nothing changes.pending: the gateway has not recorded the request yet, which is normal right after the response. The outcome is stored and applied when the request lands; the receipt tells you when.
source_event_id makes the report idempotent: a retry with the same id never counts twice (the same id
with a different body is a 409). A delayed outcome works the same way, days later, on the stored
request id. More recording modes (batch, warehouse backfill, the durable queue) are in
Record outcomes.
Check the receipt (ocr_…) by its id, or every receipt of a request:
curl -sS "$UR_BASE_URL/v1/outcome-receipts/$RECEIPT_ID" -H "Authorization: Bearer $UR_API_KEY"
curl -sS "$UR_BASE_URL/v1/requests/$REQUEST_ID/outcome-receipts" -H "Authorization: Bearer $UR_API_KEY"A receipt moves from pending to applied. rejected, failed and expired are final and carry a
reason; expired with request_not_found means no request with that id appeared in your workspace
within 15 minutes. Its episode says whether the outcome reached the episode it belongs to.
Turn on learning
Learning is off until you turn it on for a call type, and the learning routes take the call type's
id (ct_…), not its slug. Look the id up once the first request has created the call type:
curl -sS "$UR_BASE_URL/v1/call-types" -H "Authorization: Bearer $UR_API_KEY" \
| jq -r '.data[] | select(.slug == "my-agent") | .id'
export UR_CALL_TYPE_ID="<the ct_ id printed above>"Then turn it on, with a key that has config:write (or as an owner or admin):
curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/enable" \
-H "Authorization: Bearer $UR_API_KEY" -H "Content-Type: application/json" \
-d '{"reflection_model":"<a model the workspace owns>","promotion":{"mode":"evaluate"}}'The route checks what Before you start set up and names what is missing:
| Answer | Cause | Fix |
|---|---|---|
409 corrections_consent_required | Corrections or knowledge consent is off. | The owner chooses Allow learning in the dashboard or uses the session-authenticated PUT /v1/workspaces/{id}/self-learning with {"enabled":true}. |
400 reflection_model_not_workspace_owned | The model is not served by a key your workspace stored or by one of its custom providers (or prepaid billing funds with credits). | Add a key under Providers & BYOK, or pick a model one of your keys serves. |
403 | The key lacks config:write, or you are not an owner or admin. | Use an owner or admin session, or a key with config:write. |
404 | No call type with that id in your workspace. | Use the ct_… id from GET /v1/call-types. |
reflection_model is required. promotion, lessons_mode, lessons_k and recipe are optional. With
the promotion mode evaluate, the default, a learned candidate is never activated just because it was
produced. Each episode is assigned the candidate or the current version, and the candidate becomes
active only if it wins under a rule fixed before the evaluation starts. A negative or inconclusive
result keeps the current version.
Watch, pause or roll back
Read the status. state is off until learning is on, then collecting (waiting for evidence),
learning (a reflection is due or an evaluation runs), paused, disabled, or unavailable,
quota_exhausted or budget_exhausted with a reason. reflection.provider says whether the
reflection model is usable and, if not, why. evidence separates episodes that arrived, were verified,
and were used by the active version, plus the backlog; outcome_receipts counts receipts by state.
curl -sS "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning" -H "Authorization: Bearer $UR_API_KEY"
curl -sS "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/evaluations" -H "Authorization: Bearer $UR_API_KEY"
curl -sS "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/versions" -H "Authorization: Bearer $UR_API_KEY"Each evaluation shows the rule it was judged by, the two arms, the interval and the decision. An inconclusive evaluation is the system working, not a failure.
Every control is audited and needs config:write (or an owner or admin):
# Pause: stop reflecting and evaluating, keep serving the active version (resume undoes it)
curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/pause" -H "Authorization: Bearer $UR_API_KEY"
curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/resume" -H "Authorization: Bearer $UR_API_KEY"
# Disable: stop serving learned lessons (a running evaluation stops too)
curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/disable" -H "Authorization: Bearer $UR_API_KEY"
# Roll back: point the call type at an earlier version (omit version_id for the previous active one)
curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/rollback" \
-H "Authorization: Bearer $UR_API_KEY" -H "Content-Type: application/json" \
-d '{"version_id":"<an earlier version id>"}'Remove it entirely
- Disable learning for the call type (above).
- Point your client back at its previous base URL and remove the outcome report.
- If you added the MCP servers:
ultrarouter-mcp uninstall --host claude --with-docsorultrarouter-mcp uninstall --host codex --with-docs(the plugins have their own uninstall, on MCP servers for Claude Code and Codex). - Revoke the API key in the dashboard Settings. Revoking corrections consent deletes the stored episodes.
What learning does not do
- It never changes model weights. It adapts the context ultrarouter serves your agent.
- It never learns from self-rating, a click or the gateway's own signals; only the outcomes you report from a trusted source teach.
- It never shares what it learned with another workspace.
- It never promises an improvement. See the evidence status for what is and is not demonstrated.
Overview
Frontier self-improvement for your AI Agent — we build and manage your platform's data moat with a plug-and-play continuous learning agent.
Evidence status
What exists, what is verified end to end, and what is demonstrated about learning, kept apart. Learning is a preview; improvement for your agent is not yet demonstrated.