# Write an agent

> Define an agent in a pack, with its instructions, the tools it may call, the guardrails on its answer and what its model must support.

An agent is a file under `agents/`. This one answers customers' questions about their orders. It looks orders up with a tool, and the sample pack's `acme.response-not-empty` guardrail checks its answer.

## The tool it calls

Ask your coding agent: "I want a tool that looks up an order by its id." Kindgi's skills in your pack tell it how ([How it knows Kindgi](https://docs.kindgi.com/v0.1.9/start/coding-agents.md)). For reference, the tool looks like this. This guide calls it `acme.lookup-order`:

- **TypeScript**

  `tools/lookup-order/index.ts`

  ```ts
  import { defineTool } from '@kindgi/sdk/define';
  import type { ToolId } from '@kindgi/sdk/types';
  import { z } from 'zod';

  const ORDERS: Record<string, { status: string; eta: string | null }> = {
    'A-1001': { status: 'shipped', eta: '2026-10-06' },
    'A-1002': { status: 'processing', eta: null },
  };

  const defined = defineTool({
    id: 'acme.lookup-order' as ToolId,
    description: 'Looks up an order by its id (like A-1001): its status and expected delivery date.',
    version: '0.1.0',
    input: z.object({ orderId: z.string() }),
    output: z.object({ orderId: z.string(), status: z.string(), eta: z.string().nullable() }),
    effects: [],
    mutating: false,
    handler: async ({ orderId }) => {
      const order = ORDERS[orderId];
      return order === undefined
        ? { orderId, status: 'not-found', eta: null }
        : { orderId, ...order };
    },
  });

  if (defined.kind === 'err') throw new Error(defined.error.message);

  export default defined.value;
  ```

- **Python**

  `tools/lookup_order.py`

  ```python
  from pydantic import BaseModel, Field

  from kindgi import tool

  ORDERS = {
      "A-1001": {"status": "shipped", "eta": "2026-10-06"},
      "A-1002": {"status": "processing", "eta": None},
  }


  class OrderQuery(BaseModel):
      order_id: str = Field(alias="orderId")


  class Order(BaseModel):
      order_id: str = Field(alias="orderId")
      status: str
      eta: str | None


  @tool(id="acme.lookup-order", mutating=False)
  def lookup_order(input: OrderQuery) -> Order:
      """Looks up an order by its id (like A-1001): its status and expected delivery date."""
      order = ORDERS.get(input.order_id, {"status": "not-found", "eta": None})
      return Order(orderId=input.order_id, **order)
  ```

- **Java**

  `src/main/java/acme/tools/LookupOrder.java`

  ```java
  package acme.tools;

  import com.kindgi.pack.Tool;
  import java.util.Map;
  import org.jspecify.annotations.Nullable;

  public final class LookupOrder {
    public record Input(String orderId) {}

    public record Output(String orderId, String status, @Nullable String eta) {}

    private static final Map<String, Output> ORDERS = Map.of(
        "A-1001", new Output("A-1001", "shipped", "2026-10-06"),
        "A-1002", new Output("A-1002", "processing", null));

    public static final Tool<Input, Output> TOOL = Tool.define("acme.lookup-order")
        .description("Looks up an order by its id (like A-1001): its status and expected delivery date.")
        .version("0.1.0")
        .input(Input.class)
        .output(Output.class)
        .mutating(false)
        .handler((input, ctx) ->
            ORDERS.getOrDefault(input.orderId(), new Output(input.orderId(), "not-found", null)));

    private LookupOrder() {}
  }
  ```

- **Scala**

  `src/main/scala/acme/tools/LookupOrder.scala`

  ```scala
  package acme.tools

  import com.kindgi.pack.scaladsl._

  object LookupOrder {
    final case class Input(orderId: String)
    final case class Output(orderId: String, status: String, eta: Option[String])

    private val orders = Map(
      "A-1001" -> Output("A-1001", "shipped", Some("2026-10-06")),
      "A-1002" -> Output("A-1002", "processing", None))

    val tool: Tool[Input, Output] = Tool[Input, Output]("acme.lookup-order")
      .description("Looks up an order by its id (like A-1001): its status and expected delivery date.")
      .version("0.1.0")
      .readOnly
      .handler((in, _) => orders.getOrElse(in.orderId, Output(in.orderId, "not-found", None)))
  }
  ```

The model sees the tool's id, its description and its input schema. The description is what it reads to decide when to call the tool, so say what the tool does and what its input looks like.

## The agent

Ask your coding agent: "I want an agent that answers customers' questions about their orders, using that tool." For reference, the agent looks like this. This guide calls it `acme.order-desk`:

- **TypeScript**

  `agents/order-desk/index.ts`

  ```ts
  import { defineAgent } from '@kindgi/sdk/define';

  const defined = defineAgent({
    id: 'acme.order-desk',
    version: '0.1.0',
    name: 'Order desk',
    description: "Answers customers' questions about their orders.",
    instructions: [
      'You answer customer questions about Acme orders. Today is {{ today }}.',
      'Look an order up with the lookup-order tool before you say anything about it.',
      'If the customer gives no order id, ask for it. Never guess a status or a date.',
      'Answer in two sentences at most.',
    ].join('\n'),
    capabilities: [{ needs: [{ feature: 'tool-use' }] }],
    tools: [{ id: 'acme.lookup-order', version: '^0.1.0' }],
    retrieval: [],
    guardrails: ['acme.response-not-empty'],
    budget: { maxSteps: 6, maxCostUsd: 0.05, maxWallMs: 60_000 },
  });

  if (defined.kind === 'err') throw new Error(defined.error.message);

  export default defined.value;
  ```

  `defineAgent` checks the definition where it's written and returns an error for an invalid one (an empty `capabilities`, a version that isn't semver); `defined.error.issues` lists the problems. Throwing makes `kindgi dev` report the file.

- **Python**

  `agents/order_desk.py`

  ```python
  from kindgi import Agent

  from ..guardrails.response_not_empty import response_not_empty
  from ..tools.lookup_order import lookup_order

  order_desk = Agent(
      id="acme.order-desk",
      version="0.1.0",
      name="Order desk",
      description="Answers customers' questions about their orders.",
      instructions="\n".join([
          "You answer customer questions about Acme orders. Today is {{ today }}.",
          "Look an order up with the lookup-order tool before you say anything about it.",
          "If the customer gives no order id, ask for it. Never guess a status or a date.",
          "Answer in two sentences at most.",
      ]),
      capabilities=[{"needs": [{"feature": "tool-use"}]}],
      tools=[lookup_order],
      guardrails=[response_not_empty],
      budget={"maxSteps": 6, "maxCostUsd": 0.05, "maxWallMs": 60_000},
  )
  ```

  Assign the agent at module level: an agent built inside a function isn't found. The keys inside the dicts (`maxSteps`, `feature`) keep the API's camelCase; only the keyword arguments are snake_case.

- **Java**

  `src/main/java/acme/agents/OrderDesk.java`

  ```java
  package acme.agents;

  import acme.guardrails.ResponseNotEmpty;
  import acme.tools.LookupOrder;
  import com.kindgi.pack.Agent;
  import java.util.List;
  import java.util.Map;

  public final class OrderDesk {
    public static final Agent AGENT = Agent.define("acme.order-desk")
        .version("0.1.0")
        .name("Order desk")
        .description("Answers customers' questions about their orders.")
        .instructions(String.join("\n",
            "You answer customer questions about Acme orders. Today is {{ today }}.",
            "Look an order up with the lookup-order tool before you say anything about it.",
            "If the customer gives no order id, ask for it. Never guess a status or a date.",
            "Answer in two sentences at most."))
        .capability(Map.of("needs", List.of(Map.of("feature", "tool-use"))))
        .tool(LookupOrder.TOOL)
        .guardrail(ResponseNotEmpty.GUARDRAIL)
        .set("budget", Map.of("maxSteps", 6, "maxCostUsd", 0.05, "maxWallMs", 60_000))
        .build();

    private OrderDesk() {}
  }
  ```

  An agent is a `public static final` field of a class under `agents/`. `build()` refuses one with no version, name or instructions, and the indexer names the file (`agent acme.order-desk: name is required`). Any other field of an agent (`budget` here, `parameters`, `retrieval`, …) goes in with `set(field, value)`, as the API names it.

- **Scala**

  `src/main/scala/acme/agents/OrderDesk.scala`

  ```scala
  package acme.agents

  import acme.guardrails.ResponseNotEmpty
  import acme.tools.LookupOrder
  import com.kindgi.pack.scaladsl._

  object OrderDesk {
    val agent: Agent = Agent("acme.order-desk")
      .version("0.1.0")
      .name("Order desk")
      .description("Answers customers' questions about their orders.")
      .instructions(Seq(
        "You answer customer questions about Acme orders. Today is {{ today }}.",
        "Look an order up with the lookup-order tool before you say anything about it.",
        "If the customer gives no order id, ask for it. Never guess a status or a date.",
        "Answer in two sentences at most.").mkString("\n"))
      .capability(Map("needs" -> Seq(Map("feature" -> "tool-use"))))
      .tool(LookupOrder.tool)
      .guardrail(ResponseNotEmpty.guardrail)
      .set("budget", Map("maxSteps" -> 6, "maxCostUsd" -> 0.05, "maxWallMs" -> 60000))
      .build()
  }
  ```

  An agent is a `val` of an object named like its file, under `agents/`. The maps and lists are Scala's; `set(field, value)` takes any other field of an agent, as in Java.

* **`id`** is `<pack>.<name>`; **`version`** is an exact semver.
* **`instructions`** is what the model is told at the start of every turn: a template, below. Name a tool by what it does ("the lookup-order tool"), not by its id: a model sees a tool's id in its provider's form (`acme__lookup-order` for Anthropic and OpenAI-compatible models), and an id with a dot in the instructions can make it call a name it wasn't given.
* **`capabilities`** says what the model must support. `tool-use` is the one an agent that calls tools needs. Kindgi picks the model when the turn starts: see [Choose the model an agent uses](https://docs.kindgi.com/v0.1.9/guides/agents/choose-a-model.md).
* **`tools`** are the tools the model may call (below). An agent with `tools: []` only talks.
* **`guardrails`** check the agent's final answer, once per turn, before it's stored. A guardrail whose action is `halt` fails the turn, and the answer is never written to the conversation.
* **`budget`** caps each turn: see [Set an agent's budget](https://docs.kindgi.com/v0.1.9/guides/agents/budgets.md).

In TypeScript, `tools`, `retrieval` and `guardrails` are required; pass `[]` for none.

## Run it

- **TypeScript**

  ```sh
  pnpm exec kindgi runs start --agent=acme.order-desk --input='{"userMessage":"Where is my order A-1001?"}'
  ```

- **Python**

  ```sh
  uv run kindgi runs start --agent=acme.order-desk --input='{"userMessage":"Where is my order A-1001?"}'
  ```

- **Java**

  ```sh
  ./kindgiw runs start --agent=acme.order-desk --input='{"userMessage":"Where is my order A-1001?"}'
  ```

- **Scala**

  ```sh
  ./kindgiw runs start --agent=acme.order-desk --input='{"userMessage":"Where is my order A-1001?"}'
  ```

```json excerpt
{
  "id": "f3835200-e4c6-4579-a3fc-4b70f7444333",
  …
  "status": "completed",
  …
  "output": {
    …
    "usage": { "steps": 2, "durationMs": 3701, "promptTokens": 1119, "totalCostUsd": 0.004328, "completionTokens": 209 },
    "appended": [
      { "role": "user", "content": "Where is my order A-1001?", "sequence": 0, … },
      { "role": "agent", "actor": "acme.order-desk",
        "content": { "text": "", "toolCalls": [{ "id": "toolu_01…", "name": "acme.lookup-order", "arguments": { "orderId": "A-1001" } }] },
        "sequence": 1, … },
      { "role": "tool", "content": { "eta": "2026-10-06", "status": "shipped", "orderId": "A-1001" }, "sequence": 2, … },
      { "role": "agent", "actor": "acme.order-desk",
        "content": "Order A-1001 shows as shipped, and its expected delivery date was October 6, 2026. That date has passed and I can't see any newer tracking details, so the order is running late.", "sequence": 3, … }
    ],
    "provider": { "id": "anthropic", "model": "claude-sonnet-5-5" },
    "response": { "role": "agent", "content": "Order A-1001 shows as shipped, and its expected delivery date was October 6, 2026. That date has passed and I can't see any newer tracking details, so the order is running late.", … },
    …
    "turnNumber": 1,
    "violations": [],
    "conversationId": "936af311-f1c8-4260-a1ed-973876b4ac71"
  }
}
```

The turn took two steps: the model asked for `acme.lookup-order`, the tool ran, and the model answered with its result. `response.content` is the answer; `appended` is everything the turn added to its conversation. The rest of the result is on [Give an agent its input](https://docs.kindgi.com/v0.1.9/guides/agents/give-an-agent-input.md#what-a-turn-returns). The instructions give the model the date of the turn, so it reads the tool's `eta` against it: this run was on 2026-10-08, after the order's expected date.

Edit the file and save: `kindgi dev` reloads it, and the next run uses it.

## Instructions are a template

`{{ today }}` above is filled in when the turn starts. The template language is Liquid, so tags such as `{% if %}` work too. Partials (`{% include %}`, `{% render %}`, `{% layout %}`) don't: an agent or prompt block whose template uses one is refused with "templates can't use include, render or layout". To write `{{` or `{%` as text, put it in `{% raw %}…{% endraw %}`. These variables are always there:

| Variable                                  | Example                                  |
| ----------------------------------------- | ---------------------------------------- |
| `today`                                   | `2026-10-03`                             |
| `now`                                     | `2026-10-03T20:24:43.576Z`               |
| `agent.id`, `agent.name`, `agent.version` | `acme.order-desk`, `Order desk`, `0.1.0` |
| `conversation.id`, `conversation.turn`    | `da5ea34a-…`, `1`                        |

Your own variables are the agent's `parameters`, and an agent that runs as a flow step can read the step's input as `{{ input.* }}`: both are on [Give an agent its input](https://docs.kindgi.com/v0.1.9/guides/agents/give-an-agent-input.md). A parameter can't take a name the turn fills in (`today`, `now`, `agent`, `conversation`, `input`, `settings`): defining the agent refuses it.

Rendering is strict. A variable that is neither built in nor declared fails the turn before the model is called:

```text
Error [model-invocation-failed]: Prompt render failed: Template references an unresolved variable: customer
```

The rendered instructions are in the run's journal (`kindgi runs journal <run-id>`), as the output of its `render-prompt` step.

## Tools and their versions

In TypeScript, each tool is an `{ id, version }` reference, and `version` is a semver range: `'^0.1.0'` takes the highest registered version from `0.1.0` up to, not including, `0.2.0`. In Python, pass the tool itself to pin it to its version, or the same `{"id": …, "version": …}` reference for a range. In Java and Scala, `.tool(LookupOrder.TOOL)` pins the tool's version, and `.tool("acme.lookup-order", "^0.1.0")` takes a range.

Under `kindgi dev`, the range is resolved when each turn starts. A published agent version resolves it once, when it's published, and keeps that version: see [Agent and tool versions](https://docs.kindgi.com/v0.1.9/guides/agents/agent-and-tool-versions.md). When no registered version is in the range, the turn fails:

```text
Error [tool-version-unresolvable]: Tool "acme.lookup-order" has no version satisfying "^0.2.0" (available: 0.1.0)
```

A tool given as a plain string is refused before anything runs: TypeScript reports `Type 'string' is not assignable to type 'ToolRef'`, and the Python index reports the file:

```sh
uv run python -m kindgi.pack index --pack-dir .
```

```json excerpt
{
  …
  "fileErrors": [
    {
      "code": "manifest-validation-failed",
      "message": "agents/order_desk.py: agent 'acme.order-desk': each tool is a Tool or an {id, version} ref, got 'acme.lookup-order'",
      "filePath": "agents/order_desk.py"
    }
  ]
}
```

## Guardrails

A guardrail the agent names must be registered: `kindgi dev` registers the pack's own. An unknown id fails the turn before the model is called:

```text
Error [unresolved-guardrail]: Agent "acme.order-desk" references guardrails not in the registry: acme.no-refunds
```

In Python, passing the guardrail object you import (as above) keeps the id right.

## When a file doesn't load

`kindgi dev` keeps serving the rest of the pack and names the file:

```text
⚠ loaded 12 of 12 in 1406ms — 4 tools, 1 guardrails, 5 agents, 2 flows
  ✗ indexer: agents/order-desk/index.ts [file-import-failed] Failed to import agents/order-desk/index.ts: Error: Agent "acme.order-desk" is invalid (1 issue)
```

In a Python pack, `uv run python -m kindgi.pack index --pack-dir .` reports a file that fails to load without the runtime: a bare-string tool (above), or a version that isn't an exact semver (`version must be an exact semver, got '0.1'`).

In a Java or Scala pack, the compiler finds most mistakes first: `kindgi dev` shows its error with the file and line, and keeps the last good code running.
