# Iblai API Agent Session

> Talk to a deployed ibl.ai agent directly over REST/SSE (or WebSocket) and manage its chat sessions — POST a prompt to the agent chat endpoint, attach arbitrary metadata (surfaced later as client_context), and list/read sessions and per-task history exports. The direct-transport counterpart to iblai-api-agent-chat's MCP wiring; use when you want raw streamed chat + session records rather than an MCP server.

- Skill: `iblai/iblai-api-agent-session` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add iblai/iblai-api-agent-session`
- Raw SKILL.md: https://api.skillmd.com/api/skills/iblai/iblai-api-agent-session/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: iblai (https://skillmd.com/u/iblai)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/iblai/iblai-api-agent-session

---


# iblai-api-agent-session

Drive a deployed agent's **chat transport directly** and read its **sessions**.
Where `/iblai-api-agent-chat` wires a hosted MCP server for conversation, this
skill is the raw REST/SSE (and WebSocket) surface: POST a prompt, stream the
reply, attach `metadata` that resurfaces as `client_context`, and list/inspect
the resulting session records. Get `IBLAI_ORG`/`IBLAI_USERNAME`/`IBLAI_API_KEY`
from `/iblai-api-login`.

## Auth & conventions

- **Header:** `Authorization: Api-Token $IBLAI_API_KEY` on every request.
- **Path vars:** `{org}` = `$IBLAI_ORG`, `{user}` = `$IBLAI_USERNAME`.
- **Two hosts — chat is streaming/ASGI:**
  - Chat turn (SSE / WebSocket) → `https://asgi.data.iblai.app`
  - Session reads/writes → `https://api.iblai.app/dm/api/ai-mentor/orgs/{org}/users/{user}/v1` … i.e. `…/orgs/{org}/users/{user}/sessions/…`
- Not connected yet? Run **`/iblai-api-login`** first.

## Concepts

**Two independent context fields — one soft, one hard.** A chat turn can carry both
`metadata` (soft) and `document_filter` (hard); they do different jobs and don't
substitute for each other:

- **`metadata` (soft)** — steers *how the agent reasons*. It's appended to the prompt and
  persisted; it never restricts which documents RAG can retrieve.
- **`document_filter` (hard)** — steers *which documents RAG may retrieve*. It's a
  document-level allow-list applied in the vector store; it does not touch the prompt.

Passing `metadata` alone narrows nothing in retrieval; passing `document_filter` alone
scopes retrieval without giving the agent any extra prose context. Use both when you want
both effects.

**`metadata` → `client_context` passthrough (soft).** Every chat turn (WS or SSE) may
carry a `metadata` object of arbitrary key/values (`BaseConsumerPayload.metadata`). The
runner folds it into the prompt the agent sees, so the agent can tailor its reply, and the
consumer persists it on the session as `Session.metadata["client_context"]`. It is
**session-level**: each turn's `metadata` overwrites the session's `client_context`, so
it sticks across turns until you send new keys. Use it to tell one deployed agent
*where/why* a message arrives (product, plan tier, page, region) without editing its
prompt. It then echoes back on every read below. The sibling field `page_content` is
also appended to the prompt, but — unlike `metadata` — is stripped before the message is
saved.

**`document_filter` → retrieval scoping (hard).** An optional
`BaseConsumerPayload.document_filter` object (`{key: scalar}`) restricts RAG retrieval to
documents whose ingested `custom_metadata` is compatible with the filter. It is matched
**inclusively per key**: a document is kept if, for **every** filter key, it either
*matches that key's value* **or** *does not carry that key at all*; it is excluded only
when it carries the key with a **different** value. Multiple keys are AND'd. Because a
document that lacks a key is never excluded by it, generic/untagged material always
survives — e.g. `{"stateCode":"CA"}` keeps California docs **and** generic docs that carry
no `stateCode`, while dropping docs tagged for other states. Unlike `metadata`,
`document_filter` is **not** session-sticky — it applies only to the turn that sends it —
and it never appears in the prompt or saved history. It only shrinks the retrieval
candidate set; top-k similarity ranking still runs afterward, so an eligible document is
not guaranteed to be retrieved if higher-scoring eligible documents fill the top-k.

## Reads

- **GET** `…/dm/api/ai-mentor/orgs/{org}/users/{user}/sessions/` — list the user's
  chat sessions.
- **GET** `…/orgs/{org}/users/{user}/sessions/{session_id}/` — the session's paginated
  chat messages (`MessageView`); the response also carries `client_context`, read from
  the session's `metadata["client_context"]`.
- **GET** `…/orgs/{org}/users/{user}/sessions/{session_id}/tasks/{task_id}/` — the
  chat-history export (`DownloadableChatHistory`); every item carries a `client_context`
  field. Add `?to_csv=true` for a CSV whose columns are exactly
  `type,content,timestamp,client_context`. Kicking off (POST) and polling that export
  task is owned by **`/iblai-api-agent-history`**.
- **Analytics echo:** the same value comes back at `summary.client_context` from
  **GET** `…/dm/api/analytics/messages/details/?platform_key={org}&session_id={session_id}`
  — documented under **`/iblai-api-analytics`**.

## Writes

- **POST** `https://asgi.data.iblai.app/api/agent/chat/?platform_key={org}&session_id={session_id}`
  — send a chat turn; response is Server-Sent Events. Body (`BaseConsumerPayload`):
  ```json
  {
    "session_id": "…",
    "prompt": "Hello",
    "flow": { "name": "<agent unique_id>", "tenant": "<org key>" },
    "page_content": "optional text appended to the prompt, stripped before saving",
    "metadata": { "any": "soft client context keys" },
    "document_filter": { "stateCode": "CA" }
  }
  ```
  `session_id` (a UUID4) and `flow` are **required**; `flow.name` selects the agent (its
  `unique_id`, or a slug/name) and `flow.tenant` is the org key. `prompt` and
  `page_content` default to empty, `metadata` and `document_filter` to null. `metadata` is
  soft passthrough — stored on the session as `client_context` (see Concepts) and echoed in
  the reads above. `document_filter` is the hard retrieval scope — an inclusive per-key
  allow-list over documents' ingested `custom_metadata`, applied only to this turn and never
  persisted (see Concepts and Schema). The **same payload** works over WebSocket at
  `wss://asgi.data.iblai.app/ws/chat/`.
- **POST** `…/dm/api/ai-mentor/orgs/{org}/users/{user}/sessions/` — create/retrieve a
  session (`ChatSessionView`; the body's `mentor` field picks the agent). Or let the
  first chat turn create one by passing a new `session_id`.

## Example

```bash
# Stream a chat turn with attached client context (SSE). MENTOR = the agent's unique_id.
curl -N -X POST \
  "https://asgi.data.iblai.app/api/agent/chat/?platform_key=$IBLAI_ORG&session_id=$SESSION" \
  -H "Authorization: Api-Token $IBLAI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{"session_id":"'"$SESSION"'","prompt":"Summarize my notes","flow":{"name":"'"$MENTOR"'","tenant":"'"$IBLAI_ORG"'"},"metadata":{"source":"docs","tab":"notes"}}'

# Read the session's messages back (client_context is the metadata you sent)
curl "https://api.iblai.app/dm/api/ai-mentor/orgs/$IBLAI_ORG/users/$IBLAI_USERNAME/sessions/$SESSION/" \
  -H "Authorization: Api-Token $IBLAI_API_KEY"

# Download the history export as CSV (client_context is a column)
curl "https://api.iblai.app/dm/api/ai-mentor/orgs/$IBLAI_ORG/users/$IBLAI_USERNAME/sessions/$SESSION/tasks/$TASK/?to_csv=true" \
  -H "Authorization: Api-Token $IBLAI_API_KEY"
```

## Notes

- **Streaming runs on ASGI** — the chat turn (`/api/agent/chat/`, `/ws/chat/`) is on
  `asgi.data.iblai.app`; session reads are ordinary REST on the `api.iblai.app/dm`
  gateway. `session_id` and `flow` are required on every turn, and `flow.name` must
  resolve to a deployed agent (its `unique_id`, slug, or name).
- For **OpenAI-format** inference against a provider/model (no agent RAG/memory), use
  `/iblai-api-inference`; to chat via an **MCP server** instead of raw SSE, use
  `/iblai-api-agent-chat`; the history-export task (kick off + poll) is
  `/iblai-api-agent-history`; the `summary.client_context` analytics read is
  `/iblai-api-analytics`.

## Schema

**`metadata` / `client_context`** (soft) — an arbitrary JSON object (`dict[str, Any] |
null`, `BaseConsumerPayload.metadata`). No fixed keys; use whatever your app needs, e.g.
`product`, `planTier`, `userRole`, `region`. Sent as `metadata` on a chat turn, it is
persisted at `Session.metadata["client_context"]` and read back as `client_context` in
the session-messages response, the history export (a `client_context` field per item, or
CSV column via `?to_csv=true`), and analytics `summary.client_context`. It is appended to
the prompt but never restricts retrieval.

**`document_filter`** (hard) — an optional JSON object (`dict[str, str|int|float|bool] |
null`, `BaseConsumerPayload.document_filter`) that scopes RAG retrieval to documents whose
ingested `custom_metadata` is compatible with the filter. It never touches the prompt and
is **not** persisted on the session (per-turn only). The keys/values here match the
`custom_metadata` you attach when adding documents — see `/iblai-api-agent-dataset` for
tagging documents at ingestion (the `custom_metadata` field on `documents/train/`).

- **Keys** must be flat and alphanumeric/underscore (`^\w+$`) — no `__`, no ORM-style
  lookup suffixes (`__icontains`, etc.). **Values** must be scalars (string, number, or
  boolean); lists/objects/null are rejected. A malformed filter fails the turn with a
  validation error rather than being silently ignored.
- **Inclusive per key:** a document is kept if, for **every** filter key, it *matches the
  value* **or** *does not carry that key*; it is excluded only when it carries the key with
  a **different** value. Multiple keys are AND'd.
- **Matching is exact** — value comparison is case- and type-sensitive (`"CA"` ≠ `"ca"`;
  the integer `2026` ≠ the string `"2026"`), and keys are matched exactly (`"stateCode"` ≠
  `"statecode"`); a key no document carries acts as a no-op for that key.
- **State + generic pattern:** ingest generic/shared documents with **no** state key and
  state-specific documents with `stateCode = <state>`; then `{"stateCode":"CA"}` retrieves
  California **and** generic documents while excluding other states — no per-state agent or
  two-stage retrieval needed. To search everything, send **no** `document_filter` (an empty
  or fully non-matching filter can yield an empty candidate set, and the agent may then
  answer without any retrieved context).
- **Eligibility ≠ retrieval:** the filter only shrinks the candidate set; top-k similarity
  ranking still runs, so an eligible document is not guaranteed to surface if higher-scoring
  eligible documents fill the top-k. Raise the agent's retrieval `k` if broad generic
  material is being crowded out.

## Reference material

- [`references/metadata-passthrough.md`](references/metadata-passthrough.md) — the
  `metadata` pass-through companion: the `<CONTEXT METADATA>` prompt-injection format,
  per-transport wire notes (SSE/WebSocket + the embedded-iframe `postMessage` channel),
  session caching (send-once, replace-not-merge, ~2h TTL), the
  one-agent-many-contexts pattern, the storage/pipeline map (session
  `client_context` vs. the per-message snapshot), and the **soft `metadata` vs. hard
  `document_filter`** comparison with the state-specific + generic retrieval pattern.

