# AGENTO configuration reference

Attach this file with api-context.md and the config-generation prompt. Public package: `agento-runtime@1.1.0`. The configuration format remains `version: "2"`, independently of the npm package version. Relative links refer to the website; core chapter text is included below.

---

# Connect one endpoint from your existing API

If your API already has OpenAPI, use [the OpenAPI generator](/docs/openapi) as an alternative config-authoring path when your installed release includes it. Review the result before continuing with runtime integration.

You already have a working API. Start by making AGENTO call one operation you understand, then add natural-language access to that same operation. You do not need a mock server or a replacement backend.

**You will produce three things:** a reusable `api-context.md` describing your API, a `config.yml` containing one tool, and a real request whose result you can compare with a direct API call.

Using a model to help prepare the files is optional. Running the first tool does not require a model: first verify the HTTP integration, then introduce conversation.

## 1. Choose an operation you can already call

Choose a read-only endpoint, such as searching services or listing products. Call it with your usual HTTP client and keep the successful request and a sanitized response. Start with an operation that does not create, charge, update, or delete anything.

Record its method, complete URL, required fields and their locations, and authentication scheme. These facts come from **your API**. Do not change the API to fit the names in this tutorial.

We will explain the mapping using `GET /services?q=hair`. This is an illustrative contract, not a hosted demo. Your API might use `/products?search=phone`; preserve that contract when adapting the example.

## 2. Create a portable API context document

If you already know the contract, write it down yourself and continue. To use a model, provide your OpenAPI document, endpoint documentation, or route definitions with sanitized request and response examples.

[Open the API-analysis prompt](/examples/api-onboarding/analyze-api.en.md) and send it with those sources to any model. It asks for `api-context.md`, not speculative configuration based on a project name.

The resulting document should explain the first operation, each request field's type and location, authentication, response structure, documented errors, and where each value originates. User input, trusted server context, constants, and IDs obtained from another endpoint are different sources.

**Review the document.** Ask where a required field or constraint came from. One sample containing `id` does not prove that the field always exists or that another operation accepts it. Keep unknown details marked `UNKNOWN` until you resolve them.

You can also fill in the [api-context.md template](/examples/api-onboarding/api-context.en.md) manually.

Save the reviewed document with your project, without secrets. You can give it to another model later without repeating this conversation. AGENTO does not load Markdown; this document helps you and your tools author the YAML.

## 3. Generate or write one tool, then understand it

Use the explanation below to write the configuration yourself. Alternatively, give a model your reviewed `api-context.md`, the [configuration-generation prompt](/examples/api-onboarding/generate-config.en.md), and the [portable AGENTO reference](/examples/api-onboarding/agento-reference.en.md). It includes the core configuration chapters so a new model conversation has the required library context.

Ask for **a complete file with one tool**. Converting every endpoint at once makes it harder to identify why the first call fails.

If your API matches the public search contract above, the starting configuration is:

```yaml
version: "2"

assistant:
  name: Agento #Your agent's name
  language: auto
  timezone: UTC
  system_prompt: |
     Help users find and book appointments using the available tools.
      Do not invent availability or claim a booking before it completes.


models:
  routing:
    provider: openai
    model: gpt-5.6-luna
    api_key: $ENV:OPENAI_API_KEY
    temperature: 0

tools:
  - id: search-services
    tool:
      description: Search available services by the user's search phrase.
      input_schema:
        type: object
        properties:
          query:
            type: string
            minLength: 1
        required: [query]
    request:
      method: GET
      url: https://YOUR_API_HOST/services
      auth:
        type: none
      map:
        query:
          q: $.query
```

Replace the host, path, fields, and authentication with your actual contract before running it. `YOUR_API_HOST` is deliberately a placeholder.

Read the file from the outside in. `version: "2"` selects the AGENTO configuration format, not your API version. `tools` is a list of operations. Its first item has an internal identifier, `search-services`, which we will use in `invoke()`. The identifier need not match your backend route name.

The `tool` block describes the operation and its accepted inputs. `description` will help the model decide when to use it after conversation is enabled. It does not configure an HTTP request.

`input_schema` requires an object containing a non-empty string named `query`. This is the tool's input contract. We chose a readable name for it; the API still expects `q`.

`request.method` and `request.url` define the actual HTTP operation. `auth.type: none` is appropriate only for a public endpoint. Editing the tool description does not change that URL or method.

### Mapping Tool Inputs to the API

`request.map.query` connects the tool inputs to the query parameters expected by the API.

```yaml
q: $.query
```

This means:

- `q` is the parameter name expected by the API.
- `$.query` is the value taken from the tool inputs.

For example, if the tool input is:

```json
{ "query": "hair" }
```

AGENTO will send:

```http
GET /services?q=hair
```

In short: **the left side is the API parameter name, and the right side is where its value comes from in the tool inputs.**

We have not added a model, retries, or an output schema. First verify the real request. Then use the verified response contract to add [response validation](/docs/responses).

## 4. Configure Authentication

Authentication is defined per tool inside `request.auth`.

For a public endpoint:

```yaml
auth:
  type: none
```

No user token is required.

If the endpoint must run with the permissions of the current user:

```yaml
auth:
  type: session
```

In this case, the token is **not stored in `config.yml`**. Your application passes the current user's Access Token at runtime:

```js
{
  sessionId: `user:${user.id}`,
  auth: {
    token: user.accessToken,
  }
}
```

These two values serve different purposes:

```text
sessionId   → identifies the user's AGENTO session
auth.token  → authenticates that user with your API
```

`user.id` and `user.accessToken` come from your own application and authentication system. AGENTO does not log users in or generate Access Tokens.

For example, in a web application they may come from your authenticated request:

```js
const user = req.user;
```

and then be passed to AGENTO:

```js
const request = {
  sessionId: `user:${user.id}`,
  auth: {
    token: user.accessToken,
  },
};
```

Use a stable `sessionId` for the same conversation or user when you want AGENTO to keep the session context.

If the selected tool uses `auth.type: none`, you do not need to provide `auth.token`.

### Service API Keys

Some APIs use a shared credential that belongs to your application rather than to an individual user.

For that case:

```yaml
auth:
  type: api-key
  env: $ENV:SERVICE_KEY
  header: X-API-Key
```

`SERVICE_KEY` is the name of the environment variable containing the credential. A `$ENV:` prefix indicates that AGENTO should read it from the environment file.

The distinction is simple:

```text
session   → dynamic credential for the current user
api-key   → static credential for the application/service
none      → no authentication
```

---

## 5. Send a Message to AGENTO

Create the Runtime once when your application starts:

```js
import { AgentRuntime } from "agento-runtime";

const runtime = await AgentRuntime.create({
  configPath: "./config.yml",
  presentation: "raw",
});
```

Reuse that Runtime for incoming requests instead of creating a new one for every message.

### Let AGENTO Choose the Tool

When an AI provider is configured, use `chat()` for normal Agent behavior:

```js
const result = await runtime.chat({
  sessionId: `user:${user.id}`,
  auth: {
    token: user.accessToken,
  },
  message: "Find hair services",
});
```

You only provide the user's message.

AGENTO handles the rest:

```text
User Message
     ↓
Understand Request
     ↓
Select Tool
     ↓
Collect Inputs
     ↓
Execute API Request
```

You do not need to manually specify `search-services`.

If the selected tool uses:

```yaml
auth:
  type: session
```

AGENTO uses the `auth.token` supplied with the current request when calling your API.

For public tools, the same call can simply be:

```js
const result = await runtime.chat({
  sessionId: `user:${user.id}`,
  message: "Find hair services",
});
```

### Direct Tool Execution

Sometimes your application already knows exactly which tool should run. In that case, use `invoke()`:

```js
const result = await runtime.invoke({
  sessionId: `user:${user.id}`,
  auth: {
    token: user.accessToken,
  },
  tool: "search-services",
  arguments: {
    query: "hair",
  },
});
```

`invoke()` skips AI tool selection and executes the specified tool directly.


## 6. Interpret the result

For `completed`, inspect `result.data` and compare it with the direct API call using the same values. An empty list can be a valid success.

For `needs_input`, inspect `missing` and provide the required field. Remove `query` from the input file to observe this behavior: an incomplete search should not reach the API.

For `error`, inspect `error.code` and `error.message`. A missing argument differs from an invalid one: `{ "query": 15 }` violates the string schema. `AUTH_REJECTED` calls for checking credentials, not renaming the argument.

If `create()` rejects before a result exists, fix the configuration-loading or validation error first. An invalid URL and a missing service-key environment variable are startup issues.

`needs_selection` and `needs_confirmation` require user interaction. This minimal read tool introduces neither. If you add those policies later, do not automatically select or confirm in the runner; follow the [result-handling guide](/docs/runtime-flow).

## 7. Continue from a verified integration

This quick start is complete when the tool sends the expected request to your API, you can explain where every value came from, and you understand the resulting status.

Continue with [understanding config.yml](/docs/configuration), [tool inputs](/docs/tools), [mapping](/docs/mapping), and [responses](/docs/responses). Then add a [model](/docs/providers) to turn a user's message into a call to the same working tool.

---

# Understand how config.yml controls a tool

You can write config by hand or use [the OpenAPI generator](/docs/openapi) in a release that includes it. Generated config uses the same native format; review it and add the settings your application needs.

After the [quick start](/docs/quick-start), you have a request working against your API. This chapter explains where configuration belongs, why you would add it, and how to observe its effect.

The runtime loads configuration at creation and validates executable rules. It is not a general prompt that leaves the model to decide how HTTP should work.

## Answer three questions before adding options

**What operation may the user request?** This determines the tool and its description. Searching for services and booking a slot are distinct operations.

**What inputs are acceptable?** This determines `tool.input_schema`. A search might require a string named `query`. A booking may require a date and a slot identifier. A required identifier is not necessarily something the user knows; dependencies can supply it later.

**How does the API accept those values?** This determines `request` and `request.map`. Declaring `query` does not automatically send an API parameter named `q`.

```text
arguments.query = "hair"
        |
        v
input_schema: accept a non-empty string
        |
        v
request.map.query: q <- $.query
        |
        v
GET /services?q=hair
```

A number fails the string schema. A valid argument without a mapping is not automatically sent. A correct mapping with a wrong URL still calls the wrong location. Identify the failing layer before editing descriptions.

## Know the levels of the file

Root settings apply to the runtime. Each item in `tools` describes one operation. Within that item, `tool` describes inputs and `request` configures HTTP. `response`, `depends_on`, and `behavior` are siblings of those blocks when needed.

This is a placement diagram, not runnable YAML:

```text
config.yml
├── version
├── models                  used by chat
├── assistant               conversation guidance
├── session                 conversation state limits
└── tools
    └── one tool
        ├── id
        ├── tool
        │   ├── description
        │   └── input_schema
        ├── request
        │   ├── method / url / auth
        │   └── map
        ├── response
        ├── depends_on
        └── behavior
```

YAML indentation determines structure. `models` does not belong inside a tool, and `input_schema` belongs inside `tool`. When a familiar key is rejected, check its placement.

The permitted root keys are `version`, `assistant`, `routing`, `session`, `rate_limits`, `models`, and `tools`. Version is the string `"2"`; the tools list must not be empty.

## Add each section for a concrete reason

After the request works, add [response validation](/docs/responses) from a verified contract. HTTP 200 can then fail validation if the returned shape violates what your application expects.

To accept user messages, configure [models.routing](/docs/providers) and use `chat()`. This enables tool selection and input extraction; it is not needed just to call a known tool through `invoke()`.

When an operation needs an ID the user cannot supply, configure a [dependency](/docs/dependencies). Multiple lookup results may produce `needs_selection`: the runtime is waiting for a choice.

For writes, configure [effect and confirmation policies](/docs/effects). Expect `needs_confirmation` before execution. Merely changing GET to POST is insufficient.

Before integrating multiple users and workers, address [authentication](/docs/authentication), [sessions](/docs/sessions), and [HTTP failures](/docs/http). More options do not automatically improve a tool.

## Separate validation from integration testing

Configuration validation rejects unknown keys, duplicate IDs, mappings to undeclared inputs, and references to missing tools. It cannot prove your API still serves the endpoint or that a credential has permission.

Successful runtime creation checks configuration rules. A real tool invocation checks the API integration. A conversation test then checks whether language leads to the correct tool.

**On your first read, continue to [tool inputs](/docs/tools) now.** The remaining sections are a root-settings reference to revisit when tuning conversation, sessions, or usage limits.

## Assistant: language and conversation, not execution permission

```yaml
assistant:
  name: Booking assistant
  language: auto
  timezone: Asia/Hebron
  system_prompt: |
    Help users find and book appointments using the available tools.
    Do not invent availability or claim a booking before it completes.
  conversation:
    enabled: true
    include_capability_summary: true
    capability_summary:
      include_title: true
      include_description: true
    use_presentation_model: true
```

`name` is optional, 1–100 characters. `language` defaults to `auto`; it guides responses, and Arabic selection prompts use explicit `ar` or Arabic detected in the last user message. `timezone` defaults to `UTC` and must be a valid IANA zone. Set it for relative-date routing; it does not rewrite your API's stored timestamps.

`system_prompt` guides conversation and result presentation. It cannot authorize a write or override the mapper. Put execution rules in schemas and policies.

> You can use it to clarify the model's personality and tone in conversations.

`conversation.enabled` defaults to true. Turning it off makes a deliberate no-match return `ROUTING_NO_MATCH` instead of normal conversation; pending work has its own preservation behavior. Keep it enabled for assistants that must answer greetings or detours.

The capability-summary switches control whether conversation sees tool titles/descriptions. Disabling them reduces context but gives the assistant less information about what it can do. They do not remove tools from routing. `use_presentation_model` defaults to true; it uses the presentation model for conversation, falling back to the routing model if no separate presentation model exists.

## Routing: balance recall, context, and latency

```yaml
routing:
  candidate_limit: 6
  fallback_attempts: 2
  lexical:
    enabled: true
    min_score: 1
    normalize_unicode: true
    normalize_arabic: true
    phrase_matching: true
  semantic_recall:
    enabled: true
    candidate_limit: 6
    max_catalog_tools: 100
    max_catalog_chars: 100000
    metadata:
      include_id: true
      include_title: true
      include_description: true
    allow_no_match: true
    multilingual: true
```

These are the defaults.

- `candidate_limit` bounds the final selection shortlist, from 1 to 100. Increase it only when relevant tools are being excluded; larger lists can increase cost and ambiguity.
- `fallback_attempts` permits 1–3 structured/JSON selection fallback attempts. It is not an HTTP retry policy.
- `lexical.enabled` controls cheap metadata matching. `min_score` sets the confidence threshold. Disabling normalization can reduce matches for equivalent Unicode or Arabic spellings. `phrase_matching` rewards configured phrases.
- `semantic_recall.enabled` permits model-assisted discovery when lexical recall is weak. Disabling it can save calls but lose paraphrases and cross-language matches.
- Its `candidate_limit` bounds recalled IDs. `max_catalog_tools` and `max_catalog_chars` bound metadata sent for recall; raising them allows broader catalogs at additional context cost. Their maximums are 10,000 tools and 1,000,000 characters.
- Metadata title/description switches trade richer discovery context for smaller prompts. `include_id`, `allow_no_match`, and `multilingual` currently accept only true; they are not off switches.

Measure [routing outcomes](/docs/routing) on representative messages before tuning these values. A precise description is often more useful than increasing every limit.

## Session: distinguish memory, expiry, and work limits

| Setting | Default | Effect of changing it |
| --- | --- | --- |
| `ttl_ms` | 1,800,000 | Controls session lifetime. Longer sessions retain pending context longer; do not use this to extend a slot's dependency TTL. |
| `max_history_messages` | 20 | Bounds model conversation history. Zero disables retained history; maximum 1,000. This is not a lifetime message allowance. |
| `max_sessions` | 10,000 | Capacity passed to the default in-memory store. Custom stores must implement their own capacity policy. |
| `max_steps` | 32 | Bounds dependency orchestration per operation, maximum 100. More steps permit deeper work but increase possible latency. |

Except for history length, these values must be positive integers. In-memory storage is suitable for a single process; configuration does not turn it into shared storage. See [sessions](/docs/sessions) for deployment consequences.

## Rate limits: count real work

```yaml
rate_limits:
  ai_calls: 60
  tool_calls: 120
  messages: 120
  messages_per_session: 1000
  window_ms: 60000
```

The window budgets are enforced per session in the default implementation. `ai_calls` includes model work such as routing, recall, conversation, and presentation; one chat message can spend more than one call. `tool_calls` includes every actual HTTP attempt, including retries. `messages` counts incoming runtime operations, including invoke, select, confirm, and cancel, in the window; `messages_per_session` is a separate lifetime counter.

Increasing `window_ms` with unchanged budgets makes the allowance stricter over time. Lower budgets can interrupt an otherwise valid multi-step flow. Handle `RATE_LIMITED` explicitly. A distributed application needs a shared limiter; these settings are not an account-wide billing cap.

## Models: enable only the work you need

`models.routing` enables chat routing. `models.presentation` optionally separates result wording and conversation from routing. Supported provider IDs are `openai`, `anthropic`, `mistral`, `cohere`, and `ollama`.

Each provider needs `provider` and `model`. Hosted providers require `api_key: $ENV:NAME`; missing environment values fail compilation. Optional `base_url` selects the provider endpoint. `timeout_ms` defaults to 30,000. `temperature` is optional, from 0 to 2, and must be supported by the model.

Capability overrides describe real adapter/model behavior; setting `nativeTools: true` does not give an unsupported model tool calling. See [providers](/docs/providers) before changing them.

## Validate changes before startup

```js
import { readFile } from "node:fs/promises";
import { compileConfig } from "agento-runtime";

const compiled = compileConfig(await readFile("./config.yml", "utf8"));
console.log(compiled.tools.values());
```

Compilation checks relationships that a YAML parser cannot: duplicate IDs, invalid schemas, unmapped URL placeholders, unknown input paths, dependency cycles, conflicting destinations, unsafe credentials, and missing confirmations.

It does not prove the API returns your declared structure or that descriptions route correctly. Pair it with mocked execution tests and a routing evaluation set. The [booking fixture](/docs/booking-example) demonstrates a complete behavioral test.

---

# Design a tool the model can choose and the runtime can validate

A tool represents one useful business operation. Start from a real request and response contract, then describe the intent that should reach it. Do not begin by copying every API endpoint into a large undifferentiated tool catalog.

The [quick start](/docs/quick-start) connects your own API. This chapter develops its input schema. The example below names its search input `q`; you may keep `query` from the quick start instead, provided both the schema and request mapping use that chosen input name.

## Separate interpretation from execution

Each tool has a public model-facing contract—ID, description, input schema—and a private execution definition—URL, auth, mapping, dependencies, behavior, response policy.

The compiler derives the model-facing schema. Top-level fields produced by dependencies or consumed references are removed from it, while remaining part of the execution schema. Inspect `runtime.listTools()` to see what the model actually receives; do not assume it sees the entire YAML.

## Define inputs from concrete examples

This fragment belongs under an entry in `tools`, alongside its `id` and `request`:

```yaml
tool:
  title: Search services
  description: >
    Search the service catalog by name.
    Use this to discover services, not to create a booking.
  input_schema:
    type: object
    additionalProperties: false
    properties:
      q:
        type: string
        minLength: 1
        description: The service name or words supplied by the user.
      limit:
        type: integer
        minimum: 1
        maximum: 20
        default: 5
    required: [q]
```

For “show hair coloring services,” `q` contains the user's search phrase. `limit` is optional; validation fills 5 if it is absent. The API request will not contain either field until you [map them](/docs/mapping).

| Schema decision | Why it exists | Observable effect |
| --- | --- | --- |
| `type: object` | Tool arguments must be named fields | Other root shapes fail compilation. |
| `required: [q]` | Search cannot run without a query | Missing q becomes `needs_input`. |
| `minLength: 1` | An empty query is not useful | Present-but-empty q fails validation. |
| Integer limit with bounds | Protect the API from unbounded requests | Floats, strings, zero, and values over 20 fail. |
| `default: 5` | Give omitted optional input a defined meaning | Omission supplies 5; it does not repair an invalid provided value. |
| `additionalProperties: false` | Reject undeclared input | A fabricated extra field cannot quietly enter execution. |

The compiler defaults root `additionalProperties` to false if unspecified. Declare it explicitly on nested objects too when that is your intent. Use draft-07-compatible JSON Schema; AJV strict validation does not fetch remote schema references or coerce types.

## Required does not mean the model must invent it

For booking, the execution schema can require `date` and `slot_token`. The user supplies the date, while a dependency produces the token. Declare both properties, then assign token ownership through `depends_on.map`.

The runtime asks for missing user-owned fields before running dependencies. After lookups, it validates the completed input again. Partial model output can therefore be legitimate; a missing date is a question to ask, not an excuse to hallucinate one.

Host context, constants, and generated values can be mapped directly without inviting the model to supply them. Authentication never belongs in an input schema.

## Write descriptions that separate neighboring intents

Poor: “Manage appointments.”

Better: “List available appointment times for a date. Use for availability questions, not for creating a booking.”

A description should explain the action, relevant inputs, and the boundary with related tools. `title` is optional, up to 200 characters. Description is required, non-empty, and limited to 8,000 characters. Long prose is not inherently better routing context.

Tool IDs must be unique, start with a letter or underscore, contain letters, digits, underscores, or hyphens, and be no longer than 64 characters. Choose stable names such as `find-slots` and `book`; changing an ID also changes its references in dependencies and navigation.

## Add retrieval hints after the contract works

```yaml
# Tool-level fragment:
trigger_hints:
  - available appointments
  - free slots
  - مواعيد متاحة
keywords:
  availability: 3
  appointment: 1.5
  موعد: 2
```

Hints and positive keyword weights improve lexical retrieval. They do not authorize execution and do not force a match. A very broad word with a high weight can crowd out a better tool.

Test actual messages and inspect candidate selection before adding dozens of synonyms. [Routing](/docs/routing) explains how lexical confidence affects model recall.

## Keep secrets outside the model contract

A property such as `access_token` is rejected as credential-like. Pass a user token through the host request's `auth`, or configure an environment-backed API key under `request.auth`.

Similarly, declaring `tenant_id` as an unconstrained model input does not establish tenancy. Use trusted server context, and let the downstream API enforce permission.

## Verify the intended boundary

Test these inputs independently: valid values, missing required input, unknown properties, invalid types, enum violations, and out-of-range numbers. Then inspect `listTools()` for hidden dependency fields. Finally test routing with positive examples, near-neighbor intents, and unrelated conversation.

A schema proves shape and constraints. It cannot prove that a customer owns an ID or that a slot is still available. That is why [dependencies](/docs/dependencies) and [API authorization](/docs/authentication) remain necessary.

---

# Map values into an HTTP request

A tool's input schema describes the data AGENTO accepts. It does **not** send those fields to your API automatically. `request.map` is the explicit bridge between validated values and the REST endpoint.

Read [your first tool](/docs/quick-start) first. This guide shows where each value comes from, where it goes, and what happens if it is absent.

## Start with the request your API expects

Suppose your API accepts:

```http
GET /services/svc-color/slots?date=2026-10-01&limit=5
X-Locale: ar
```

Your application supplies `service_id`, `date`, and optional `limit`. The names on the left of each mapping belong to the **HTTP contract**; paths on the right read the **tool input**.

Complete tool entry; place it under `tools` in `config.yml`:

```yaml
- id: find-slots
  tool:
    description: Find available appointment slots for a service and date.
    input_schema:
      type: object
      properties:
        service_id: { type: string, minLength: 1 }
        date: { type: string, format: date }
        limit: { type: integer, minimum: 1, maximum: 20, default: 5 }
      required: [service_id, date]
  request:
    method: GET
    url: https://api.example.com/services/{service}/slots
    map:
      path:
        service: $.service_id
      query:
        date: $.date
        limit: $.limit
      headers:
        X-Locale:
          source: constant
          value: ar
```

For `{ service_id: "svc-color", date: "2026-10-01" }`, validation inserts `limit: 5`. Mapping replaces `{service}`, adds the two query parameters, and writes the header. The model never chooses the hostname or constructs this URL.

Try changing `limit` to `"5"`. It fails schema validation: AGENTO does not coerce strings into numbers. Try removing `service_id`: the runtime asks for missing input before sending HTTP.

## Understand the four destinations

| Destination | Use it for | What changes at execution |
| --- | --- | --- |
| `request.map.path` | URL segments such as a service ID | Replaces declared `{placeholders}` with encoded scalar values. Missing values leave an unresolved placeholder and stop execution. |
| `request.map.query` | Search terms, dates, filters, pagination | Adds URL parameters. An array becomes repeated parameters, such as `tag=a&tag=b`. |
| `request.map.body` | JSON fields for POST, PUT, PATCH, or DELETE | Builds a JSON object from mapped fields. GET and HEAD bodies are rejected at compilation. |
| `request.map.headers` | Non-secret metadata such as locale | Converts scalar values to strings. Credentials and reserved transport headers are rejected; use `request.auth`. |

A body mapping is a map of top-level keys. To send a nested object, map the entire declared object to a body key; dotted destination keys are not a nested-object construction language.

```yaml
# Fragment inside request.map of a confirmed write tool.
body:
  customer: $.customer
  channel:
    source: constant
    value: web
```

With input `{ customer: { name: "Maya" } }`, the body is `{"customer":{"name":"Maya"},"channel":"web"}`. Declare `customer` and its shape in `tool.input_schema`, and configure [effects and confirmation](/docs/effects) for the write.

## Choose the source deliberately

`$.date` is shorthand for `{ source: tool-input, path: $.date }`. A dollar path is always relative to the selected source; it is not a global variable.

| Source | Root of its path | When to use it |
| --- | --- | --- |
| `tool-input` | Current tool arguments after validation and dependency resolution | User-owned values, plus values explicitly filled by dependencies. |
| `session` | Host-provided `context` | Tenant, locale, or other trusted application context. It does not expose the session's internal history or credentials. |
| `dependency` | Resolved data for the named direct dependency | A value returned by a declared lookup. After a selection, this is the selected item, not the original response envelope. |
| `constant` | Its literal `value` | A fixed currency or application channel. A bare string such as `SAR` is not a valid value source. |
| `generated` | A newly generated UUID | Request correlation. This is generated during request preparation, not a substitute for API idempotency support. |
| `selection` | Declared selection display facts | Confirmation previews only. It is forbidden in HTTP and dependency argument mappings. |

Example fragments:

```yaml
# Under request.map.body of a confirmed write tool:
tenant_id: { source: session, path: $.tenantId }
currency: { source: constant, value: SAR }
request_id: { source: generated, generator: uuid }

# Under request.map.body when availability is a declared dependency:
slot_token:
  source: dependency
  tool: availability
  path: $.slot_token
```

Supply session context from your server:

```ts
const result = await runtime.invoke({
  sessionId: authenticatedConversationId,
  tool: "book",
  arguments: { date: "2026-10-01" },
  context: { tenantId: authenticatedTenantId },
  auth: { token: userAccessToken },
});
```

The identifiers in this host snippet come from your authenticated application. Do not copy a tenant ID from untrusted chat text. Credentials belong in `auth`, never in `context` mappings or the tool schema.

## Paths are property access, not expressions

Supported: `$`, `$.id`, `$.customer.address.city`, and numeric property segments where the source permits them. Unsupported: `$.items[*]`, `$.items[0]`, filters, interpolation, function calls, and JavaScript expressions.

For tool-input mappings, the compiler also checks that the path exists in the declared schema. A path that is syntactically valid can still be rejected because it points at an undeclared property. Prototype-related segments are forbidden.

Use `depends_on.select` to choose from a list. Do not approximate selection by guessing an array index in a mapping.

## Missing is different from empty

An absent optional tool-input value is omitted from the mapped object. An absent session or dependency source raises `INPUT_MAPPING_MISSING`; the runtime does not silently invent a tenant or identifier. A literal `null` is a value, not “missing”; it can be sent in a JSON body but is invalid for a URL or header.

Path and header values must be scalar. Path values cannot contain slashes, backslashes, control characters, or the special segments `.` and `..`. Header values cannot contain line breaks. Query arrays must contain scalar members.

## Do not confuse the three maps

```yaml
# Fragment inside a dependency on services:
arguments:
  q: $.service_query
map:
  $.service_id: $.id

```

This is a separate fragment inside the parent tool’s request, not a continuation of the dependency block above:

```yaml
map:
  body:
    service: $.service_id
```

1. `arguments` sends the parent's `service_query` into the lookup's argument `q`.
2. The dependency's `map` copies the lookup result's `id` into the parent's `service_id`. Its left side is a destination path.
3. `request.map.body` sends that resolved value as the API field `service`. Its left side is a field name.

This distinction is the foundation of [dependencies and selection](/docs/dependencies).

## Verify the boundary

Compare the request reaching your staging API with the direct request you know works. Inspect URL, method, and field names without logging credentials. You can then capture this behavior with a mocked transport in automated tests. Test an absent optional field, missing context, invalid types, and encoded path characters. The [complete booking example](/docs/booking-example) gives you a runnable fixture and its expected sequence.

---

# Validate the response and decide what to display

Your first tool now sends a request to the API. Next ask two separate questions: does the response have the structure your application relies on, and which parts should your application or a model display?

Put `response` inside the tool, alongside `tool` and `request`. It does not belong inside the outgoing request mapping.

## Start with your actual response contract

Continuing the search example, suppose the API guarantees this shape:

```json
{
  "services": [
    { "id": "svc-color", "name": "Hair coloring", "price": 80 }
  ]
}
```

The response is an object containing a list. Each item has an ID, name, and numeric price. If your API uses `data.items` or returns prices as strings, use that contract instead. A sample helps explain structure but does not prove which fields are always required.

## Validate the structure you rely on

For the contract above, add:

```yaml
response:
  output_schema:
    type: object
    properties:
      services:
        type: array
        items:
          type: object
          properties:
            id: { type: string }
            name: { type: string }
            price: { type: number }
          required: [id, name, price]
    required: [services]
```

The outer `type: object` describes the whole response. `services` is an array; the rules in `items` apply to each service. The outer `required` requires the list, while the inner one requires fields in each item.

Returning `{ "services": "unavailable" }` violates this contract and produces `TOOL_OUTPUT_SCHEMA_VIOLATION`, even after HTTP 200. By contrast, `{ "services": [] }` is valid: this schema does not require at least one item.

## Choose a presentation mode

The quick start uses `presentation: "raw"` so your application can inspect and render structured data without a presentation model.

Use `"ai"` for a generated message after [configuring a model](/docs/providers), or `"both"` when the UI needs both data and language. This is a runtime or invocation option, not a field to add under `response`:

```ts
const result = await runtime.invoke({
  sessionId: authenticatedConversationId,
  tool: "search-services",
  arguments: { query: "hair" },
  presentation: "both",
});
```

This application fragment assumes an existing runtime and model. Remain on raw output while verifying your API integration. In ai mode, data is omitted after successful presentation. A presentation failure preserves the successful execution/data path; do not repeat an API write merely to get better wording.

## Send only relevant facts to the presentation model

Add this alongside `output_schema`, inside `response`:

```yaml
model_view:
  path: $.services
  include: [name, price]
  max_items: 5
  max_chars: 4000
```

`path` selects the list from the full response. `include` keeps relevant fields from projected objects. `max_items` limits list context, and `max_chars` bounds its serialized representation.

This reduces unnecessary context for the presentation model. **It does not filter the raw data your application receives.** In raw or both mode, do not assume `data` has been reduced to the include list.

Nor does `max_items: 5` prove that the API returned only five results. This is a presentation bound, not API pagination or a total count.

Defaults are path `$`, 20 items, and 12,000 characters. Limits are 1,000 items and 100,000 characters. When path remains `$`, `response.items_path` can serve as a projection fallback. Dependency selection has its own `select.items_path`; these settings are not interchangeable.

## Hide private display fields deliberately

If the API includes an internal field that must not be displayed, configure:

```yaml
private_fields: [internal_note]
```

This removes fields by name, including nested occurrences, from display data. The implementation considers private names across configured tools, so avoid generic names such as `name` unless that broad effect is intended.

Private-field removal differs from model_view.include: one removes private display data; the other selects useful model context. Internal dependency mapping can still use values before display sanitization.

API authorization remains essential. Removing a field from a rendered result is not permission to fetch data the caller should never access.

## Diagnose the right stage

The runtime reads and validates the API response, uses required internal data, sanitizes display fields, and builds bounded model context when presentation is requested.

If output validation fails, changing `model_view.path` will not fix it. Match the full response contract first. If the data is valid but the generated answer lacks the price, check whether the price actually reached the model.

`response.instructions` can guide wording, such as refusing to invent a missing price. It cannot fix a wrong path or restore a field you excluded.

## Check what each consumer receives

Compare a direct API response with `result.data` in raw mode. Then test both mode and ensure the message makes only supported claims and distinguishes no results from a truncated view.

Exercise an empty list, a missing required field, a wrong price type, and a nested private field. Use your staging API or sanitized saved responses for failure cases that are impractical to trigger live.

Once request and response behavior are understood, [add a conversation model](/docs/providers) to the working tool.

---

# Authentication and security

## Choose credentials based on who owns the action

A public catalog needs no credential. “My bookings” needs the signed-in user's token. A server-only partner lookup may need a service API key. These are different authorities, not interchangeable configuration styles.

In the [booking fixture](/docs/booking-example), the host supplies `auth.token`, and `request.auth.type: session` adds a Bearer header at execution. Without that token the runtime does not manufacture an identity; it can send the request without the header and leave rejection to the API. Therefore, enforce sign-in in your host before starting protected operations.

For service keys, configure an environment variable name under `env`; changing its header name changes the HTTP header, not the privilege the key grants. A missing required service key fails configuration compilation.

## Verify the boundary with your API

Test the same operation with a valid token, an expired token, another user's resource ID, and no token. Expect the API to enforce ownership, and handle its 401/403 through `AUTH_REJECTED`. Then test account switching: allocate a new authorized conversation or clear the old session rather than carrying pending approval into a different identity.

Keep credentials out of browser-authored config, model input, mapped headers, and ordinary context. Runtime redaction supplements your application's access control; it cannot replace it.


## AGENTO is not your auth server

The current Runtime intentionally does not decode user JWTs, fetch JWKS keys, validate issuers/audiences, or decide business permissions.

Your downstream API remains authoritative for:

- token validity and expiry;
- login state;
- roles/scopes;
- tenant boundaries;
- resource ownership;
- business authorization.

## User-authenticated tools

```yaml
request:
  auth:
    type: session
```

Then the trusted host passes the caller's token:

```ts
const result = await runtime.invoke({
  sessionId: serverOwnedSessionId,
  tool: "my-bookings",
  auth: { token: userAccessToken },
});
```

At execution time AGENTO forwards:

```http
Authorization: Bearer <opaque-token>
```

The token is not intended for model prompts, model-facing schemas, ordinary session context, results, or hooks.

## 401 and 403

Upstream 401 and 403 responses are surfaced as `AUTH_REJECTED`, with the bounded upstream status available in error details. Your host decides whether that means “sign in again,” “refresh login,” or “you are not allowed.”

## Session ID is not identity

This is a critical production rule:

> A `sessionId` is a conversation/continuation handle, not proof of identity.

The host must bind server-owned session IDs to callers. Do not let a client arbitrarily choose another user's session ID or attach a different user's token to an existing session.

For account switching, clear the old session or allocate a fresh server-owned session ID.

## API-key tools

For server-to-server credentials:

```yaml
request:
  auth:
    type: api-key
    env: SERVICE_KEY
    header: X-API-Key
```

The compiler expects the environment variable to exist. Do not put service keys in URLs, user arguments or prompts.

## Public tools

```yaml
request:
  auth:
    type: none
```

Use only when the endpoint is genuinely public.

## Session credential reuse

AGENTO can retain an opaque session token so later `select()`/`confirm()` continuations can use it. The exported authentication contract supports a custom `SessionTokenStore`; stored token values are encrypted before the store sees them.

With a custom store, provide a shared 32-byte encryption key to workers that must reuse credentials across instances.

## Redaction

Runtime redaction protects known secrets and sensitive fields across results, history, hooks and model-bound data. It is a transport safeguard, **not a full PII classifier**.

Your API must still return only data appropriate for the caller and apply domain-specific minimization.

## Transport safeguards

- credential-bearing requests require HTTPS unless local development explicitly opts out;
- redirects are blocked;
- runtime validates the final request origin against configured origin;
- response bodies have byte ceilings;
- model-facing projections are bounded;
- credentials cannot be declared as normal model input fields.

---

# Providers and custom model adapters

## Add a model only after direct execution works

Start with the [model-free first tool](/docs/quick-start). When its HTTP contract passes, add `models.routing` to interpret language. This isolates provider failures from mapping failures.

A provider configuration names the adapter protocol and a real model ID in your account. The model ID is not inferred from the provider name. Hosted providers require an environment key reference; Ollama can use a local base URL. Never copy a model name without verifying that your deployment supports it.

```yaml
# Root fragment; replace the model placeholder before use.
models:
  routing:
    provider: openai
    model: YOUR_TOOL_CAPABLE_MODEL
    api_key: $ENV:OPENAI_API_KEY
    timeout_ms: 30000
```

Use raw presentation first. Then add a separate presentation model only if you need different cost, latency, or wording behavior for results. Without a separate one, presentation falls back to the routing model. Conversation uses the presentation model by default, controlled by `assistant.conversation.use_presentation_model`.

## Capability flags describe reality

`nativeTools` selects native tool calling when supported. Otherwise structured output or validated JSON fallback may be used. `strictSchema`, `systemMessages`, and `temperature` describe protocol features; they are not guarantees about model intelligence or business correctness.

Do not enable flags to suppress an integration error. Test a valid tool call, malformed arguments, a no-tool answer, an aborted request, and provider timeout. With a custom adapter, return the documented structured shape and usage values; the runtime still validates tool selection and input.


## Built-in provider IDs

AGENTO's built-in provider configuration accepts:

- OpenAI
- Anthropic
- Mistral
- Cohere
- Ollama

Provider/model capabilities vary. The adapter exposes capability flags rather than assuming every model supports every protocol feature.

## Capability contract

```ts
interface ModelCapabilities {
  nativeTools: boolean;
  structuredOutput: boolean;
  strictSchema: boolean;
  systemMessages: boolean;
  temperature: boolean;
}
```

Native tool calling takes precedence. If unavailable, AGENTO may use structured output or validated JSON fallback depending on capabilities.

## Provider behavior summary

| Provider | Native tool path | Structured fallback |
|---|---|---|
| OpenAI | Chat Completions tools | JSON Schema response format |
| Anthropic | Messages tools/input_schema | conservative validated JSON fallback |
| Mistral | Chat tools | JSON Schema response format |
| Cohere | v2 Chat tools | JSON object/schema path |
| Ollama | `/api/chat` tools | `format` JSON Schema |

Treat this table as adapter behavior, not a promise that every model deployment supports every feature.

## Custom `ModelAdapter`

The public interface is small:

```ts
interface ModelAdapter {
  capabilities(): ModelCapabilities;
  generateText(request: GenerationRequest): Promise<TextGenerationResult>;
  generateStructured(
    request: GenerationRequest & { schema: JsonSchema }
  ): Promise<StructuredGenerationResult>;
  selectTool(request: ToolSelectionRequest): Promise<ToolSelectionResult>;
}
```

Inject it:

```ts
const runtime = await AgentRuntime.create({
  config: configObject,
  model: myRoutingModel,
  presentationModel: myPresentationModel,
});
```

## Adapter requirements

A production adapter should:

- respect `AbortSignal`;
- return bounded usage metadata;
- never return multiple unreviewed tool calls as one action;
- preserve the model-facing tool schemas;
- surface provider failures rather than smuggling provider error text into normal assistant messages;
- clearly declare unsupported capabilities.

## Test your exact deployment

Provider protocols and model support change. Run routing fixtures/evaluations against the exact model IDs and endpoints you plan to deploy.

---

# Resolve dependencies before you execute

A user can ask for an appointment on a date. They cannot know the opaque `slot_token` your API requires. A dependency turns that gap into an explicit lookup and, when needed, a real choice.

The parent tool declares **what must be known before it can execute**. AGENTO runs read-only tools to obtain those facts, maps them into the parent's input, then resumes the parent. It is not a sequence the model improvises.

Use the runnable [booking example](/docs/booking-example) alongside this guide. Read [mapping](/docs/mapping) first if `$.date` is unfamiliar.

## Separate user-owned and API-owned input

Our `book` tool needs two fields:

- `date`: supplied by the user, for example `2026-10-01`.
- `slot_token`: supplied by the availability API after a valid slot is chosen.

Both belong in the execution input schema, including `required: [date, slot_token]`. But the dependency fills `slot_token`. The compiler removes top-level dependency-produced fields from the model-facing schema. The model extracts a date; it is not asked to invent a slot token.

That removal is currently top-level. Prefer flat fields for API-owned identifiers instead of assuming arbitrary nested targets will be hidden from the model.

## Add the lookup to the parent

This fragment belongs directly on `book`, next to `tool`, `behavior`, and `request`. It assumes an `availability` tool accepting `date` and returning the response below.

```yaml
depends_on:
  - tool: availability
    when: always
    arguments:
      date: $.date
    select:
      items_path: $.slots
      id_path: $.slot_token
      label_path: $.label
      id_sensitive: true
      facts:
        time: $.label
    map:
      $.slot_token: $.slot_token
    ttl_ms: 30000
```

```json
{
  "slots": [
    { "slot_token": "slot-a", "label": "10:00" },
    { "slot_token": "slot-b", "label": "11:00" }
  ]
}
```

Read the declaration from top to bottom:

1. `tool` names an existing lookup tool. It must be read-only.
2. `arguments.date` reads the parent's date and passes it to that lookup.
3. `items_path` finds the array in the lookup's full response.
4. `id_path` and `label_path` are relative to **one array item**, not the response root.
5. `id_sensitive` replaces the outward-facing option IDs with opaque IDs. The original token stays available for internal mapping.
6. `facts.time` captures a display value for a later confirmation preview. It does not authorize execution.
7. `map` copies the chosen item's `slot_token` into the parent's `slot_token`. Left is the parent destination; right is the selected item source.
8. `ttl_ms` gives these facts 30 seconds of validity. It also limits how long a dependent confirmation can remain valid.

If the API returns `{ data: { slots: [...] } }`, change `items_path` to `$.data.slots`. Leave `id_path: $.slot_token` unchanged because it still reads one item.

## Trace one invocation

```ts
let result = await runtime.invoke({
  sessionId: "booking-demo",
  tool: "book",
  arguments: { date: "2026-10-01" },
});
```

With two slots, the sequence is:

1. Validate the date. A missing user-owned date returns `needs_input` before any lookup.
2. Call availability with that date.
3. Return `needs_selection` containing only options backed by the API.
4. Wait for the user's choice. No booking POST has happened.
5. Map the selected token and validate the completed parent input.
6. Prepare the booking and return `needs_confirmation`.
7. Only an explicit `confirm()` call sends the POST.

The UI should render every returned option and submit the option the user actually selected:

```ts
// In the UI choice handler, after the user selects an option:
const next = await runtime.select({
  sessionId,
  selectionId: result.selection!.id,
  choice: selectedOption.id,
});
```

Do not substitute the array index for `choice`, and do not automatically choose `options[0]` in a real interface. A pending `chat()` can recognize displayed one-based numbers or an exact label; `select()` expects the returned ID.

## What changes with the number of results?

| Lookup result | Runtime behavior | Host responsibility |
| --- | --- | --- |
| No items | `needs_input`, possibly with an empty `missing` list | Explain that nothing is available and ask for another date or preference. |
| One item, no explicit preference | Selects it automatically | Continue handling the next status; a write still needs confirmation. |
| Several items | `needs_selection` | Render the returned options. |
| Invalid array, duplicate IDs, or non-string labels | `DEPENDENCY_SELECTION_INVALID` | Fix the response contract or selection paths. |
| Selected item lacks a mapped field | `DEPENDENCY_UNRESOLVED` | Check the right-hand side of `map` against one actual item. |

An empty result is not a malformed result. Preserve this distinction in your UI.

## Match a preference without inventing an ID

Add an optional `preferred_time` string to the parent's schema, then add this inside the dependency's `select`:

```yaml
match:
  input_path: $.preferred_time
  item_path: $.label
```

A value such as `10:00` is compared using trimmed, case-insensitive string equality. This is **not** fuzzy time parsing: `4 PM` does not equal `16:00` unless the extracted input and API value already use the same representation.

One exact match is selected automatically. Multiple exact matches still require a choice. No exact matches show alternatives, even if only one unrelated option exists.

## Choose a freshness policy

The default is `when: always`. It resolves the dependency even when the destination was supplied by the caller, but can reuse a valid cached result for matching dependency arguments and session context. “Always” does not mean “perform HTTP on every turn.”

`when: missing` skips a not-yet-resolved dependency only when **all** its mapped destinations are already present. Use it when a trusted host, navigation step, or reference has already supplied the required value. It is not a validation check against the API: a direct caller can provide a value that skips the lookup.

After a dependency has resolved, its cache still expires. Changing input or context can invalidate previously prepared choices. A confirmation cannot outlive the earliest dependency or selection expiry, even if its own TTL is longer.

Use a short TTL for availability and prices. Longer-lived reference catalogs can use a longer TTL. Increasing TTL reduces lookup traffic while increasing the risk that a shown choice is no longer valid at the API.

## Chain lookups only when the next one needs them

A full service booking can form `book → availability → services`. Declare the service lookup on `availability`, because availability needs a service ID before it can run. AGENTO resolves the deepest read first, then returns to its parent.

Within a parent's list, dependencies resolve in declaration order; they are not parallel tasks. A later dependency can read parent fields filled by an earlier map. Its `arguments` cannot directly use `source: dependency`; map the required value into a declared parent input first.

Cycles, duplicate dependencies, overlapping destination mappings, missing tool IDs, and effectful dependency tools are rejected at compilation. `session.max_steps` bounds orchestration work; it is not simply the number of tools in your file.

## Use a dependency without a choice

If a lookup returns one object, omit `select`. The right-hand paths then read the **whole response**:

```yaml
# Parent fragment; current-customer returns { customer: { id: "cus-42" } }.
depends_on:
  - tool: current-customer
    map:
      $.customer_id: $.customer.id
```

Declare `customer_id` on the parent and define the read-only lookup. Adding or removing `select` changes the root seen by `map`; forgetting this is a common source of unresolved values.

## Prove the behavior before production

Test no date, zero slots, one slot, several slots, duplicate IDs, a missing token, an expired choice, and a changed host context. Assert that the write count stays zero until confirmation. Then test an API 409/410 with [dependency refresh recovery](/docs/effects), which must produce a new review step rather than silently book a different slot.

---

# Make writes wait for an informed confirmation

A side effect is an operation that changes business state: creating a booking, updating a profile, charging an account, or deleting an item. A model's decision to call a tool is not user approval for that change.

In AGENTO, the effect classification is executable policy. It influences compilation, confirmation, dependency eligibility, and retry safety.

## Choose the effect that describes the operation

| `behavior.effect` | Typical operation | Enforced consequence |
| --- | --- | --- |
| `read-only` | GET availability or HEAD metadata | Default; eligible as an automatic dependency. |
| `side-effect` | POST booking or PATCH profile | Requires `confirmation.required: true`; cannot be an automatic dependency. |
| `destructive` | DELETE booking or irreversible removal | Requires confirmation and has the same core write guards; it does not add a second approval screen automatically. |

The current compiler rejects every non-GET/HEAD method marked `read-only`. Even a POST search endpoint must be configured as effectful in this release. Do not label it read-only just to bypass confirmation.

## Start with the booking operation

This is the `behavior` block of the [complete booking config](/docs/booking-example):

```yaml
behavior:
  effect: side-effect
  confirmation:
    required: true
    ttl_ms: 30000
    preview:
      date: $.date
      time: { source: selection, path: $.time }
      notes: $.notes
```

Without the effect declaration, POST fails compilation with `CONFIG_EFFECT_INVALID`. With an effect but without required confirmation, compilation fails with `CONFIG_CONFIRMATION_INVALID`. These are startup errors, not UI hints.

Adding this block changes execution: after resolving inputs and dependencies, AGENTO prepares and stores the HTTP request, returns `needs_confirmation`, and waits. It has not sent the booking yet.

## Design a preview a person can verify

The preview is a map, not a template string. `date` and `notes` read tool input. `time` reads a declared selection fact, such as the chosen slot label. Display names and times are more useful than opaque IDs.

Selection facts are **presentation-only**. Use dependency mappings to supply the actual token for HTTP. Preview paths cannot expose credential-like or declared private fields; generated values are prohibited in previews. Undeclared selection facts fail compilation.

If you omit `preview`, the runtime falls back to model-visible input fields. That may be technically valid but less informative: dependency-produced IDs are hidden from the model, and the user's chosen time may be absent. Define an explicit preview for consequential actions.

## The host owns the approval action

A result contains a confirmation ID, expiry, and preview. Render the preview and an explicit confirmation control. In that control's server handler:

```ts
// Called only after the user approves the displayed action.
const next = await runtime.confirm({
  sessionId: authenticatedConversationId,
  confirmationId: displayedConfirmation.id,
  auth: { token: userAccessToken },
});
```

Use the same authorized session and context. Do not accept an arbitrary session ID from a browser. Return the next status to the UI; confirmation can still fail at the downstream API.

A chat reply such as “yes” or “نعم” does not send the write. It returns the pending confirmation again. Your host must call `confirm()` with the current ID. Use `cancel()` for rejection; hiding a card is not cancellation.

## Understand expiry and invalidation

`confirmation.ttl_ms` defaults to 300,000 milliseconds. Effective expiry is the earliest of this deadline and the selections/dependencies it relies on. A five-minute confirmation cannot make a 30-second slot token fresh for five minutes.

The runtime binds the confirmation to the prepared request and relevant execution context. A changed operation, changed context, expired dependency, wrong ID, or replayed approval produces a stale-confirmation outcome. Do not retry an old ID; reconstruct the intended action and ask for a new review.

## Consumption is not the same as exactly-once delivery

AGENTO persists consumption before sending HTTP. The same approval is not meant to execute repeatedly. However, a network timeout can occur after the API accepted the booking, and a worker can crash between sending and recording success.

For important writes, make the backend support idempotency and an operation-status lookup. `TOOL_EXECUTION_UNCERTAIN` means reconcile the operation, not blindly issue a second booking.

```yaml
# request fragment, only when the API honors this header:
idempotency:
  header: Idempotency-Key
  source: generated
retry:
  attempts: 3
```

The key is attached to the prepared request and reused across its transport attempts. A completely new invocation prepares a new key. This setting does not implement server-side deduplication for you. See [HTTP policy](/docs/http).

## Refresh an unavailable choice, then ask again

If the API returns 409 or 410 because the selected slot disappeared, add:

```yaml
# Merge into behavior on the parent write tool.
recovery:
  refresh_dependency: availability
  statuses: [409, 410]
```

The named dependency must be directly declared by the tool. Runtime discards its stale values, refreshes the read-only lookup, and returns a new selection or confirmation. It does not silently approve a replacement.

Recovery is appropriate only if those API statuses actually mean the dependency became stale. A 409 for a different business rule should not automatically be interpreted as “choose a different slot.”

## Test the policy

Run [the booking fixture](/docs/booking-example), then assert these boundaries: zero POSTs before approval, one POST after approval, no second POST from a repeated ID, stale IDs rejected after expiry, and a 409 refresh requiring another review. These tests prove the policy more directly than checking that a confirmation card renders.
