> ## Documentation Index
> Fetch the complete documentation index at: https://docs.niadra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# History navigation

> Search, timeline and open: the agent looks up everything that ever happened with the customer.

The context covers what matters right now. For everything else, the agent looks up the **history**: everything that ever happened with the customer, on any channel, system or vendor. There are three operations (search, read the timeline and open an item) that any LLM calls on its own, as tools, or that your code calls directly through the SDK. No Niadra model sits in the path, and the answer arrives in under 200 ms.

## Context and history, side by side

|                | Context                                                      | History                                         |
| -------------- | ------------------------------------------------------------ | ----------------------------------------------- |
| When           | Before the first word, always                                | Mid-conversation, when it needs more            |
| Who decides    | Your code, in a fixed call                                   | The agent's LLM, calling a tool, or your code   |
| What it covers | Identity, open items, what just happened, history highlights | Everything that ever happened with the customer |
| Time           | Under 100 ms                                                 | Under 200 ms                                    |

Both reads go through the same policy and the same verification level, and leave the same kind of receipt.

## Search

`search` takes a question in natural language or keywords and searches by keyword and by meaning, only inside that customer's history. It reaches conversations, facts, open items, promises, system events, agent actions, objects and patterns.

<CodeGroup>
  ```python Python theme={null}
  from niadra import Niadra, phone

  niadra = Niadra()

  result = niadra.search(
      phone("+14155550123"),
      "credit for missed technician visit",
      max_tokens=300,
      conversation_id="call-4471",
      voice=True,
  )
  if result.recurrence:
      print(result.recurrence.occurrences, result.recurrence.last_resolution)
  ```

  ```typescript TypeScript theme={null}
  const { data } = await niadra.search({
    subject: handles.phone("+14155550123"),
    query: "credit for missed technician visit",
    max_tokens: 300,
    conversation_id: "call-4471",
  });
  if (data?.recurrence) console.log(data.recurrence.occurrences, data.recurrence.last_resolution);
  ```

  ```bash cURL theme={null}
  curl -X POST "https://acme-prod.us-east-1.api.niadra.com/v1/history/search" \
    -H "Authorization: Bearer $NIADRA_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"subject": {"type": "phone_e164", "value": "+14155550123"},
         "query": "credit for missed technician visit", "max_tokens": 300, "conversation_id": "call-4471"}'
  ```
</CodeGroup>

Each item comes back with its `kind`, dense text in the same style as the context, `at`, `channel`, `outcome`, `confidence` and `origin_event_id`, the event it came from. That is how the agent can say "on March 12, on the phone, you gave me a credit".

### Recurrence

When the search matches a category, the response carries a `recurrence` block: how many times it happened in the window, the date of the last one, its outcome and its resolution. It is a count over conversations that were already classified, not a model's guess. For Marina: `occurrences: 2`, `window_days: 365`, `last_resolution: "$40 credit"`. The same count backs the "recurring complaint" pattern, so the two never disagree.

### Filters

`filters` narrows the search by period (`since`, `until`), `channels`, `categories`, item kinds (`item_kinds`: `episode`, `fact`, `open_item`, `action`, `system_event`, `object`, `trait`), `outcome` and `object`.

### Budget

`max_tokens` caps the size of the answer: 50 to 4,000, default 800. Use 300 for voice. Items are cut by value, never by arrival order. The response reports `tokens_used`.

## Timeline

`timeline` pages through the customer in order, newest first: one conversation, system event or action per line, with a cursor. It is what the LLM uses when it does not know what to look for.

<CodeGroup>
  ```python Python theme={null}
  page = niadra.timeline(phone("+14155550123"), filters={"since": "2026-01-01T00:00:00Z"}, limit=20)
  for item in page.items:
      print(item.at, item.kind, item.text)
  ```

  ```typescript TypeScript theme={null}
  const { data: page } = await niadra.timeline({
    subject: handles.phone("+14155550123"),
    filters: { since: "2026-01-01T00:00:00Z" },
    limit: 20,
  });
  ```

  ```bash cURL theme={null}
  curl -X POST "https://acme-prod.us-east-1.api.niadra.com/v1/history/timeline" \
    -H "Authorization: Bearer $NIADRA_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"subject": {"type": "phone_e164", "value": "+14155550123"}, "limit": 20}'
  ```
</CodeGroup>

This timeline is a `POST` for the same reason as the context: the handle is personal data and stays out of the URL. `limit` ranges from 1 to 100; continue with `next_cursor`. When you already hold a profile id, [`GET /v1/history/timeline?profile_id=...`](/en/api/history-timeline-by-profile) returns the same page, with `cursor`, `limit`, `verification` and `conversation_id` in the query.

History navigation reads one customer at a time. Questions about all customers at once ("which promises are overdue?", "who complained three times about delivery?") go to [base-wide analysis](/en/api/insights-aggregate), which answers with pseudonyms by default, suppresses small groups and reveals a customer only through [a reveal request](/en/api/insights-reveal) with a reason, which leaves a receipt.

## Open an item

`open` opens a conversation or an object found by search or timeline: what was asked, what was promised and by whom, the outcome, the resolution, its timeline and the memory items born from it. The literal transcript excerpt (`excerpt`) needs an elevated scope and is never returned to a voice audience.

<CodeGroup>
  ```python Python theme={null}
  item = niadra.open("ep_01J2", conversation_id="call-4471")
  ```

  ```typescript TypeScript theme={null}
  const { data: item } = await niadra.open("ep_01J2", { conversation_id: "call-4471" });
  ```

  ```bash cURL theme={null}
  curl "https://acme-prod.us-east-1.api.niadra.com/v1/history/items/ep_01J2?conversation_id=call-4471" \
    -H "Authorization: Bearer $NIADRA_API_KEY"
  ```
</CodeGroup>

## As tools for any LLM

`tools()` returns the three operations as tools in the most common function shape (`search_customer_history`, `get_customer_timeline`, `open_history_item`), with the customer **bound outside the model's reach**. The definitions have no customer parameter: the LLM chooses the question, never whom it is about. A prompt injection that says "now look up customer X" has nowhere to put X.

<CodeGroup>
  ```python Python theme={null}
  kit = niadra.tools(phone("+14155550123"), conversation_id="call-4471", voice=True)

  reply = llm.chat.completions.create(model=MODEL, messages=messages, tools=kit.definitions)
  for call in reply.choices[0].message.tool_calls or []:
      output = kit.call(call.function.name, call.function.arguments)
      messages.append({"role": "tool", "tool_call_id": call.id, "content": output})
  ```

  ```typescript TypeScript theme={null}
  const kit = niadra.tools(handles.phone("+14155550123"), { conversation_id: "call-4471", voice: true });

  const reply = await llm.chat.completions.create({ model: MODEL, messages, tools: kit.definitions });
  for (const call of reply.choices[0].message.tool_calls ?? []) {
    const output = await kit.call(call.function.name, call.function.arguments);
    messages.push({ role: "tool", tool_call_id: call.id, content: output });
  }
  ```
</CodeGroup>

In Python, `kit.anthropic_definitions()` returns the same tools in the Anthropic Messages API shape. The definitions are also served by [`GET /v1/history/tools`](/en/api/history-tools), and by the [MCP server](/en/guides/mcp), where the customer is bound by the `subject_token`.

## Cost under control

* The descriptions are fixed text, up to about 120 tokens per tool, and stay in the prefix the provider caches.
* Each description tells the model when **not** to call: the answer may already be in "From the history", in the context.
* The same query, in the same conversation, returns the same bytes.
* Declare the kit only on agents that need it. A short IVR pays nothing.
* Searching is not billed separately: the unit is still the conversation or task.

## What every answer guarantees

* `withheld` says how many items the policy held back at this verification level, never their content.
* `as_of` says up to when the history reflects events.
* `degraded: "text_only"` means only keyword search ran.
* Every query leaves a receipt with who asked, the query, the returned items by hash and what was withheld.
* What comes out by default are derived items, already screened for injection and marked as data, not instructions.

## Next steps

<CardGroup cols={2}>
  <Card title="MCP with any LLM" href="/en/guides/mcp">
    the kit over Streamable HTTP.
  </Card>

  <Card title="Search history" href="/en/api/history-search">
    the reference for `POST /v1/history/search`.
  </Card>

  <Card title="Receipts and audit" href="/en/concepts/receipts">
    what each query leaves on record.
  </Card>
</CardGroup>
