# Write a tool

> Write a tool in TypeScript, Python, Java or Scala, try it from a flow, test it, and give it to an agent.

A tool is a function with a typed input and a typed output. This one looks an order up by its id. The orders live in a map here; in your app, the handler calls your own code.

## The tool

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.8/start/coding-agents.md)). For reference, the tool looks like this. This guide calls it `my-pack.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';

  // Stands in for your app's database.
  const ORDERS = new Map([
    ['ord_1001', { status: 'shipped', totalCents: 4200 }],
    ['ord_1002', { status: 'pending', totalCents: 1250 }],
  ]);

  const defined = defineTool({
    id: 'my-pack.lookup-order' as ToolId,
    description: 'Looks up an order by id. Returns its status and total in cents.',
    version: '0.1.0',
    input: z.object({
      orderId: z.string().regex(/^ord_\d+$/),
    }),
    output: z.object({
      orderId: z.string(),
      status: z.enum(['pending', 'shipped', 'delivered']),
      totalCents: z.number().int(),
    }),
    effects: [],
    mutating: false,
    handler: async ({ orderId }, ctx) => {
      const order = ORDERS.get(orderId);
      if (order === undefined) throw new Error(`No order ${orderId}`);
      console.log(`lookup-order ${orderId} for tenant ${ctx.tenantId}`);
      return { orderId, status: order.status as 'pending' | 'shipped', totalCents: order.totalCents };
    },
  });

  if (defined.kind === 'err') {
    throw new Error(`my-pack.lookup-order failed to compile: ${defined.error.message}`);
  }

  export default defined.value;
  ```

  - `input` and `output` are Zod schemas (or JSON Schema). The handler gets the parsed input, with defaults and transforms applied.
  - `defineTool` returns a result instead of throwing. Unwrap it when the file loads, so a bad definition fails at once rather than on the first call.
  - `ctx` carries `tenantId`, `runId`, `requestId` and an `abortSignal` that fires when the call is cancelled. In a run it also carries `projectId`, the run's project, and `orgId`, the project's org when it has one. The runtime sets both from the run, never from the input, so compare an id in the input with them rather than trusting it.

- **Python**

  `tools/lookup_order.py`

  ```python
  from typing import Literal

  from pydantic import BaseModel, Field

  from kindgi import ToolContext, tool

  # Stands in for your app's database.
  ORDERS = {
      "ord_1001": {"status": "shipped", "totalCents": 4200},
      "ord_1002": {"status": "pending", "totalCents": 1250},
  }


  class OrderRef(BaseModel):
      order_id: str = Field(alias="orderId", pattern=r"^ord_\d+$")


  class Order(BaseModel):
      order_id: str = Field(alias="orderId")
      status: Literal["pending", "shipped", "delivered"]
      total_cents: int = Field(alias="totalCents")


  @tool(id="my-pack.lookup-order", mutating=False)
  def lookup_order(ref: OrderRef, ctx: ToolContext) -> Order:
      """Looks up an order by id. Returns its status and total in cents."""
      order = ORDERS.get(ref.order_id)
      if order is None:
          raise LookupError(f"No order {ref.order_id}")
      print(f"lookup-order {ref.order_id} for tenant {ctx.tenant_id}")
      return Order(orderId=ref.order_id, **order)
  ```

  - The first parameter's annotation is the input, the return annotation the output: pydantic models here (a `TypedDict` or a dataclass works too). Field aliases are the names on the wire.
  - The docstring is the description. The version is the pack's, unless you pass `version=`.
  - `ctx` carries `tenant_id`, `run_id`, `request_id` and `cancellation`. In a run it also carries `project_id`, the run's project, and `org_id`, the project's org when it has one (else `None`). The runtime sets both from the run, never from the input. A `def` handler runs in a worker thread, so blocking calls are fine; an `async def` handler runs on the event loop.

- **Java**

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

  ```java
  package mypack.tools;

  import com.fasterxml.jackson.annotation.JsonProperty;
  import com.kindgi.pack.Tool;
  import jakarta.validation.constraints.Pattern;
  import java.util.Map;

  public final class LookupOrder {
    public record Input(@Pattern(regexp = "^ord_\\d+$") String orderId) {}

    public enum Status {
      @JsonProperty("pending") PENDING,
      @JsonProperty("shipped") SHIPPED,
      @JsonProperty("delivered") DELIVERED
    }

    public record Output(String orderId, Status status, long totalCents) {}

    // Stands in for your app's database.
    private static final Map<String, Output> ORDERS = Map.of(
        "ord_1001", new Output("ord_1001", Status.SHIPPED, 4200),
        "ord_1002", new Output("ord_1002", Status.PENDING, 1250));

    public static final Tool<Input, Output> TOOL = Tool.define("my-pack.lookup-order")
        .description("Looks up an order by id. Returns its status and total in cents.")
        .version("0.1.0")
        .input(Input.class)
        .output(Output.class)
        .mutating(false)
        .handler((input, ctx) -> {
          Output order = ORDERS.get(input.orderId());
          if (order == null) throw new IllegalArgumentException("No order " + input.orderId());
          System.out.println("lookup-order " + input.orderId() + " for tenant " + ctx.tenantId());
          return order;
        });

    private LookupOrder() {}
  }
  ```

  - The tool is a `public static final` field of a class under `tools/`: the indexer finds it there. The package comes from the pack's id (`my-pack` → `mypack`).
  - `input` and `output` are records, and their schemas come from the records: a Jakarta Validation constraint such as `@Pattern` becomes its schema keyword, and an enum the list of its JSON values.
  - `ctx` carries `tenantId()`, `runId()`, `requestId()` and `cancellation()`, which fires when the call is cancelled. In a run it also carries `projectId()`, the run's project, and `orgId()`, the project's org when it has one (else `null`). The runtime sets both from the run, never from the input. Each call runs on its own thread, so blocking calls are fine.

- **Scala**

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

  ```scala
  package mypack.tools

  import com.kindgi.pack.scaladsl._
  import jakarta.validation.constraints.Pattern

  object LookupOrder {
    final case class Input(@Pattern(regexp = "^ord_\\d+$") orderId: String)

    enum Status {
      case pending, shipped, delivered
    }

    final case class Output(orderId: String, status: Status, totalCents: Long)

    // Stands in for your app's database.
    private val orders = Map(
      "ord_1001" -> Output("ord_1001", Status.shipped, 4200),
      "ord_1002" -> Output("ord_1002", Status.pending, 1250))

    val tool: Tool[Input, Output] = Tool[Input, Output]("my-pack.lookup-order")
      .description("Looks up an order by id. Returns its status and total in cents.")
      .version("0.1.0")
      .readOnly
      .handler { (in, ctx) =>
        val order = orders.getOrElse(in.orderId, throw new NoSuchElementException(s"No order ${in.orderId}"))
        println(s"lookup-order ${in.orderId} for tenant ${ctx.tenantId}")
        order
      }
  }
  ```

  - The tool is a `val` of an object named like its file, under `tools/`: the indexer finds it there.
  - `Input` and `Output` are case classes, and their schemas come from them: a Jakarta Validation constraint such as `@Pattern` becomes its schema keyword, and a Scala 3 enum's cases are the values its string may take.
  - `ctx` is the same context as in Java: `ctx.tenantId`, `ctx.runId`, `ctx.projectId` and `ctx.orgId` (`null` when the project has no org), set by the runtime from the run.

In every language:

- The id is `<pack-id>.<tool-name>`, in kebab case.
- The description is what a model reads to decide when to call the tool. Say what it does and what it returns.
- `mutating: false` (`.mutating(false)` in Java, `.readOnly` in Scala) declares that the tool only reads. See [Mark a tool read-only](https://docs.kindgi.com/v0.1.8/guides/tools/read-only-tools.md).
- What the handler prints shows in the `kindgi dev` terminal, prefixed `[pack]`.
- Open files, databases and connections inside the handler, on first use, not at the module's top level (in a static field or an object's `val`, in Java and Scala). `kindgi build` loads every tool, guardrail and flow module to index it, so what a module does when it's loaded runs during the build too, inside an image that doesn't have your app's files or services ([Build, sign and push your pack](https://docs.kindgi.com/v0.1.8/deploy/self-host.md#3-build-sign-and-push-your-pack)).

Once the file is saved, `kindgi dev` indexes it and registers the tool; the next call runs the new code.

## Try it from a flow

A flow step can call any tool, which makes a one-step flow the quickest way to run one. Ask your coding agent: "I want a flow that runs the order lookup, so I can try it." For reference, the flow and the command that starts it look like this:

- **TypeScript**

  `flows/check-order/index.ts`

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

  const defined = defineFlow({
    id: 'my-pack.check-order',
    version: '0.1.0',
    name: 'Check order',
    description: 'Looks up one order.',
    nodes: [
      {
        id: 'lookup',
        kind: 'tool',
        ref: 'my-pack.lookup-order',
        inputMapping: { orderId: { path: 'runInput.orderId' } },
      },
    ],
    edges: [
      { id: 'e-start', from: '$start', to: 'lookup' },
      { id: 'e-end', from: 'lookup', to: '$end' },
    ],
  });

  if (defined.kind === 'err') {
    throw new Error(`my-pack.check-order failed to compile: ${defined.error.message}`);
  }

  export default defined.value;
  ```

  ```sh
  pnpm exec kindgi runs start --flow=my-pack.check-order --input='{"orderId":"ord_1001"}'
  ```

- **Python**

  `flows/check_order.py`

  ```python
  from kindgi import Flow

  from ..tools.lookup_order import lookup_order

  check_order = Flow(
      id="my-pack.check-order",
      version="0.1.0",
      name="Check order",
      description="Looks up one order.",
      nodes=[
          {
              "id": "lookup",
              "kind": "tool",
              "ref": lookup_order,
              "inputMapping": {"orderId": {"path": "runInput.orderId"}},
          }
      ],
      edges=[
          {"id": "e-start", "from": "$start", "to": "lookup"},
          {"id": "e-end", "from": "lookup", "to": "$end"},
      ],
  )
  ```

  ```sh
  uv run kindgi runs start --flow=my-pack.check-order --input='{"orderId":"ord_1001"}'
  ```

- **Java**

  `src/main/java/mypack/flows/CheckOrder.java`

  ```java
  package mypack.flows;

  import com.kindgi.pack.Flow;
  import java.util.Map;
  import mypack.tools.LookupOrder;

  public final class CheckOrder {
    public static final Flow FLOW = Flow.define("my-pack.check-order")
        .version("0.1.0")
        .set("name", "Check order")
        .set("description", "Looks up one order.")
        .node(Map.of(
            "id", "lookup",
            "kind", "tool",
            "ref", LookupOrder.TOOL,
            "inputMapping", Map.of("orderId", Map.of("path", "runInput.orderId"))))
        .edge("e-start", "$start", "lookup")
        .edge("e-end", "lookup", "$end")
        .build();

    private CheckOrder() {}
  }
  ```

  ```sh
  ./kindgiw runs start --flow=my-pack.check-order --input='{"orderId":"ord_1001"}'
  ```

- **Scala**

  `src/main/scala/mypack/flows/CheckOrder.scala`

  ```scala
  package mypack.flows

  import com.kindgi.pack.scaladsl._
  import mypack.tools.LookupOrder

  object CheckOrder {
    val flow: Flow = Flow("my-pack.check-order")
      .version("0.1.0")
      .set("name", "Check order")
      .set("description", "Looks up one order.")
      .node(Map(
        "id" -> "lookup",
        "kind" -> "tool",
        "ref" -> LookupOrder.tool,
        "inputMapping" -> Map("orderId" -> Map("path" -> "runInput.orderId"))))
      .edge("e-start", "$start", "lookup")
      .edge("e-end", "lookup", "$end")
      .build()
  }
  ```

  ```sh
  ./kindgiw runs start --flow=my-pack.check-order --input='{"orderId":"ord_1001"}'
  ```

```json excerpt
{
  "id": "1a9707b6-ae88-47b5-a457-eef7dea2d1fc",
  …
  "flowId": "my-pack.check-order",
  "flowVersion": "0.1.0",
  "status": "completed",
  "dryRun": false,
  …
  "output": {
    "status": "shipped",
    "orderId": "ord_1001",
    "totalCents": 4200
  },
  …
}
```

A flow without a declared `output` returns its last step's output. The `kindgi dev` terminal shows the handler's log line:

```text
    [pack] lookup-order ord_1001 for tenant bb22c936-5111-429a-bfce-09650af85492
```

## Log from a tool

`ctx.log` writes a record bound to the call: it carries the run's ids, the tool's id and the runtime's trace id, so it can be found beside the run's own records ([Logs](https://docs.kindgi.com/v0.1.8/deploy/logs.md#your-packs-service)). Fields go in as an object, never in the message. Ask your coding agent: "Log each order my lookup tool looks up." For reference, it adds a line like this to the handler:

- **TypeScript**

  ```ts excerpt
  // in tools/lookup-order/index.ts, in the handler
  ctx.log?.info('looked up order', { orderId, status: order.status });
  ```

  The pack service sets `ctx.log`. A context your own code builds may not have one, so write `ctx.log?.`.

- **Python**

  ```python excerpt
  # in tools/lookup_order.py, in the handler
  ctx.log.info("looked up order", {"orderId": ref.order_id, "status": order["status"]})
  ```

  Fields can also be keywords (`order_id=…`). In a test, `ToolContext.for_test(…)` logs nothing unless you pass it a logger as `log=`.

- **Java**

  ```java excerpt
  // in src/main/java/mypack/tools/LookupOrder.java, in the handler
  ctx.log().info("looked up order", Map.of("orderId", input.orderId(), "status", order.status()));
  ```

  `ctx.log().warn("retrying", e)` logs an exception, under `err`. In a test, `ToolContext.forTest()` logs nothing; `ToolContext.forTest(log)` takes a logger.

- **Scala**

  ```scala excerpt
  // in src/main/scala/mypack/tools/LookupOrder.scala, in the handler
  ctx.log.info("looked up order", "orderId" -> in.orderId, "status" -> order.status)
  ```

  Fields can also be a map. A case class is logged as an object of its fields, so one named like a secret (`apiKey`) is redacted.

Under `kindgi dev`, the record shows as a `[pack]` line. Here from a Java pack:

```text
    [pack] 12:39:41.533 INFO  [pack.tool] looked up order traceId=877833b3f86d26ce95c94bd7b7bea72f spanId=cfe974bcb4284c35 requestId=669b77fc-7ae6-4c48-8002-ebfca944e287 tenantId=cac037c9-c5ff-4f0a-ae85-7a0e5e324bc3 projectId=12a6cc0f-83d8-4739-8223-5f99210c73f0 runId=669b77fc-7ae6-4c48-8002-ebfca944e287 toolId=acme.lookup-order packId=acme artifactVersion=20261009.1 target=tool orderId=o-1001
```

The record's subsystem is `pack.tool`, and its level is the method's: `info`, `warn`, `error`, `debug` or `trace`. Values under secret-looking keys are redacted, but don't log a secret. Printing the context itself (`console.log(ctx)`, `print(ctx)`, `System.out.println(ctx)`) never shows its secrets.

## When a call fails

Kindgi checks the input before your handler runs. An order id that doesn't match the pattern never reaches it:

- **TypeScript**

  ```sh
  pnpm exec kindgi runs start --flow=my-pack.check-order --input='{"orderId":"9999"}'
  ```

- **Python**

  ```sh
  uv run kindgi runs start --flow=my-pack.check-order --input='{"orderId":"9999"}'
  ```

- **Java**

  ```sh
  ./kindgiw runs start --flow=my-pack.check-order --input='{"orderId":"9999"}'
  ```

- **Scala**

  ```sh
  ./kindgiw runs start --flow=my-pack.check-order --input='{"orderId":"9999"}'
  ```

```json excerpt
  "status": "failed",
  "failureMessage": "input-validation-failed: Input for tool \"my-pack.lookup-order\" failed validation",
```

An exception in the handler fails the call with its message:

- **TypeScript**

  ```sh
  pnpm exec kindgi runs start --flow=my-pack.check-order --input='{"orderId":"ord_9999"}'
  ```

- **Python**

  ```sh
  uv run kindgi runs start --flow=my-pack.check-order --input='{"orderId":"ord_9999"}'
  ```

- **Java**

  ```sh
  ./kindgiw runs start --flow=my-pack.check-order --input='{"orderId":"ord_9999"}'
  ```

- **Scala**

  ```sh
  ./kindgiw runs start --flow=my-pack.check-order --input='{"orderId":"ord_9999"}'
  ```

```json excerpt
  "status": "failed",
  "failureMessage": "handler-error: Tool \"my-pack.lookup-order\" handler threw: handler-throw: Handler for tool \"my-pack.lookup-order\" threw: Error: No order ord_9999",
```

What the handler returns is checked against the output schema too; a return value that doesn't fit fails with `output-validation-failed`.

## Make a side effect happen once

A run's step can run more than once: resumed after an approval, retried after a failure, or run again when the runtime restarted while it ran. A tool that changes something (a refund, an email, a payment) would then do it twice.

`ctx.idempotencyKey` is the same every time the same call runs, and different for every other call. Pass it to the system you write to: one that takes an idempotency key keeps the first request and answers a repeat with its result. Ask your coding agent: "Make sure my refund tool never issues the same refund twice." For reference, the handler passes the key on like this:

- **TypeScript**

  ```ts excerpt
  // in tools/issue-refund/index.ts, in the handler
  const res = await fetch('https://payments.acme.example/refunds', {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      ...(ctx.idempotencyKey !== undefined && { 'Idempotency-Key': ctx.idempotencyKey }),
    },
    body: JSON.stringify({ orderId: input.orderId, amount: input.amount }),
  });
  ```

- **Python**

  ```python excerpt
  # in tools/issue_refund.py, in the handler
  headers = {"Idempotency-Key": ctx.idempotency_key} if ctx.idempotency_key else {}
  res = httpx.post(
      "https://payments.acme.example/refunds",
      json={"orderId": ref.order_id, "amount": ref.amount},
      headers=headers,
  )
  ```

- **Java**

  ```java excerpt
  // in src/main/java/mypack/tools/IssueRefund.java, in the handler
  HttpRequest.Builder request = HttpRequest.newBuilder(URI.create("https://payments.acme.example/refunds"))
      .POST(HttpRequest.BodyPublishers.ofString(body));
  if (ctx.idempotencyKey() != null) {
    request.header("Idempotency-Key", ctx.idempotencyKey());
  }
  ```

- **Scala**

  ```scala excerpt
  // in src/main/scala/mypack/tools/IssueRefund.scala, in the handler
  val request = HttpRequest.newBuilder(URI.create("https://payments.acme.example/refunds"))
    .POST(HttpRequest.BodyPublishers.ofString(body))
  Option(ctx.idempotencyKey).foreach(key => request.header("Idempotency-Key", key))
  ```

When the system has no such key, keep it with what you wrote (in a column that must be unique) and look for it before you write. The key is a UUID.

- **`requestId` won't do.** A model's call id is only unique within one of its answers: two turns of a loop can share one.
- **It can be absent** outside a run: a unit test passes its own, or none. Check for it, as above; without one, the call can't be deduped on it.

## Check the run's org

When your app keeps several customers' data, give each customer its own Kindgi org and project, and start that customer's runs there. A tool then checks what it reads against the run's org, `ctx.orgId`, which the runtime sets from the run. An org id in the input is something a model or a caller wrote, so it proves nothing. Ask your coding agent: "My order lookup tool should only find orders from the run's own org." For reference, the tool checks it like this:

- **TypeScript**

  ```ts excerpt
  // in tools/lookup-order/index.ts
  // Stands in for your app's database. Each order belongs to one customer's org.
  const ORDERS = new Map([
    ['ord_1001', { orgId: '1154e250-2a07-491e-b00a-3675b411c411', status: 'shipped', totalCents: 4200 }],
    ['ord_1002', { orgId: 'feb005e6-6798-47eb-a87a-8feed584d37b', status: 'pending', totalCents: 1250 }],
  ]);

  // …
    handler: async ({ orderId }, ctx) => {
      const order = ORDERS.get(orderId);
      // Another org's order is as missing as one that doesn't exist.
      if (order === undefined || order.orgId !== ctx.orgId) throw new Error(`No order ${orderId}`);
      return { orderId, status: order.status as 'pending' | 'shipped', totalCents: order.totalCents };
    },
  ```

- **Python**

  ```python excerpt
  # in tools/lookup_order.py
  # Stands in for your app's database. Each order belongs to one customer's org.
  ORDERS = {
      "ord_1001": {"org_id": "1154e250-2a07-491e-b00a-3675b411c411", "status": "shipped", "totalCents": 4200},
      "ord_1002": {"org_id": "feb005e6-6798-47eb-a87a-8feed584d37b", "status": "pending", "totalCents": 1250},
  }


  @tool(id="my-pack.lookup-order", mutating=False)
  def lookup_order(ref: OrderRef, ctx: ToolContext) -> Order:
      """Looks up an order by id. Returns its status and total in cents."""
      order = ORDERS.get(ref.order_id)
      # Another org's order is as missing as one that doesn't exist.
      if order is None or order["org_id"] != ctx.org_id:
          raise LookupError(f"No order {ref.order_id}")
      return Order(orderId=ref.order_id, status=order["status"], totalCents=order["totalCents"])
  ```

- **Java**

  ```java excerpt
  // in src/main/java/mypack/tools/LookupOrder.java
  // Stands in for your app's database. Each order belongs to one customer's org.
  record Stored(String orgId, Output order) {}

  private static final Map<String, Stored> ORDERS = Map.of(
      "ord_1001", new Stored("1154e250-2a07-491e-b00a-3675b411c411", new Output("ord_1001", Status.SHIPPED, 4200)),
      "ord_1002", new Stored("feb005e6-6798-47eb-a87a-8feed584d37b", new Output("ord_1002", Status.PENDING, 1250)));

  // …
      .handler((input, ctx) -> {
        Stored stored = ORDERS.get(input.orderId());
        // Another org's order is as missing as one that doesn't exist.
        if (stored == null || !stored.orgId().equals(ctx.orgId())) {
          throw new IllegalArgumentException("No order " + input.orderId());
        }
        return stored.order();
      });
  ```

- **Scala**

  ```scala excerpt
  // in src/main/scala/mypack/tools/LookupOrder.scala
  // Stands in for your app's database. Each order belongs to one customer's org.
  private val orders = Map(
    "ord_1001" -> ("1154e250-2a07-491e-b00a-3675b411c411", Output("ord_1001", Status.shipped, 4200)),
    "ord_1002" -> ("feb005e6-6798-47eb-a87a-8feed584d37b", Output("ord_1002", Status.pending, 1250)))

  // …
      .handler { (in, ctx) =>
        orders.get(in.orderId) match {
          // Another org's order is as missing as one that doesn't exist.
          case Some((orgId, order)) if orgId == ctx.orgId => order
          case _ => throw new NoSuchElementException(s"No order ${in.orderId}")
        }
      }
  ```

A run in the first org's project finds `ord_1001`. Asking it for the second org's order fails as if the order didn't exist:

```json excerpt
  "status": "failed",
  "failureMessage": "handler-error: Tool \"my-pack.lookup-order\" handler threw: handler-throw: Handler for tool \"my-pack.lookup-order\" threw: Error: No order ord_1002",
```

A project with no org has no `orgId` (`None` in Python, `null` in Java and Scala), so in it this tool finds no order at all. Start a run in a project with `projectId` on `runs.start`.

## Test it

Ask your coding agent: "Write tests for my order lookup tool." For reference, the tests look like this, with the command that runs them:

- **TypeScript**

  `invokeTool` calls a tool the way the runtime does: input checked, handler called, output checked. It returns a result rather than throwing. `toolContextForTest()`, also from `@kindgi/sdk/define`, builds a context like the one below and takes what to change; for a tool with env values, `invokeToolForTest` decides them as a run does ([Give a tool env values](https://docs.kindgi.com/v0.1.8/guides/tools/give-a-tool-env-values.md#test-it)).

  `tools/lookup-order/index.test.ts`

  ```ts
  import { invokeTool } from '@kindgi/sdk/define';
  import type { TenantId } from '@kindgi/sdk/types';
  import { expect, test } from 'vitest';

  import lookupOrder from './index.js';

  const ctx = {
    tenantId: 'test-tenant' as TenantId,
    abortSignal: new AbortController().signal,
  };

  test('finds a shipped order', async () => {
    const result = await invokeTool(lookupOrder, { orderId: 'ord_1001' }, ctx);
    expect(result).toEqual({
      kind: 'ok',
      value: { orderId: 'ord_1001', status: 'shipped', totalCents: 4200 },
    });
  });

  test('rejects a malformed id before the handler runs', async () => {
    const result = await invokeTool(lookupOrder, { orderId: '1001' }, ctx);
    expect(result.kind === 'err' && result.error.code).toBe('input-validation-failed');
  });
  ```

  ```sh
  pnpm exec kindgi test
  ```

  ```text excerpt
   ✓ tools/lookup-order/index.test.ts (2 tests) 16ms
  …
        Tests  4 passed (4)
  ```

- **Python**

  A tool is still a function: call it with its input model and a test context.

  `tests/test_lookup_order.py`

  ```python
  import pytest

  from kindgi import ToolContext
  from tools.lookup_order import OrderRef, lookup_order


  def test_finds_a_shipped_order():
      order = lookup_order(OrderRef(orderId="ord_1001"), ToolContext.for_test())
      assert order.status == "shipped"
      assert order.total_cents == 4200


  def test_an_unknown_order_raises():
      with pytest.raises(LookupError):
          lookup_order(OrderRef(orderId="ord_9999"), ToolContext.for_test())
  ```

  ```sh
  uv run pytest
  ```

  ```text
  .....                                                                    [100%]
  5 passed in 0.10s
  ```

  Test files aren't indexed: they never become tools.

- **Java**

  `TOOL.call` runs the handler with an input and a context, as a unit test wants it. `ToolContext.forTest()` is a context with a test tenant and run, whose logger writes nothing. The pack service checks the input and the output against their schemas on every call; `call` doesn't.

  `src/test/java/mypack/tools/LookupOrderTest.java`

  ```java
  package mypack.tools;

  import static org.junit.jupiter.api.Assertions.assertEquals;
  import static org.junit.jupiter.api.Assertions.assertThrows;

  import com.kindgi.pack.ToolContext;
  import org.junit.jupiter.api.Test;

  class LookupOrderTest {
    @Test
    void findsAShippedOrder() throws Exception {
      LookupOrder.Output order = LookupOrder.TOOL.call(new LookupOrder.Input("ord_1001"), ToolContext.forTest());
      assertEquals(LookupOrder.Status.SHIPPED, order.status());
      assertEquals(4200, order.totalCents());
    }

    @Test
    void anUnknownOrderThrows() {
      assertThrows(IllegalArgumentException.class,
          () -> LookupOrder.TOOL.call(new LookupOrder.Input("ord_9999"), ToolContext.forTest()));
    }
  }
  ```

  ```sh
  ./mvnw test
  ```

  ```text excerpt
  [INFO] Running mypack.tools.LookupOrderTest
  lookup-order ord_1001 for tenant t-test
  [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.091 s -- in mypack.tools.LookupOrderTest
  …
  [INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
  [INFO]
  [INFO] ------------------------------------------------------------------------
  [INFO] BUILD SUCCESS
  ```

  Tests (`src/test/`, `*Test.java`) aren't indexed: they never become tools.

- **Scala**

  `tool.call` runs the handler, as in Java: the schema checks are the pack service's.

  `src/test/scala/mypack/tools/LookupOrderSuite.scala`

  ```scala
  package mypack.tools

  import com.kindgi.pack.ToolContext

  class LookupOrderSuite extends munit.FunSuite {
    test("finds a shipped order") {
      val order = LookupOrder.tool.call(LookupOrder.Input("ord_1001"), ToolContext.forTest())
      assertEquals(order.status, LookupOrder.Status.shipped)
      assertEquals(order.totalCents, 4200L)
    }

    test("an unknown order throws") {
      intercept[NoSuchElementException] {
        LookupOrder.tool.call(LookupOrder.Input("ord_9999"), ToolContext.forTest())
      }
    }
  }
  ```

  ```sh
  sbt test
  ```

  ```text excerpt
  lookup-order ord_1001 for tenant t-test
  …
  Test run mypack.tools.LookupOrderSuite finished: 0 failed, 0 ignored, 2 total 0.095s
  …
  [info] Passed: Total 5, Failed 0, Errors 0, Passed 5
  ```

  Tests (`src/test/`) aren't indexed: they never become tools.

## Give it to an agent

An agent lists the tools it may call. Ask your coding agent: "Let my agent use the order lookup tool." For reference, it adds the tool to the agent's list:

- **TypeScript**

  ```ts excerpt
  // in agents/order-desk/index.ts
  tools: [{ id: 'my-pack.lookup-order' as ToolId, version: '^0.1.0' }],
  ```

  The version is a range: a turn uses the highest registered version that matches it.

- **Python**

  ```python excerpt
  # in agents/order_desk.py
  tools=[lookup_order],
  ```

  The `Tool` object pins its version. A ref with a range works too: `{"id": "my-pack.lookup-order", "version": "^0.1.0"}`.

- **Java**

  ```java excerpt
  // in src/main/java/mypack/agents/OrderDesk.java
  .tool(LookupOrder.TOOL)
  ```

  The tool itself pins its version. A ref with a range works too: `.tool("my-pack.lookup-order", "^0.1.0")`.

- **Scala**

  ```scala excerpt
  // in src/main/scala/mypack/agents/OrderDesk.scala
  .tool(LookupOrder.tool)
  ```

  The tool itself pins its version. A ref with a range works too: `.tool("my-pack.lookup-order", "^0.1.0")`.

With a model that can call tools ([Models](https://docs.kindgi.com/v0.1.8/guides/models.md)), the agent's turn shows the call and its result. Here `my-pack.order-desk`, an agent with this tool, answers with Llama 3.1 on Ollama ([registered as an OpenAI-compatible endpoint](https://docs.kindgi.com/v0.1.8/guides/models/openai-compatible.md)):

- **TypeScript**

  ```sh
  pnpm exec kindgi runs start --agent=my-pack.order-desk --input='{"userMessage":"What is the status of order ord_1002?"}'
  ```

- **Python**

  ```sh
  uv run kindgi runs start --agent=my-pack.order-desk --input='{"userMessage":"What is the status of order ord_1002?"}'
  ```

- **Java**

  ```sh
  ./kindgiw runs start --agent=my-pack.order-desk --input='{"userMessage":"What is the status of order ord_1002?"}'
  ```

- **Scala**

  ```sh
  ./kindgiw runs start --agent=my-pack.order-desk --input='{"userMessage":"What is the status of order ord_1002?"}'
  ```

```json excerpt
"appended": [
  { "role": "user", "content": "What is the status of order ord_1002?", … },
  {
    "role": "agent",
    "content": {
      "text": "",
      "toolCalls": [{ "id": "call_wzydfzdg", "name": "my-pack.lookup-order", "arguments": { "orderId": "ord_1002" } }]
    },
    …
  },
  {
    "role": "tool",
    "content": { "status": "pending", "orderId": "ord_1002", "totalCents": 1250 },
    …
  },
  {
    "role": "agent",
    "content": "The status of order ord_1002 is pending, with a total amount of 12.50 dollars in cents.",
    …
  }
],
```

`dev-echo`, the stand-in a new pack answers with, always calls an agent's first tool with `{"message": …}`, so try a tool with any other input from a flow, as above.

[Write an agent](https://docs.kindgi.com/v0.1.8/guides/agents/write-an-agent.md) covers the rest of the agent file.
