#region AgentChat.ApiUx [C:3] [TYPE ADR] [SEMANTICS ux,api,agent-chat,final]
@defgroup Ux Final API contract — Gradio `submit()`, LangChain v1 HITL via second submit(), PostgreSQL checkpointer, structured metadata.

## Gradio Transport

**Connection**: `Client.connect("/api/agent/gradio")` → nginx proxy → Gradio. JWT forwarded.

**Primary submit**: `app.submit("/chat", {message, conversation_id})` — two-arg API. `conversation_id` is an `additional_inputs` key.
**Examples on UI** (for `additional_inputs`): `[["Покажи дашборды", null], ["Статус системы", null]]`.

**Cancel**: `submission.cancel()`.

### Streaming Events

`for await (event of submission)` — `event.data` is a `ChatMessage` with structured `metadata`:

| `metadata.type` | `metadata` fields | UX Reaction |
|-----------------|-------------------|-------------|
| `stream_token` | `{token: "При"}` | Append token to last assistant msg |
| `tool_start` | `{tool: "search_dashboards", input: {...}}` | Tool card: name + spinner |
| `tool_end` | `{tool: "search_dashboards", output: {...}}` | Spinner→checkmark |
| `tool_error` | `{tool: "search_dashboards", error: "..."}` | Spinner→cross + detail |
| `confirm_required` | `{prompt: "Подтвердить деплой?", thread_id: "..."}` | Confirmation card + timer |
| `confirm_resolved` | `{result: "confirmed"/"denied"}` | Card collapses |
| `error` | `{code: "LLM_UNAVAILABLE", detail: "..."}` | Error card + retry |
| (none) | plain `{content: "..."}` | Normal text |

### HITL Resume Protocol

```
Primary: submit("/chat", {message}, [conversation_id, null])
  → handler: graph.astream_events(config={"thread_id": conversation_id})
      → interrupt_before fires → checkpoint saved (PostgreSQL)
      → handler: yield {metadata: {type: "confirm_required", thread_id: conversation_id}}
      → Svelte: renders confirmation card (primary stream ends)

Confirm: submit("/chat", {message: "confirm", action: "confirm", conversation_id}, [conversation_id, "confirm"])
  → handler: detects action="confirm" → loads checkpoint by thread_id
      → invokes graph with Command(resume={"action": "confirm"}, config={"thread_id": conversation_id})
      → graph resumes from checkpoint → new stream begins

Deny: submit("/chat", {message: "deny", action: "deny", conversation_id}, [conversation_id, "deny"])
  → handler: Command(resume={"action": "deny"}, config=...)
      → yields confirm_resolved(result="denied") → agent responds
```

---

## REST Endpoints

| Method | Endpoint | Purpose |
|--------|----------|---------|
| `POST` | `/api/assistant/conversations` | Create → `{id, title, created_at}` |
| `GET` | `/api/assistant/conversations` | Paginated list + search |
| `GET` | `/api/assistant/history` | Paginated messages |
| `POST` | `/api/assistant/conversations/{id}/messages` | Append after stream_end |
| `DELETE` | `/api/assistant/conversations/{id}` | Soft-delete (is_archived=true) |
| `GET` | `/api/agent/conversations/{id}/active` | Multi-tab gate |
| `POST` | `/api/auth/service-token` | Gradio obtains service JWT |

---

## Dual Identity RBAC

```
Gradio handler:
  service_jwt = POST /api/auth/service-token  (authenticates the agent service)
  user_jwt    = request.headers["Authorization"]  (browser user)

@tool function:
  call FastAPI with:
    Authorization: Bearer {service_jwt}  (agent is authorized to call)
    X-User-JWT: {user_jwt}               (operation authorized under user)

FastAPI endpoint:
  validates service JWT → agent is legitimate
  validates X-User-JWT → user has RBAC permission for this operation
  audit: {service_actor: "agent", user_actor: user_id, operation, ...}
```

## Error Taxonomy

| Code | Source | UX |
|------|--------|-----|
| `UNAUTHORIZED` | JWT | Redirect /login |
| `FORBIDDEN` | RBAC | metadata.type="error" in stream |
| `LLM_UNAVAILABLE` | LangChain | metadata.type="error" → card + retry |
| `GRADIO_UNREACHABLE` | connect | Dot red, retry 5×5s |
| `TOOL_FAILURE` | FastAPI | metadata.type="tool_error" |

#endregion AgentChat.ApiUx
