Agent and tool versions
This page explains which tool version a run of an agent gets, how a published agent version locks it, how a pin holds a tool at a version, which versions a self-hosted runtime can run, and what unregistering a version does.
Tools and agents are versioned, and several versions of each can be active at once. An agent names each tool by a range.
A range picks a version
Section titled “A range picks a version”A range picks the highest active version it allows. ^1.0.0 picks 1.2.0
when 1.0.0, 1.1.0 and 1.2.0 are active, and never picks 2.0.0.
A pack can hold several versions of one tool side by side: two tool files with
the same id and different versions. Both are served, and each agent runs the
one its range picks. kindgi tools versions <tool-id> lists them.
A published agent version locks its tools
Section titled “A published agent version locks its tools”When an agent version is published, by kindgi deploy or by
POST /v1/agents, each tool range is resolved once, to an exact version: the
version's pins. Every run of that version uses them.
An agent acme.greeter 1.0.0 asks for acme.greet ^1.0.0 while
acme.greet 1.0.0 and 1.1.0 are published. It's pinned to 1.1.0, as reading
the version shows:
curl "$KINDGI_API_URL/v1/agents/acme.greeter/versions/1.0.0" -H @<(printf 'Authorization: Bearer %s\n' "$KINDGI_API_TOKEN")$KINDGI_API_URL and $KINDGI_API_TOKEN are your runtime's URL and API
token. It prints the version, with its pins:
{ "version": "1.0.0", "pins": { "tools": { "acme.greet": "1.1.0" }, "prompts": {}, "settings": {} }, "pinsDigest": "sha256:d63b0dc80adfcece6df1989ba7a423d78ec4c917f1f501fa46495457af9b75eb", …}Then acme.greet 1.2.0 is published. acme.greeter 1.0.0 still runs 1.1.0.
The next agent version, 1.0.1, is pinned to 1.2.0. A new tool version
reaches a published agent only through a new agent version.
A range that no published version matches is refused when the agent is published, so a version can't be published with a tool it can't run:
{ "code": "validation-failed", "message": "Agent \"acme.greeter\" uses tool versions that aren't published (1 issue)", "details": { "issues": [ { "path": "/tools/0/version", "message": "tool \"acme.greet\" has no published version in \"^3.0.0\" (published: 1.2.0, 1.1.0, 1.0.0); publish one in range first, or change the range" } ] }}Under kindgi dev, agents aren't published: each turn resolves its
ranges when it starts, so a tool you save is used at the next turn. An agent
version published without pins resolves its ranges per run too.
A pin holds a tool at a version
Section titled “A pin holds a tool at a version”A pin holds a tool at a version for a project, an org or the whole workspace (runtime 0.3.0 or later), so you can publish a new tool version without it reaching the agents and flows made there. When an agent or flow version is made in a project a tool's pin covers (the project's own pin, then its org's, then the workspace's), the version locks the tool at the pinned version:
- An agent's tool range locks the pinned version when the range allows it.
A range that leaves it out, such as the exact
1.2.0while 1.1.0 is pinned, picks as above. - A flow's tool step locks the pinned version.
This pin is set for a scope. It isn't a version's own pins, the tool
versions that agent or flow version locked when it was published, which never
change.
A pin counts only while its version is active. It changes no version already made: a pin reaches an agent or flow through its next version, as a new tool version does.
To try a new tool version without putting it in use:
- Pin the version in use, 1.1.0, for the project.
- Publish 1.2.0. Agent and flow versions made in the project still lock 1.1.0.
- Try it in an agent version whose range is the exact
1.2.0, and run that version by name (agentVersion). With the agent's pinned version set, runs that name no version keep the one they had. - Move the pin to 1.2.0. The next agent and flow versions made there lock it.
Ask your coding agent: "I want to pin my tool's version in a project, then try a new version there without putting it in use." For reference, step 1 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];
// Agent and flow versions made in the project lock acme.greet 1.1.0.await kindgi.tools.pinned.pin('acme.greet', { version: '1.1.0', scope: { kind: 'project', projectId }, reason: '1.1.0 is the version we checked',});
// Which version would the next agent or flow version made there lock, and why?const locks = await kindgi.tools.pinned.resolve('acme.greet', { projectId });console.log(locks.version, locks.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]
# Agent and flow versions made in the project lock acme.greet 1.1.0.kindgi.tools.pinned.pin( "acme.greet", version="1.1.0", scope={"kind": "project", "project_id": project_id}, reason="1.1.0 is the version we checked",)
# Which version would the next agent or flow version made there lock, and why?locks = kindgi.tools.pinned.resolve("acme.greet", project_id=project_id)print(locks.version, locks.via)package com.acme.app;
import com.kindgi.client.Kindgi;import com.kindgi.client.models.PinScopeProject;import com.kindgi.client.models.PinVersionBody;import com.kindgi.client.models.PinnedVersionResolution;import com.kindgi.client.resources.ToolsPinnedResolveParams;import java.util.UUID;
public final class PinATool { public static void main(String[] args) { Kindgi kindgi = Kindgi.create(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev UUID projectId = UUID.fromString(args[0]);
// Agent and flow versions made in the project lock acme.greet 1.1.0. kindgi.tools().pinned().pin("acme.greet", PinVersionBody.builder() .version("1.1.0") .scope(PinScopeProject.builder().projectId(projectId).build()) .reason("1.1.0 is the version we checked") .build());
// Which version would the next agent or flow version made there lock, and why? PinnedVersionResolution locks = kindgi.tools().pinned().resolve("acme.greet", ToolsPinnedResolveParams.builder().projectId(projectId).build()); System.out.println(locks.version() + " " + locks.via()); }}package acme.app
import com.kindgi.client.Kindgiimport com.kindgi.client.models.{PinScopeProject, PinVersionBody}import com.kindgi.client.resources.ToolsPinnedResolveParamsimport java.util.UUID
object PinATool { def main(args: Array[String]): Unit = { val kindgi = Kindgi.create() // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev val projectId = UUID.fromString(args(0))
// Agent and flow versions made in the project lock acme.greet 1.1.0. kindgi.tools().pinned().pin("acme.greet", PinVersionBody.builder() .version("1.1.0") .scope(PinScopeProject.builder().projectId(projectId).build()) .reason("1.1.0 is the version we checked") .build())
// Which version would the next agent or flow version made there lock, and why? val locks = kindgi.tools().pinned().resolve("acme.greet", ToolsPinnedResolveParams.builder().projectId(projectId).build()) println(s"${locks.version} ${locks.via}") }}It prints the version the next agent or flow version made there locks, and how it was chosen:
1.1.0 pinnedkindgi.tools.pinned also has resolve (the version a flow version made in
a project would lock, and why), list, unpin and history, as
Pin a flow's version shows for flows.
On the CLI, they're kindgi tools pin, pinned, pinned-versions, unpin
and pin-history list. Pinning and unpinning need pin on the tool, which
its admins have.
In an environment order, versions are made in the order's first
environment, so a tool in an order is pinned there. A pin for a later
environment would never act, and is refused with
409 environment-takes-promotions, naming the first environment in
details.firstProjectId when you can read that project. A pin for an org or
the workspace is refused with 409 pin-scope-spans-environments.
Some tools can't be pinned. A tool an MCP endpoint's catalog lists (its
metadata has the catalog's kindgi.mcp key) follows its server: a call goes
to the live server whatever the version, so a pin couldn't keep it still. The
runtime's own tools follow the runtime. Pinning either is refused with
400 tool-not-pinnable; reference them with a range.
Deploys make the new version for you
Section titled “Deploys make the new version for you”kindgi deploy publishes the agents in your pack. A published version never
changes. So when an agent keeps its version number but its pins would change,
because a tool it uses has a new version, the deploy registers the next free
version and says so:
agent acme.matcher: registered new version 1.4.1 (1.4.0's pins changed: tool acme.score 1.0.0 → 1.1.0); set version: '1.4.1' in acme.matcher to matchSet that version in the agent's code, so the code and the runtime agree.
Until you do, deploying again registers nothing new and says
1.4.0 runs as 1.4.1, registered by an earlier deploy.
The same happens when you change an agent's code and keep its version
number: the deploy registers the next free version, because
1.4.0 is taken by a different definition.
A tool's version doesn't move for you. A tool version keeps the
definition it was first published with, so code you change under the same
version would run under the old one. When you change what a tool declares,
such as its description, its input or output, its needs, mutating or its
approvals, and keep its version, the deploy is refused with
409 tool-already-registered. The
refusal names the fields that differ, and nothing is deployed. Give the tool a
new version. A range that takes it (^1.0.0 takes 1.1.0) picks it up in the
agent's next version, as above. A new image of the same tool keeps its
version: the deploy points the version at the new image.
Flows are pinned the same way. A flow version pins its tool nodes to their latest at publish (a tool's pinned version, where a pin covers the flow's project), and its agent steps that don't name a version to what a run in the flow's project gets then: the pinned version that covers it, else the project's Latest. A tool change reaches a flow through a new agent version and a new flow version, in one deploy, with a line for each.
Where a version runs. An agent version runs in the project it was
published in, and in each project a deployment that isn't retired shipped
it into, such as the next environment kindgi promote carried its build to.
So a change to the agent reaches each of them. client.agents.projects(agentId)
(GET /v1/agents/{agentId}/projects) lists them, the agent's first project
first, naming only the ones you can read. A project stays on the list after
the versions shipped there are unregistered, because reinstating one would
run there again. Each project says what a run there that names no version
gets, and why: the pinned version that covers it, else its
Latest.
An agent version published without pins, such as by an older runtime, gets a pinned next version at the first deploy:
agent acme.matcher: registered new version 1.4.1 (1.4.0 was published before pins; 1.4.1 pins its tools); set version: '1.4.1' in acme.matcher to matchAn app that runs an exact version keeps running the old, unpinned one until
you change it: agentVersion: '1.4.0', or --agent-version=1.4.0 from the
CLI. An app that names no version gets the pinned one, since the deploy
made it Latest.
A self-hosted runtime runs what your pack has
Section titled “A self-hosted runtime runs what your pack has”A self-hosted runtime with one pack service, as in the Docker and Azure Container Apps setups, sends every tool call to that service, whatever version the call names. (On Cloud Run, each deployment has a pack service of its own, and a call goes to the newest deployment in the run's project that shipped the tool version it names.) A new build of your pack replaces the one the service runs. So a tool version runs only while the build the service runs has it, and so does each agent and flow version that locks it. What this page says about an older version still running holds there while its tools are in the pack.
When the pack no longer has a tool version that an agent or flow version locked, a run of that version still starts. A flow version fails at that tool's step, after the steps before it have run, and an agent version's call to the tool fails:
tool-version-mismatch: Tool "acme.score" is 1.1.0 in this pack; the caller asked for 1.0.0After a deploy that gives a tool a new version:
- Run the versions the deploy made. It registered a new version of each agent and flow whose pins changed, with a line for each. A run that names no version takes the newest, unless a pinned agent version or a pinned flow version holds it at an older one: move it.
- Move the tool's pin too. While a pin holds the tool at its old version, the agent and flow versions made under it lock the old version, and fail the same way.
- To keep an older version running, keep its tools in the pack. A pack can hold several versions of a tool side by side (A range picks a version), and the service runs each.
Until the service runs the new build, the versions the deploy made fail the same way, since the build before it doesn't have their new tool versions.
The many-tenants setup runs a pack service for each deployment, and sends each call to the newest one in the run's project that shipped the version it names. There, a version keeps running after later deploys while that service is up.
What unregistering a version does
Section titled “What unregistering a version does”Unregistering a version stops it from being chosen. It doesn't stop the versions that pin it. Ask your coding agent: "I want a script that takes an old version of my agent out of use, and can bring it back." For reference, the code looks like this:
import { createClient } from '@kindgi/sdk/client';import type { AgentId, Semver } from '@kindgi/sdk/types';
const kindgi = createClient(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi devconst agent = 'acme.greeter' as AgentId;const version = '1.0.0' as Semver;
// No new run gets 1.0.0. What pins it keeps running it.await kindgi.agents.versions.unregister(agent, version);const unregistered = await kindgi.agents.versions.get(agent, version);console.log(unregistered.unregisteredAt, unregistered.pins?.tools);
// 1.0.0 can be chosen again, unchanged.await kindgi.agents.versions.reinstate(agent, version);from kindgi.client import Kindgi
kindgi = Kindgi() # KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev
# No new run gets 1.0.0. What pins it keeps running it.kindgi.agents.unregister("acme.greeter", "1.0.0")unregistered = kindgi.agents.get_version("acme.greeter", "1.0.0")print(unregistered.unregistered_at, unregistered.pins.tools if unregistered.pins else None)
# 1.0.0 can be chosen again, unchanged.kindgi.agents.reinstate_version("acme.greeter", "1.0.0")package com.acme.app;
import com.kindgi.client.Kindgi;import com.kindgi.client.models.Agent;
public final class UnregisterVersion { public static void main(String[] args) { Kindgi kindgi = Kindgi.create(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev String agent = "acme.greeter"; String version = "1.0.0";
// No new run gets 1.0.0. What pins it keeps running it. kindgi.agents().unregister(agent, version); Agent unregistered = kindgi.agents().getVersion(agent, version); System.out.println(unregistered.unregisteredAt() + " " + (unregistered.pins() == null ? null : unregistered.pins().tools()));
// 1.0.0 can be chosen again, unchanged. kindgi.agents().reinstateVersion(agent, version); }}The operation ids name the methods: agents().unregister,
agents().getVersion, agents().reinstateVersion. The pins print as a
Java map: {acme.greet=1.1.0}.
package acme.app
import com.kindgi.client.Kindgi
object UnregisterVersion { def main(args: Array[String]): Unit = { val kindgi = Kindgi.create() // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev val agent = "acme.greeter" val version = "1.0.0"
// No new run gets 1.0.0. What pins it keeps running it. kindgi.agents().unregister(agent, version) val unregistered = kindgi.agents().getVersion(agent, version) println(s"${unregistered.unregisteredAt} ${Option(unregistered.pins).map(_.tools).orNull}")
// 1.0.0 can be chosen again, unchanged. kindgi.agents().reinstateVersion(agent, version) }}The operation ids name the methods: agents().unregister,
agents().getVersion, agents().reinstateVersion. The pins print as a
Java map: {acme.greet=1.1.0}.
It prints when the version was unregistered, and its pins, which it keeps:
2026-10-06T05:42:12.899Z { 'acme.greet': '1.1.0' }Tool and flow versions are unregistered and reinstated the same way. This
is for a runtime you deploy to: under kindgi dev, the pack's files are the
agents and tools, so you remove or change the file instead. There, an
unregister is refused:
Under kindgi dev, the pack is the source of agents: change the agent in the pack's files and kindgi dev reloads it.-
No new run gets it: not by a range, not as Latest, not by its exact number. A run that names it is refused with
404 agent-not-found:Agent "acme.greeter@1.0.0" is unregistered; reinstate it or run another version.A flow's version is refused the same way, as
404 flow-not-found:{"error":{"code":"flow-not-found","message":"Flow \"acme.refunds@1.0.0\" is unregistered; reinstate it or run another version.","requestId":"req-…"}}ExcerptA run that names no version gets the pinned version that covers it, or the project's Latest among the versions still registered. A run already going, one waiting for an approval included, finishes on the version it started.
-
A version pinned somewhere can't be unregistered. First unpin it or pin another version everywhere this one is pinned, as Release an agent version shows. Until then, it's refused with
409 agent-version-pinned, naming where it's pinned indetails.scopes:acme.order-desk 1.0.0 is pinned in a scope: unpin, or pin another version there first, then unregister it. -
A pinned flow or tool version can't be unregistered either (runtime 0.3.0 or later). Move or remove the pins first; until then, it's refused with
409 flow-version-pinnedor409 tool-version-pinned, naming the pins' scopes indetails.scopes. -
What pins it keeps running: an agent version pinned to an unregistered tool version, or a flow whose step names an unregistered agent version, runs as before.
-
You can still read it: it keeps its pins, and fetching it shows
unregisteredAt. Reinstating it makes it choosable again. -
Finding one to reinstate: the versions list of an agent, flow, tool or set of test cases includes unregistered versions when you ask, each with
unregisteredAt. Ask with?includeTombstoned=true, orkindgi agents versions <id> --include-unregistered, and the same forflowsandtools. -
A deploy brings a tool version back: deploying an image whose pack has an unregistered tool version reinstates it, as
kindgi tools reinstatedoes, so the same image deploys again after a deploy that failed and rolled back. A tool version that names a model provider's key stays unregistered: the pack's tool names it too, and the deploy is refused with400 deployment-validation-failed, as for any tool that names one (A model provider's key isn't a tool's). -
One with every version unregistered is retired: fetching it answers
410, such asagent-gone. Its list leaves it out unless you ask with?includeRetired=true, orkindgi agents list --include-retired, and the same forflows,toolsandtest-sets. Then it's listed as its highest version, withunregisteredAt. The lists of blocks, policies and release gates take?includeRetired=truetoo, asclient.blocks.list({ includeRetired: true })does, and the same forpoliciesandreleaseGates. From the CLI, ask withkindgi blocks list --include-retiredorkindgi release-gates list --include-retired. -
A run waiting on an approval resumes after its version is unregistered.