Release an agent version
This guide pins an agent version where you choose, checks which version a run gets, unpins, and gates a pin behind your test cases.
A run that names no version gets the agent's Latest in its project: the version the newest deployment into the project shipped, or the one a person published there in full, whichever came last. A version derived from an edit to its prompts or settings isn't Latest by itself, and neither is a higher number. Once a new version has done better on your test cases, as Try a version on the test cases shows, you can pin it where you choose: the whole tenant, an org, a project, or a segment of a project's runs, such as one company's. Runs there use it from the next run, nothing changes anywhere else, and you can unpin at once. Pinned versions reference has every rule, refusal and console page.
Before you start
Section titled “Before you start”- An agent with more than one version, registered on a runtime such as
kindgi dev, with Kindgi 0.2's CLI. This guide usesacme.order-desk, with versions 1.0.0, 1.1.0, 1.2.0 and 1.3.0. - A project's id, for a project or a segment:
kindgi projects listlists them. - For a release gate: a run of the version on your test cases, and a second reviewer to approve.
- For the app's half: Node 22.12 or later, or Python 3.11 or later, with
Kindgi 0.2's client, and
KINDGI_API_URLandKINDGI_API_TOKENset.
Pin a version
Section titled “Pin a version”A pinned version is what a run that names no version gets.
Pin 1.0.0 for the whole tenant:
pnpm exec kindgi agents pin acme.order-desk 1.0.0 --tenant --reason="1.0.0 is the version we checked"uv run kindgi agents pin acme.order-desk 1.0.0 --tenant --reason="1.0.0 is the version we checked"./kindgiw agents pin acme.order-desk 1.0.0 --tenant --reason="1.0.0 is the version we checked"./kindgiw agents pin acme.order-desk 1.0.0 --tenant --reason="1.0.0 is the version we checked"acme.order-desk 1.0.0is the agent and the version.--tenantis the scope: the whole tenant.--reasonis kept on the record.
It prints the pin request:
{ "id": "ea4e7c15-7990-41b6-b153-0847a83f2e9d", "agentId": "acme.order-desk", "scope": { "kind": "tenant" }, "action": "pin", "fromVersion": null, "toVersion": "1.0.0", "requestedBy": { "kind": "user", "id": "5c0a7e11-0000-4000-8000-0000000000aa" }, "reason": "1.0.0 is the version we checked", "createdAt": "2026-10-06T18:46:15.125Z"}From the next run, a run that names no version gets 1.0.0, and says why:
pnpm exec kindgi runs start --agent=acme.order-desk --input='{"userMessage":"Where is order A-1002?"}'uv run kindgi runs start --agent=acme.order-desk --input='{"userMessage":"Where is order A-1002?"}'./kindgiw runs start --agent=acme.order-desk --input='{"userMessage":"Where is order A-1002?"}'./kindgiw runs start --agent=acme.order-desk --input='{"userMessage":"Where is order A-1002?"}' "agent": { "id": "acme.order-desk", "version": "1.0.0", "conversationId": "4af092ea-5e8e-42f1-832e-b7ba1ed6e3d8", "via": "pinned", "pinnedScope": { "kind": "tenant" } },via: "pinned" says it got the pinned version of the scope in pinnedScope.
How a run gets its version
lists every via. A version is only pinned this way: publishing a newer
version doesn't change what a scope with a pinned version runs.
For an agency, with a project for each client, pin in the client's
project instead, with --project=<project-id>, as in
Pin it for some runs only but without
--segment. A tenant pin changes the agent at once for every client without
a pin of its own. When a release gate holds a pin for an
approval, the approval is in the pin's project: a tenant or org pin's is in
no project, so people who review only their project's approvals don't
see it.
Pin it for some runs only
Section titled “Pin it for some runs only”A narrower scope tries a version with some runs first. The most specific
pinned version wins: a segment path in a project, then the project, the
project's org, the tenant, and then the project's Latest. A segment
path is one or more key:value steps, coarse to fine, that a run is started
with: company:acme, or company:acme then role:counsel.
Try 1.1.0 with acme's runs only:
pnpm exec kindgi agents pin acme.order-desk 1.1.0 --project=<project-id> --segment=company:acme --reason="Trying 1.1.0 with acme first"uv run kindgi agents pin acme.order-desk 1.1.0 --project=<project-id> --segment=company:acme --reason="Trying 1.1.0 with acme first"./kindgiw agents pin acme.order-desk 1.1.0 --project=<project-id> --segment=company:acme --reason="Trying 1.1.0 with acme first"./kindgiw agents pin acme.order-desk 1.1.0 --project=<project-id> --segment=company:acme --reason="Trying 1.1.0 with acme first"--project=<project-id>is the project, where<project-id>is its id.--segment=company:acmeis the segment path in it.--org=<org-id>names an org instead.
It prints the pin request, with the segment as its scope.
A run started with that segment gets 1.1.0:
pnpm exec kindgi runs start --agent=acme.order-desk --project=<project-id> --segment=company:acme --input='{"userMessage":"Where is order A-1002?"}'uv run kindgi runs start --agent=acme.order-desk --project=<project-id> --segment=company:acme --input='{"userMessage":"Where is order A-1002?"}'./kindgiw runs start --agent=acme.order-desk --project=<project-id> --segment=company:acme --input='{"userMessage":"Where is order A-1002?"}'./kindgiw runs start --agent=acme.order-desk --project=<project-id> --segment=company:acme --input='{"userMessage":"Where is order A-1002?"}' "agent": { "id": "acme.order-desk", "version": "1.1.0", "conversationId": "d35be284-99c0-4437-9186-b951386db8b3", "via": "pinned", "pinnedScope": { "kind": "segment", "projectId": "367e1657-8067-4a1d-8ec4-bfa3c5fc5865", "path": [{ "key": "company", "value": "acme" }] } },A run with --segment=company:globex has no pinned version of its own, so it
gets the tenant's, 1.0.0. Without --project, a run is in its agent's own
project, which is the default project for what kindgi deploy registers.
See which version a run would get
Section titled “See which version a run would get”kindgi agents pinned answers without starting a run:
pnpm exec kindgi agents pinned acme.order-desk --project=<project-id> --segment=company:acmeuv run kindgi agents pinned acme.order-desk --project=<project-id> --segment=company:acme./kindgiw agents pinned acme.order-desk --project=<project-id> --segment=company:acme./kindgiw agents pinned acme.order-desk --project=<project-id> --segment=company:acme{ "agentId": "acme.order-desk", "version": "1.1.0", "via": "pinned", "pinnedScope": { "kind": "segment", "projectId": "367e1657-8067-4a1d-8ec4-bfa3c5fc5865", "path": [{ "key": "company", "value": "acme" }] }}kindgi agents pinned-versions acme.order-desk lists every scope with a pinned
version, and since when.
From your app
Section titled “From your app”Your app can do the same. Ask your coding agent: "I want to pin a new version of my agent for one company's runs, and check which version a run gets." For reference, the script looks like this, with the project's id as its argument:
import { createClient } from '@kindgi/sdk/client';
const kindgi = createClient(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi devconst projectId = process.argv[2];const acme = [{ key: 'company', value: 'acme' }];
// Pin 1.1.0 for acme's runs in this project.await kindgi.agents.pinned.pin('acme.order-desk', { version: '1.1.0', scope: { kind: 'segment', projectId, path: acme }, reason: 'Trying 1.1.0 with acme first',});
// Which version would a run there use, and why?const pinned = await kindgi.agents.pinned.resolve('acme.order-desk', { projectId, segments: acme });console.log(pinned.version, pinned.via, pinned.pinnedScope?.kind);
// A run that names no version gets it.const run = await kindgi.runs.start({ agent: 'acme.order-desk', projectId, segments: acme, input: { userMessage: 'Where is order A-1002?' },});console.log(run.agent?.version, run.agent?.via);import sys
from kindgi.client import Kindgi
kindgi = Kindgi() # KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi devproject_id = sys.argv[1]acme = [{"key": "company", "value": "acme"}]
# Pin 1.1.0 for acme's runs in this project.kindgi.agents.pinned.pin( "acme.order-desk", version="1.1.0", scope={"kind": "segment", "project_id": project_id, "path": acme}, reason="Trying 1.1.0 with acme first",)
# Which version would a run there use, and why?pinned = kindgi.agents.pinned.resolve("acme.order-desk", project_id=project_id, segments=acme)print(pinned.version, pinned.via, pinned.pinned_scope.kind if pinned.pinned_scope else None)
# A run that names no version gets it.run = kindgi.runs.start( agent="acme.order-desk", project_id=project_id, segments=acme, input={"userMessage": "Where is order A-1002?"},)if run.agent is not None: # set on an agent's runs print(run.agent.version, run.agent.via)It prints the version a run there would get, then the version the run got:
1.1.0 pinned segment1.1.0 pinnedkindgi.agents.pinned.list, unpin, history and getChange
(agents.pinned.get_change in Python) do what the commands on this page
do.
kindgi agents unpin removes a scope's own pinned version. Its runs then use
the scope above, or the project's Latest when nothing above is pinned:
pnpm exec kindgi agents unpin acme.order-desk --tenant --reason="Back to the latest"uv run kindgi agents unpin acme.order-desk --tenant --reason="Back to the latest"./kindgiw agents unpin acme.order-desk --tenant --reason="Back to the latest"./kindgiw agents unpin acme.order-desk --tenant --reason="Back to the latest"To go back to an earlier version, pin it again. On a self-hosted runtime with one pack service, that version runs only while your pack still has the tool versions it locked (A self-hosted runtime runs what your pack has).
Every request to pin a version and every unpin is kept, with who
asked and why.
kindgi agents pin-history list acme.order-desk --table lists them, as
The pin history shows.
Unpins
lists what each refuses.
Gate a pin
Section titled “Gate a pin”A release gate says what a request to pin a version must show before the version is pinned for a scope: a recent run on your test cases, enough corrected cases, figures at least this good, a clean replay and, if you want, a reviewer's approval. An agent has one release gate per scope. A request to pin a version is checked by the most specific one covering its scope, at its latest version.
Set a release gate
Section titled “Set a release gate”A scope with a release gate needs a pinned version of its own, so give the tenant one first:
pnpm exec kindgi agents pin acme.order-desk 1.0.0 --tenant --reason="1.0.0 is the version we checked"uv run kindgi agents pin acme.order-desk 1.0.0 --tenant --reason="1.0.0 is the version we checked"./kindgiw agents pin acme.order-desk 1.0.0 --tenant --reason="1.0.0 is the version we checked"./kindgiw agents pin acme.order-desk 1.0.0 --tenant --reason="1.0.0 is the version we checked"Ask your coding agent to write the release gate:
Before a new version of my agent is pinned, I want it to have done well on my test cases, and a senior reviewer to approve it.For reference, the release gate it writes looks like this. This guide saves it as
gate.json:
{ "comparison": { "maxAgeHours": 168 }, "evidence": { "minCases": 3 }, "metrics": [{ "name": "passRate", "minCandidate": 0.6, "maxDrop": 0.02 }], "replay": { "maxRefusedWrites": 0 }, "approvals": { "role": "senior" }}For an agent whose job is a write, such as sending a reply or placing
an order, leave replay out and gate on cases instead, for example
"cases": { "minPassing": 3, "noNewFailures": true }.
A replay refuses a call
that changes things unless the past run made the same call, with the same
arguments, and a write the model words, such as a mail's body, almost never
repeats word for word. Each refusal counts in refusedWrites, so
"maxRefusedWrites": 0 refuses every version. To keep replay, set
maxRefusedWrites, and any other count the version's run on the test cases
shows, to what you accept: those left out are 0.
Then publish it:
pnpm exec kindgi release-gates publish acme.order-desk-release --release-gate-version=1.0.0 --agent=acme.order-desk --tenant --spec=@gate.json --description="What a release of the order desk must show"uv run kindgi release-gates publish acme.order-desk-release --release-gate-version=1.0.0 --agent=acme.order-desk --tenant --spec=@gate.json --description="What a release of the order desk must show"./kindgiw release-gates publish acme.order-desk-release --release-gate-version=1.0.0 --agent=acme.order-desk --tenant --spec=@gate.json --description="What a release of the order desk must show"./kindgiw release-gates publish acme.order-desk-release --release-gate-version=1.0.0 --agent=acme.order-desk --tenant --spec=@gate.json --description="What a release of the order desk must show"acme.order-desk-releaseis the release gate's id, and--release-gate-versionits version.--agentand--tenantare the agent and the scope it gates.--spec=@gate.jsonreads the release gate from the file.
Requests to pin acme.order-desk anywhere in the tenant now go
through the release gate, unless a narrower scope has one of its own.
An approval it asks for is in the project of the pin it holds: a pin in a
client's project waits among that project's approvals.
Release gates has what
each part asks, and who may publish one.
Check before you pin it
Section titled “Check before you pin it”--check asks the release gate without recording anything:
pnpm exec kindgi agents pin acme.order-desk 1.3.0 --tenant --test-run=<test-run-id> --checkuv run kindgi agents pin acme.order-desk 1.3.0 --tenant --test-run=<test-run-id> --check./kindgiw agents pin acme.order-desk 1.3.0 --tenant --test-run=<test-run-id> --check./kindgiw agents pin acme.order-desk 1.3.0 --tenant --test-run=<test-run-id> --check--test-run=<test-run-id> names the run of 1.3.0 on the test cases. It prints
what the release gate would say:
{ "outcome": "needs-approval", "releaseGate": { "id": "acme.order-desk-release", "version": "1.0.0" }, "checks": [ { "name": "comparison.freshness", "passed": true, "message": "The test run finished 0.013 hours ago; the gate accepts up to 168.", … }, { "name": "baselineIsPinned", "passed": true, "message": "The test run measured this version against the pinned version, acme.order-desk 1.0.0.", … }, { "name": "metric.passRate.minCandidate", "passed": true, "message": "The weighted share marked right is 1; the gate needs at least 0.6.", … }, … ], "approval": { "role": "senior", "count": 1, "separateApprover": true }}gate-failed lists each check that failed, such as
This release gate needs a test run: name one with `testRunId` (a completed test run that tried this version on the test cases).
when there's no --test-run.
--check exits 0 for would-pin and 3 for the other two, so a script
can stop on the answer.
When the test cases have corrections that say what the run should have
done, the summary of the run on the test cases also says how many the
version passed, in corrections
(What it should have done).
No release gate checks it, so name it in the --reason, such as
--reason="Passes 3 of 3 corrections".
Pin through the release gate
Section titled “Pin through the release gate”Ask to pin it the same way, without --check:
pnpm exec kindgi agents pin acme.order-desk 1.3.0 --tenant --test-run=<test-run-id> --reason="1.3.0 did as well on the order checks"uv run kindgi agents pin acme.order-desk 1.3.0 --tenant --test-run=<test-run-id> --reason="1.3.0 did as well on the order checks"./kindgiw agents pin acme.order-desk 1.3.0 --tenant --test-run=<test-run-id> --reason="1.3.0 did as well on the order checks"./kindgiw agents pin acme.order-desk 1.3.0 --tenant --test-run=<test-run-id> --reason="1.3.0 did as well on the order checks"It prints the request, with its status, or the release gate's refusal:
- When the release gate has no
approvals, a request that passes is pinned at once,"status": "pinned". - When it has, it waits:
"status": "pending-approval", with anapprovalId. The scope keeps its version until a reviewer approves. - When it fails, nothing changes. It's refused with every failed check,
such as
The release gate refused acme.order-desk 1.3.0: The weighted share marked right rests on 3 corrected cases for this version; the gate needs at least 20. …, and kept in the history asrefused.
It exits 0 when the version is pinned, and 3 when it waits for an approval or the release gate refused it.
To wait for the reviewer, add --wait. It names the approval it waits for and
the command that approves it, then exits 0 once the version is pinned, or 3 when
the approval is rejected or expires or the wait runs out: an hour, or the time
--wait=30m gives. It exits 3 too when the request is superseded: it was
approved, but something the check measured had changed by then, such as the
version serving the scope or its release gate, so pin it again: the
check runs anew. It exits 1 when it can't read the request 3 times in a row,
after printing what it read last. Neither that nor Ctrl-C stops the request:
once it's approved, the version is pinned.
Approve it
Section titled “Approve it”The request is an approval like any other, for a reviewer with the release gate's role, as Decide an approval shows:
pnpm exec kindgi approvals complete <approval-id> --decision=approve --rationale="Checked the test run"uv run kindgi approvals complete <approval-id> --decision=approve --rationale="Checked the test run"./kindgiw approvals complete <approval-id> --decision=approve --rationale="Checked the test run"./kindgiw approvals complete <approval-id> --decision=approve --rationale="Checked the test run"<approval-id> is the request's approvalId. The version is pinned, and
the request is pinned. Whoever asked can't approve their own request:
Approval <approval-id> was asked for by you: someone else must approve itThat holds unless the release gate's approvals has "separateApprover": false.
A deployment that one person uses with the API token has nobody else to
approve, so its release gates with approvals need
"separateApprover": false.
With people of their own, register a
second reviewer instead.
Pin from your app
Section titled “Pin from your app”Your app checks the release gate, then pins the version. Ask your coding agent: "Check the release gate for my new agent version, and pin it unless the gate refuses." For reference, the script looks like this, with the id of the run on the test cases as its argument:
import { createClient } from '@kindgi/sdk/client';
const kindgi = createClient(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi devconst testRunId = process.argv[2]; // the test run of 1.3.0const request = { version: '1.3.0', scope: { kind: 'tenant' as const }, testRunId, reason: '1.3.0 did as well on the order checks',};
// What would the gate say? Nothing is recorded.const check = await kindgi.agents.pinned.check('acme.order-desk', request);console.log(check.outcome);for (const c of check.checks.filter((c) => !c.passed)) console.log(c.message);
if (check.outcome !== 'gate-failed') { const { pin } = await kindgi.agents.pinned.pin('acme.order-desk', request); console.log(pin.status, pin.approvalId ?? '');}import sys
from kindgi.client import Kindgi
kindgi = Kindgi() # KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi devrequest = { "version": "1.3.0", "scope": {"kind": "tenant"}, "test_run_id": sys.argv[1], # the test run of 1.3.0 "reason": "1.3.0 did as well on the order checks",}
# What would the gate say? Nothing is recorded.check = kindgi.agents.pinned.check("acme.order-desk", **request)print(check.outcome)for c in check.checks: if not c.passed: print(c.message)
if check.outcome != "gate-failed": pin = kindgi.agents.pinned.pin("acme.order-desk", **request).pin print(pin.status, pin.approval_id or "")It prints the release gate's outcome, then the request waiting for its approval:
needs-approvalpending-approval fa6fbbcf-7b0d-4c70-b462-d6a78bbcfd11kindgi.releaseGates.publish, or release_gates.publish in Python, and
kindgi.agents.releaseGate.resolve, or agents.release_gate.resolve, do what
kindgi release-gates publish and kindgi agents release-gate do.