# Navigating the agentic layer

This page describes the one loop you run to drive Sojourncase as an agent:

> **read → follow `_links` (safe) → perform `actions` (unsafe) → resolve
> `meta.docs` for the exact contract.**

It assumes you have authenticated (see
[agents.md → Authenticate](https://sojourn.cross-fare.com/agents.md)) and are
sending `Authorization: Bearer <JWT>` on every request. For the precise envelope
shape, see
[envelope.md](https://sojourn.cross-fare.com/agents.dir/envelope.md); for the
full route list, see
[routes.md](https://sojourn.cross-fare.com/agents.dir/routes.md).

---

## 1. Read — `GET /.agentic/<path>`

Every screen in the app has a mirror at `/.agentic/<same-path>`, served by the API
at `/api/agentic/*` (nginx rewrites `/.agentic/…` → `/api/agentic/…`). A `GET`
returns **one JSON envelope**, media type
`application/vnd.sojourncase.agentic+json`:

```jsonc
{
  "@context": "https://sojourn.cross-fare.com/agents.dir/context.jsonld",
  "@type": "TodayScreen",
  "@id": "https://sojourn.cross-fare.com/.agentic/today/{caseId}",
  "data":   { /* flat camelCase screen state; enums {code,label}; ISO-8601 dates */ },
  "_links": { /* HAL: rel -> { href, title, type?, templated? } — SAFE GETs */ },
  "actions":[ /* Siren-style UNSAFE mutations against the real /api */ ],
  "meta":   { /* kind, version, generatedAt, humanUrl, caseId, docs{…} */ }
}
```

Start at **`GET /.agentic`** — the entrypoint. Its `data` carries your caller
identity, credit balance, and the cases you can access; its `_links` point at the
visa catalog and at each accessible case. From there you walk the graph.

Read what you need out of `data`. It is deliberately flat and camelCase. Enum
values are objects `{ "code": "in_review", "label": "In review" }` — branch on
`code`, show `label`. All timestamps are ISO-8601 / RFC 3339.

---

## 2. Follow `_links` — safe navigation only

`_links` is a HAL-style map of `rel → { href, title, type?, templated? }`. Every
link is a **safe `GET` to another `/.agentic` route** — never an unsafe verb, never
a cross-origin URL, never a real `/api` mutation. Use links to move between
screens; use [actions](#3-perform-actions--unsafe-mutations) to change anything.

- **`self`** is always present — the canonical `/.agentic` URL of the current
  screen (equal to `@id`).
- **`describedby`** points at the spec for this screen (openapi / api-reference).
- The remaining rels are navigational and screen-specific.

### Rel vocabulary

Rels name a relationship, not a URL, so you bind to the rel and let the server own
the href. Common rels you will see:

| Rel | Goes to |
| --- | --- |
| `self` | this screen's own `/.agentic` URL |
| `describedby` | `/openapi.json` / `api-reference.md` for this screen |
| `up` / `collection` | the parent screen (e.g. a record → its tracker) |
| `case` | the case overview mirror, `/.agentic/case/{caseId}` |
| `today` `build` `evidence` `draft` `credits` | the case's main screens |
| `tracker` / `record` / `task` / `doc` | a specific child entity's mirror |
| `events` | the case event feed, `/.agentic/case/{caseId}/events` |
| `visa-types` / `visa` | the visa catalog and a single visa definition |
| `next` / `prev` | the next / previous page of a paged feed (see §5) |

When in doubt, prefer the rel over hand-building a URL — the server may template
or version paths, and following the provided `href` is always correct.

### Templated links (RFC 6570)

A link with `"templated": true` contains URI-template variables in `{braces}` you
must expand before fetching. Variables come from the surrounding `data` or from
your own context. Examples:

```jsonc
"_links": {
  "case":   { "href": "/.agentic/case/{caseId}", "templated": true, "title": "Case overview" },
  "record": { "href": "/.agentic/build/{caseId}/{trackerId}/records/{recordId}",
              "templated": true, "title": "Open record" }
}
```

Expand `{caseId}`, `{trackerId}`, `{recordId}` with the ids from `data`, then GET
the result. Level-1 simple string expansion covers every template the mirror
emits; reserved-expansion (`{+var}`) is not used. Always percent-encode an
expanded value if it can contain spaces or slashes — case `req` keys, for
instance, may contain spaces.

---

## 3. Perform `actions` — unsafe mutations

`actions` is a Siren-style array. Each entry describes a mutation you invoke
against the **real `/api` endpoint** (never a mirror):

```jsonc
{
  "name": "addRecord",
  "title": "Add record",
  "method": "POST",
  "href": "/api/cases/{caseId}/trackers/{trackerId}/records",
  "templated": true,
  "contentType": "application/json",
  "fields": [
    { "name": "id", "type": "string", "required": false,
      "description": "Client-minted UUID; server adopts it (else mints its own)." }
  ],
  "creditsCost": 0,
  "serverAuthoritative": false
}
```

To perform it:

1. Expand any `{braces}` in `href` (same RFC 6570 rules as links).
2. Send the named `method` to that real `/api/...` URL with your Bearer token.
3. Build the JSON body from `fields` — honor `required`, and constrain values to
   `enum` when present. `type` is the wire type; `description` explains intent.
4. If `creditsCost > 0`, you will be charged on success; a `402` means the
   balance was insufficient.
5. If `serverAuthoritative: true` (the AI actions — `autofillRecord`,
   `generateBrief`, `reviewBrief` — and anything that returns an authoritative
   balance), **use the server's returned result as truth.** Do not predict the
   outcome or assume the credit cost; await and merge what comes back.

### Action methods and the safe/unsafe split

`actions` use `POST`, `PATCH`, `PUT`, and `DELETE`; `_links` use only `GET`. This
is the load-bearing invariant of the whole layer:

- **Safe (`_links`)** → idempotent navigation, `/.agentic/*` only.
- **Unsafe (`actions`)** → state changes, real `/api/*` only.

Never `POST` to a `/.agentic` URL, and never expect a real `/api` mutation to
appear in `_links`. A mutation always shows up as an `action` whose `href` is the
real endpoint.

### Action names are stable operationIds

An action's `name` equals the API `operationId` (e.g. `createCase`, `patchCase`,
`addTracker`, `addRecord`, `setRecordValue`, `autofillRecord`, `spawnTask`,
`addDoc`, `setDocStatus`, `uploadDocFile`, `toggleClaim`, `generateBrief`,
`reviewBrief`, `signoffBrief`). The same string keys `meta.docs.endpoints` and
the anchor in `api-reference.md` / the `operationId` in `openapi.json` — so the
name is your single handle across runtime, docs, and contract.

---

## 4. Resolve `meta.docs` — deep-link to the contract

`meta` carries provenance and documentation pointers:

```jsonc
"meta": {
  "kind": "today",
  "version": "agentic-envelope/1.0",
  "generatedAt": "2026-06-25T12:00:00Z",
  "humanUrl": "https://sojourn.cross-fare.com/today",
  "caseId": "…",
  "docs": {
    "openapi":   "https://sojourn.cross-fare.com/openapi.json",
    "reference": "https://sojourn.cross-fare.com/agents.dir/api-reference.md",
    "endpoints": {
      "self":       ".../agents.dir/api-reference.md#getagenticToday",
      "build":      ".../agents.dir/api-reference.md#getagenticBuild",
      "patchCase":  ".../agents.dir/api-reference.md#patchcase",
      "toggleClaim":".../agents.dir/api-reference.md#toggleclaim"
    }
  }
}
```

**Invariant:** `meta.docs.endpoints` has a key for **every `_links` rel and every
action `name`** on the screen. So whenever you are unsure how a link target or an
action behaves, look the rel/name up in `endpoints`, follow the anchor, and read
the exact request/response shape — no guessing. `humanUrl` is the equivalent
page a person would open; `generatedAt` is when the snapshot was rendered.

---

## 5. Case-scoping and paging

### Case scope

Most data is **case-scoped**. The real endpoints live under
`/api/cases/{caseId}/…` and the mirror screens under `/.agentic/<screen>/{caseId}`
(e.g. `/.agentic/today/{caseId}`, `/.agentic/build/{caseId}`,
`/.agentic/build/{caseId}/{trackerId}`, `/.agentic/evidence/{caseId}`,
`/.agentic/draft/{caseId}`, `/.agentic/case/{caseId}`). The un-scoped screens are
the entrypoint `/.agentic`, the visa catalog `/.agentic/visa-types[/{visaId}]`, the
create-case screen `/.agentic/new`, and `/.agentic/credits`.

Carry the `caseId` you were handed by a `_link` or by `data`/`meta.caseId`. Do
not invent one: a `caseId` you are not a member of yields `403/404`. To discover
the cases you may act on, read the entrypoint `GET /.agentic`.

The `req` path segment on claim toggles (`/api/cases/{caseId}/claims/{req}`) and
some record fields can contain spaces — always percent-encode such segments when
you expand a template.

### Paging cursors

The two feeds — case **events** (`/api/cases/{caseId}/events`) and the credit
**ledger** (`/api/credits/ledger`) — are cursor-paged, newest first:

- `limit` — page size.
- `before` — an RFC 3339 timestamp; only rows **strictly older** are returned.

To page back through history, take the `at` timestamp of the last (oldest) row in
a page and pass it as `before` on the next request; repeat until a short/empty
page. In an envelope these feeds expose `next` / `prev` `_links` with the cursor
already baked into the `href` — prefer following those links over assembling the
query yourself.

---

## Putting it together

```text
1. POST /api/auth/login                       → { token }
2. GET  /.agentic                              → identity, credits, cases
3. follow _links.case (or /.agentic/today/{caseId})
4. read data → decide what to change
5. pick an action, e.g. addRecord:
     POST /api/cases/{caseId}/trackers/{trackerId}/records  { id }
6. if unsure: meta.docs.endpoints["addRecord"] → api-reference.md#addrecord
7. re-GET the screen mirror to see the new state, repeat
```

Read from `/.agentic`. Write to `/api`. Let the rels and `meta.docs` carry the
URLs and the contract so you never hand-build an endpoint.
