# Agent and tool versions

> How a range picks a tool version, how a published agent version locks its tools, how a pin holds a tool at a version, what a deploy does when they change, what a self-hosted runtime runs, and what unregistering a version does.

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

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

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:

```sh
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:

```json excerpt
{
  "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:

```json
{
  "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

A **pin** holds a tool at a version for a project, an org or the whole tenant (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 tenant'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.0` while 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:

1. **Pin the version in use,** 1.1.0, for the project.
2. **Publish 1.2.0.** Agent and flow versions made in the project still lock 1.1.0.
3. **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](https://docs.kindgi.com/v0.4.0/guides/improve/pinned-versions-reference.md) set, runs that name no version keep the one they had.
4. **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:

- **TypeScript**

  `pin-a-tool.ts`

  ```ts
  import { createClient } from '@kindgi/sdk/client';

  const kindgi = createClient(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev
  const 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);
  ```

- **Python**

  `pin_a_tool.py`

  ```python
  import sys

  from kindgi.client import Kindgi

  kindgi = Kindgi()  # KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev
  project_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)
  ```

- **Java**

  `src/main/java/com/acme/app/PinATool.java`

  ```java
  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());
    }
  }
  ```

- **Scala**

  `src/main/scala/acme/app/PinATool.scala`

  ```scala
  package acme.app

  import com.kindgi.client.Kindgi
  import com.kindgi.client.models.{PinScopeProject, PinVersionBody}
  import com.kindgi.client.resources.ToolsPinnedResolveParams
  import 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:

```text
1.1.0 pinned
```

`kindgi.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](https://docs.kindgi.com/v0.4.0/guides/flows/pin-a-flow-version.md) 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 tenant 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

`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:

```text
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 match
```

Set 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](https://docs.kindgi.com/v0.4.0/guides/agents/agent-and-tool-versions.md#a-pin-holds-a-tool-at-a-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](https://docs.kindgi.com/v0.4.0/guides/improve/pinned-versions-reference.md#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](https://docs.kindgi.com/v0.4.0/guides/improve/pinned-versions-reference.md#latest).

**An agent version published without pins,** such as by an older runtime, gets a pinned next version at the first deploy:

```text
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 match
```

An 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

A self-hosted runtime with one pack service, as in the [Docker](https://docs.kindgi.com/v0.4.0/deploy/self-hosted/self-host.md) and [Azure Container Apps](https://docs.kindgi.com/v0.4.0/deploy/self-hosted/azure-container-apps.md) setups, sends every tool call to that service, whatever version the call names. (On [Cloud Run](https://docs.kindgi.com/v0.4.0/deploy/self-hosted/cloud-run.md), 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:

```text
tool-version-mismatch: Tool "acme.score" is 1.1.0 in this pack; the caller asked for 1.0.0
```

After 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](https://docs.kindgi.com/v0.4.0/guides/improve/pinned-versions-reference.md) or a [pinned flow version](https://docs.kindgi.com/v0.4.0/guides/flows/pin-a-flow-version.md) holds it at an older one: move it.
- **Move the tool's pin too.** While a [pin](https://docs.kindgi.com/v0.4.0/guides/agents/agent-and-tool-versions.md#a-pin-holds-a-tool-at-a-version) 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](https://docs.kindgi.com/v0.4.0/guides/agents/agent-and-tool-versions.md#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](https://docs.kindgi.com/v0.4.0/deploy/self-hosted/serve-many-tenants.md) 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

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:

- **TypeScript**

  `unregister-version.ts`

  ```ts
  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 dev
  const 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);
  ```

- **Python**

  `unregister_version.py`

  ```python
  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")
  ```

- **Java**

  `src/main/java/com/acme/app/UnregisterVersion.java`

  ```java
  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}`.

- **Scala**

  `src/main/scala/acme/app/UnregisterVersion.scala`

  ```scala
  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:

```text
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:

```text
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`:

  ```text
  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`:

  ```json excerpt
  {"error":{"code":"flow-not-found","message":"Flow \"acme.refunds@1.0.0\" is unregistered; reinstate it or run another version.","requestId":"req-…"}}
  ```

  A 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](https://docs.kindgi.com/v0.4.0/guides/improve/release-an-agent-version.md#unpin) shows. Until then, it's refused with `409 agent-version-pinned`, naming where it's pinned in `details.scopes`:

  ```text
  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-pinned` or `409 tool-version-pinned`, naming the pins' scopes in `details.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`, or `kindgi agents versions <id> --include-unregistered`, and the same for `flows` and `tools`.

- **A deploy brings a tool version back:** deploying an image whose pack has an unregistered tool version reinstates it, as `kindgi tools reinstate` does, 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 with `400 deployment-validation-failed`, as for any tool that names one ([A model provider's key isn't a tool's](https://docs.kindgi.com/v0.4.0/guides/tools/give-a-tool-a-secret.md#a-model-providers-key-isnt-a-tools)).

- **One with every version unregistered is retired:** fetching it answers `410`, such as `agent-gone`. Its list leaves it out unless you ask with `?includeRetired=true`, or `kindgi agents list --include-retired`, and the same for `flows`, `tools` and `test-sets`. Then it's listed as its highest version, with `unregisteredAt`. The lists of blocks, policies and release gates take `?includeRetired=true` too, as `client.blocks.list({ includeRetired: true })` does, and the same for `policies` and `releaseGates`. From the CLI, ask with `kindgi blocks list --include-retired` or `kindgi release-gates list --include-retired`.

- **A run waiting on an approval** resumes after its version is unregistered.

## Next

- [Release an agent version](https://docs.kindgi.com/v0.4.0/guides/improve/release-an-agent-version.md)
- [Write an agent](https://docs.kindgi.com/v0.4.0/guides/agents/write-an-agent.md)
- [Choose the model an agent uses](https://docs.kindgi.com/v0.4.0/guides/agents/choose-a-model.md)
