# Agents

`gizai:agent` runs a language model in a loop. The model writes text or calls tools; each call runs in your program and
its result goes back to the model, until the model answers. A tool is a Gizlang function with a record for its input and
a record for its output, so the model's arguments are checked before your code runs, and a wrong answer is sent back to
the model as an error it can fix.

This guide covers the whole surface: tools, running an agent, its limits and approvals, reasoning effort, caching, the
numbers it reports, and how to test an agent with a scripted model. The model itself is set up in
[AI models](ai.md); the names here are the ones in `gizai:agent`.

## One agent, one tool

```gizlang
import {System} from "gizlang:system";
import * as Agent from "gizai:agent";

/** The weather of a city. */
type Weather = {
    /** The city's name in English. */
    city: string;
};
type Forecast = { city: string; high: i64; };

function weather(w: Weather): Forecast throws string {
    return { city: w.city, high: 24 };
}

function main(sys: System): void {
    const model = Agent.endpoint(sys)!;
    let a = Agent.agent(model, "You answer questions about the weather.", [
        Agent.readTool<Weather, Forecast>("weather", weather)
    ]);
    print(Agent.run(sys, a, "Is it warm in Seoul today?")!);
}
```

The tool's description is the doc comment of its input record, and each field's doc describes that field. Nothing else
is needed to describe a tool to the model.

## Tools

`Agent.tool<I, O>(name, run)` makes a tool from `run`, a function from the input record `I` to `O` that throws a `string`.

- An `Err` goes back to the model as the text of the call's error. The run goes on.
- `Agent.readTool<I, O>(name, run)` makes a tool that only reads. A call of it runs without approval (see [Approvals](#approvals)).
  `Agent.tool` makes a tool that may write; each of its calls is decided by `approve`.
- A tool that writes but must see the calls before it finish first is `serial`: it runs after the calls before it in the
  same turn and before the calls after it. Other calls of a turn run at once.
- `Agent.schema<T>()` is the JSON Schema of a record, the one a tool sends to the model.

```gizlang
import * as Agent from "gizai:agent";

/** A file to write. */
type Write = {
    /** The path, relative to the project. */
    path: string;
    /** The whole text of the file. */
    text: string;
};
type Written = { bytes: i64; };

function write(w: Write): Written throws string {
    return { bytes: w.text.byteLength };
}

/** The tool that writes a file: it runs alone in its turn, after the reads before it. */
function writer(): Agent.Tool {
    let t = Agent.tool<Write, Written>("write_file", write);
    t.serial = true;
    return t;
}
```

## Running an agent

`Agent.run(sys, a, prompt, each?, files?)` adds the prompt to the agent's conversation, runs the loop, and returns the
model's final answer. `each` hears each piece as it happens; the pieces are the model's text and reasoning, each call,
each result, and how the run ended.

| `Piece.kind` | Its `text` or `call` |
| --- | --- |
| `text` | A part of the model's answer, as it is written |
| `reasoning` | A part of the model's reasoning summary |
| `call` | A call the model made (`call`: its name and JSON arguments) |
| `result` | The text a tool answered, as the model reads it |
| `failed` | The error of a call (the model reads it too) |
| `waiting` | A call left to the person by `approve` (see below) |
| `compacted` | The handoff summary that replaced the conversation (see `compactAt`) |
| `done` | The final answer |
| `stopped` | The text written before `stop` ended the run |
| `error` | Why the run failed |

`PieceKind` is a literal union: the compiler refuses a kind the agent never sends, so a `switch` on it cannot miss a
case that is spelled wrong.

```gizlang
import * as Agent from "gizai:agent";

function show(p: Agent.Piece): void {
    if (p.kind === "text") { print(p.text); }
    if (p.kind === "call") { print(`calling ${p.call.name}`); }
}
```

To ask for a record instead of text, use `Agent.ask<T>(sys, a, prompt)`: the model answers by calling a tool whose input is
`T`, which is read and checked as `T`.

`Agent.resume(sys, a)` goes on with a conversation that is already in `a.history`, for example one kept in a database.
`Agent.title(sys, model, first)` makes a short title for a conversation that starts with `first`.

### Attachments

`files` takes `Agent.Attachment` records (`name`, `type`, `path`). A picture goes to the model as a picture; a document's text
joins the message, and a long one is read in parts with the tool `read_file`.

## Limits and control

An agent has defaults, and every one of them is a field you can set before a run.

| Field | Default | What it bounds |
| --- | --- | --- |
| `rounds` | 16 | Tool rounds in one run |
| `wrapUp` | 0 | The last rounds of a run, in which the model is told to finish what it has |
| `maxTokens` | 0 | Tokens one run may write (0: no limit) |
| `maxMs` | 0 | Milliseconds one run may take (0: no limit) |
| `repeats` | 3 | How many rounds in a row the same call (same tool, same arguments) may be made before it is refused |
| `mostRead` | 0 | Characters of one tool result the model reads (0: all) |
| `compactAt` | 0 | Tokens past which the conversation is replaced by the model's own handoff summary (0: never) |

The last round of a run takes no tool: the model is asked for its answer alone. `wrapUp` tells the model once, when that many
rounds are left, to finish the most important part.

```gizlang
import * as Agent from "gizai:agent";

function fixer(model: Agent.Model, tools: Vec<Agent.Tool>): Agent.Agent {
    let a = Agent.agent(model, "You fix the project in the working folder.", tools);
    a.rounds = 200;
    a.wrapUp = 10;
    a.maxMs = 30 * 60 * 1000;
    a.compactAt = 150000;
    a.mostRead = 16000;
    return a;
}
```

### Stopping and steering

- `stop: () => bool` is asked between pieces and between rounds. When it returns true the run ends and keeps what it wrote,
  with a `stopped` piece. The default never stops.
- `steer: () => Vec<Message>` gives the messages a person sent while the run worked. They are read before each round
  after the first.

### Approvals

`approve: (c: Call) => string` decides each call that is not read-only, before it runs. It returns one of:

- `Agent.RUN`: run the call.
- `Agent.WAIT`: leave the call to the person. The call is not run, a `waiting` piece is sent, and the run ends with one more
  answer that says what is left.
- any other text: refuse the call. That text is the call's error, which the model reads; `Agent.DECLINED` is the text for a
  person's "no".

## Reasoning effort and sampling

An agent's `settings` say how the model is asked: `settings.effort` and `settings.sampling`.

- `effort` is `""` (the default), `"low"`, `"medium"`, `"high"` or `"xhigh"`. A request that names no effort uses the
  default, which is the `AI_EFFORT` environment variable when it is set, and otherwise the model's own default.
- An unknown effort is an error, not the model's default: `parseEffort("extreme")` from `gizlang:openai` fails, and so does
  an `AI_EFFORT` that names no effort.
- `sampling` holds `maxTokens` (the most tokens one turn writes), `temperature`, `topP`, `seed` and `stop` strings. A
  negative temperature or top_p, and a seed of -1, mean the API's default.

```gizlang
import * as Agent from "gizai:agent";

function careful(model: Agent.Model): Agent.Agent {
    let a = Agent.agent(model, "You review code.", []);
    a.settings.effort = "xhigh";
    a.settings.sampling.temperature = 0.0;
    return a;
}
```

### Responses API and caching

With `AI_API=responses`, a model is called through the OpenAI Responses API. Two things follow:

- The model's encrypted reasoning comes back with each tool round, so a reasoning model goes on from where it was instead of
  thinking afresh after every call.
- Each conversation sends a `prompt_cache_key`, a digest of its instructions and first message. Every round of one
  conversation reaches the server that cached its prefix, and the history only grows at its end, so the prefix is reused.

The numbers of a run show how much was read from the cache:

| Field | Meaning |
| --- | --- |
| `a.tokens` | Tokens the runs wrote |
| `a.input` | Tokens the runs read (the prompt) |
| `a.cached` | Of those, the tokens the provider had cached |
| `a.context` | Tokens the conversation holds after the last turn |
| `a.window` | The model's context window in tokens (0: unknown) |

## Models

`Agent.endpoint(sys, model?)` is the model of the OpenAI-compatible API at `AI_URL`, with `AI_API_KEY` as its key and
`AI_MODEL` (or the name you pass) as the model. [AI models](ai.md) covers the other ways to name a model.

Any model is a record with a `name` and a `turn` function, so you can write your own, for example one that routes between
two models. `Agent.scripted(turns)` is a model that replies with the turns you give it: `Agent.says(text)` and
`Agent.calls(calls)` make one turn each.

## Testing an agent without a network

A test gives the agent a scripted model, so it runs without a model API and always gives the same answers.

```gizlang
import {System} from "gizlang:system";
import * as Agent from "gizai:agent";

/** The weather of a city. */
type Weather = { city: string; };
type Forecast = { city: string; high: i64; };

function weather(w: Weather): Forecast throws string { return { city: w.city, high: 24 }; }

function main(sys: System): void {
    const model = Agent.scripted([
        Agent.calls([{ id: "c1", name: "weather", arguments: "{\"city\":\"Seoul\"}" }]),
        Agent.says("It is warm in Seoul."),
    ]);
    let a = Agent.agent(model, "You answer questions about the weather.", [Agent.readTool<Weather, Forecast>("weather", weather)]);
    print(Agent.run(sys, a, "Is it warm in Seoul?")!);
}
```

A scripted run checks the whole loop: the calls a model makes, what a tool answers, the limits, the approvals and the
stop. The tests of the agent in the Gizlang repository do this for each of them.
