# Typed agent calls: use a governed agent like a function

Most "call an LLM from your backend" code has the same three problems:
- **Untyped:** you parse prose and hope.
- **Unsafe to retry:** a timeout leaves you unsure whether to resend.
- **Lost on crash:** if your process dies, so does the work.

This example triages support tickets by calling an agent through application requests with a JSON Schema result contract:

```python
triage = AgentFunction(client, agent_id, schema)
accepted, replayed = triage.submit(ticket_text, idempotency_key=key)   # 202 in milliseconds
value = triage.result(accepted["request_id"])                         # validated JSON, or a typed failure
```

- **Typed:** the platform validates the agent's answer against your schema before you can read it. You get native JSON (`value["severity"] == "sev1"`), never a string to parse.
- **Retry-safe:** resending the same `Idempotency-Key` and body returns the original acceptance (`Idempotency-Replayed: true`), not a second piece of work.
- **Durable:** acceptance means the input is stored. The agent keeps working if your process dies, and giving up on a wait never cancels it.
- **Honest failure states:** you get `validation_failed`, `generation_failed` or `no_result`, or a request that is `awaiting_action` on a human decision. Never a silently invented value.
- **Still governed:** the agent's tools, approvals, security rules and audit all apply as usual.

## Files

| File | What it is |
| --- | --- |
| `agent_function.py` | About 130 lines you can copy: `submit` (idempotent, with backpressure retry), `wait` (revision cursor, bounded long-poll) and `result` (validated value or a typed error). |
| `triage.py` | A CLI that fans tickets out, fans results in, and keeps a crash-safe SQLite ledger. |
| `triage.schema.json` | The result contract: severity, category, team, summary, impact, `needs_human` and a draft reply. |
| `tickets.jsonl` | Five sample tickets, from a payments outage down to a feature question. |

Everything is Python 3.12+ and the standard library.

## Before you start

The deployment must enable both gates: `API_APPLICATION_REQUESTS_ENABLED` and `API_STRUCTURED_RESULTS_ENABLED` (both default off). With the result gate off, typed submissions return `503 structured_results_disabled`.

You need an API key with `agents:operate`. A service credential granted to an existing agent also works. Creating the agent needs a member key with agent creation rights.

```bash
export DT_API_BASE="https://your-deployment.example/api/v1"
read -r -s -p "API key: " DT_API_KEY; export DT_API_KEY
python3 triage.py create-agent
export DT_AGENT_ID=...    # printed above
```

## Run it, and crash it on purpose

```bash
python3 triage.py run --crash-after 3
```

```
  T-1001  accepted                      request b09a0a01-…
  T-1002  accepted                      request 6f95ac4d-…
  T-1003  accepted                      request b194d770-…
  Simulated crash after submitting T-1003, before recording its request id.
```

The ledger recorded T-1003's idempotency key before the POST, but not its request ID. Run the same command again:

```bash
python3 triage.py run
```

```
  T-1003  replayed original acceptance  request b194d770-…   <- same request, no duplicate work
  T-1004  accepted                      request 86c08ea1-…
  T-1005  accepted                      request f589199c-…
  T-1001  validated
  …
  T-1005  validated

ticket   sev   team      human  summary
T-1001   sev1  platform  yes    Northwind Freight: all card authorizations failing with 502 since ~09:40 UTC; ~3,000 payments/hour blocked.
T-1002   sev3  payments  yes    Bluebird Clinics: October invoice charged twice ($1,240 duplicate); requesting refund of the duplicate.
T-1003   sev4  product   no     Quarry Labs: asking whether transactions can be exported to CSV including custom metadata fields for month-end close.
T-1004   sev2  security  yes    Harbor & Pine admins report unrequested password-reset emails and a sign-in from a new device abroad; possible account compromise.
T-1005   sev2  platform  yes    Fernway Outdoor reports a third webhook outage this quarter; late refund webhooks cause double refunds and they threaten to leave at November renewal.
```

The agent holds five requests, not six: the crash cost nothing.

`python3 triage.py show T-1004` prints one full validated value, including the suggested reply.

## How the pieces fit

1. **Intent, then submit.** Write the ticket and a fresh UUID key to the ledger, then `POST /agents/{id}/requests/` with `Idempotency-Key`, `body`, `client_reference` and `result_schema`. The `202` body carries `request_id` and a revision `cursor`. Store `request_id`: replay receipts expire (24 hours by default), so the request ID, not the key, is your long-term handle.
2. **Wait without polling hot.** `POST …/wait/` with `{"cursor", "timeout_seconds": 30}` returns as soon as the request's revision changes. A timeout returns the same cursor. Each principal may hold four waits at once by default, and `429` comes with `Retry-After`.
3. **Settle on result state, not chat.** Request detail has three independent dimensions (`publication_state`, `execution_state`, `response_state`) plus `result.state`. Stop when `result.state` is `validated`, `validation_failed`, `generation_failed`, `no_result` or `purged`. Stop early if `execution_state` is `awaiting_action` (a pending human decision; see `pending_actions`) or the request is `blocked`.
4. **Read the value.** `GET …/result/` returns `availability`, `has_value` and the native `value`. A validated `null` is `has_value: true`.

## Things to know

- **Validity is not success.** A validated shape proves the answer matches your schema. It does not prove that an external action succeeded.
- **Schema profile:** the result schema uses the restricted `application-result-v1` profile. Every node needs a `type`. `enum`, `const`, required/additional properties and numeric/length bounds are supported. `$ref`, `pattern`, `format`, unions and combinators are rejected at submission.
- **Shared context:** requests sent to one agent share its model context. Use separate agents or workspaces when callers need independent context.
- **Changed inputs:** changing the body or schema under a live key returns `409`. Use a new key for genuinely new work. An unsupported schema keyword returns `400` with `unsupported_schema` before any work is accepted.
- **TypeScript:** the maintained TypeScript client exposes the same flow (`submitRequest`, `awaitRequest`, `getApplicationResult`).
