# 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, 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, 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 [live version](https://docs.kindgi.com/v0.3.0/guides/evals/live-versions-reference.md) pinned, 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.3.0/guides/flows/pin-a-flow-version.md) shows for flows. 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, and its agent steps that don't name a version, to their latest at publish (a tool's [pinned version](https://docs.kindgi.com/v0.3.0/guides/agents/agent-and-tool-versions.md#a-pin-holds-a-tool-at-a-version), where a pin covers the flow's project). 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.

**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 runs the latest gets the pinned one.

## 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 the 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 latest live one. A run already going, one waiting for an approval included, finishes on the version it started.

- **A version live somewhere can't be unregistered.** First roll back, unpin or make another version live everywhere this one is live, as [Release an agent version](https://docs.kindgi.com/v0.3.0/guides/evals/release-an-agent-version.md#roll-back-or-unpin) shows. Until then, it's refused with `409 agent-version-live`, naming where it's live in `details.scopes`:

  ```text
  acme.order-desk 1.0.0 is live in a scope: roll back, unpin, or promote 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.3.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 `eval-suites`. Then it's listed as its highest version, with `unregisteredAt`.

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

## Next

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