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

> ## Agent Instructions
> Maximem Synap is a managed memory layer for AI agents: it ingests conversations and documents, extracts structured memory, and returns scoped context to the agent at retrieval time.
> The Python and JavaScript/TypeScript SDKs are the supported way to use Synap. Write integration code with SDK calls, not raw HTTP requests.
> Start with the Quickstart: https://docs.maximem.ai/getting-started/quickstart.md. Every SDK method is documented in the SDK reference: https://docs.maximem.ai/sdk-reference/overview.md.

# Traces and tool calls

> Send your agent's reasoning, tool calls and tool results on the stream so Synap sees the whole turn, and keep tool output searchable on your machine after a compaction.

A turn is more than the user's message and your agent's reply. In between, your agent reasons, calls tools and reads what they return. This page shows how to send those steps to Synap, what Synap does with each one, and why each is worth the extra call.

Send traces when your agent uses tools or plans across steps, and runs as a long-lived process that can hold the [gRPC stream](/concepts/grpc-vs-rest). If your agent answers in one model call with no tools, its turns are enough; see [Conversations and transcripts](/capture/conversations). For history that already exists, see [Import existing history](/getting-started/bootstrap).

## Why traces help

Synap's anticipation agent prepares the context your agent will need on its next turn. Without traces it sees only the edges of a turn: what the user asked and what your agent replied. With them it also sees how the reply was reached: what your agent decided to check, which tool it called with which arguments, and what came back. The next question usually follows from that. A user who has been told their order shipped asks when it arrives, and the agent that saw the order lookup can prepare for that question before it is asked.

## Before you start

You need an open stream. There is no REST method for traces, so they can only travel on the stream that [`instance.listen()`](/sdk-reference/instance/listen) opens. Open it once per process.

Each trace carries the same ids as the turn it belongs to. Synap files a trace under its conversation, and drops a trace that has no `conversation_id`.

```python Python theme={null}
import uuid
from maximem_synap import MaximemSynapSDK

sdk = MaximemSynapSDK()  # reads SYNAP_API_KEY
await sdk.initialize()
await sdk.instance.listen()  # once per process

# The ids your turn events carry. On a B2C instance, leave customer_id out.
ids = {
    "conversation_id": str(uuid.uuid4()),
    "user_id": "user_789",
    "customer_id": "cust_456",
}
```

```typescript TypeScript theme={null}
import { randomUUID } from 'node:crypto';
import { SynapClient } from '@maximem/synap-js-sdk';

const sdk = new SynapClient(); // reads SYNAP_API_KEY
await sdk.initialize();
await sdk.instance.listen(); // once per process

// The ids your turn events carry. On a B2C instance, leave customer_id out.
const ids = {
  conversation_id: randomUUID(),
  user_id: 'user_789',
  customer_id: 'cust_456',
};
```

Your agent also reports its turns with [`send_message`](/sdk-reference/instance/send-message): a `user_message` when the user speaks and an `assistant_message` after it replies. The reply is what makes Synap act on the traces, so do not skip it.

## Send each kind of trace

### Reasoning

Send one reasoning step per call with [`record_thinking`](/sdk-reference/instance/record-thinking). This is the only trace that shows intent: a tool call says what your agent looked at, and a reasoning step says why. `step_index` orders the steps within a turn, and `thought_type` labels the kind of step, for example `"plan"`, `"reflect"`, `"tool_decision"` or `"self_correction"`.

```python Python theme={null}
await sdk.instance.record_thinking(
    content="The user asked about a refund. Check the order status before quoting the policy.",
    step_index=1,
    thought_type="plan",
    **ids,
)
```

```typescript TypeScript theme={null}
await sdk.instance.record_thinking({
  content: 'The user asked about a refund. Check the order status before quoting the policy.',
  step_index: 1,
  thought_type: 'plan',
  ...ids,
});
```

Send what your model provider gives you. Many providers return a summary of the reasoning rather than the reasoning itself, and some return nothing. A turn without reasoning is an ordinary turn; do not fill it with text you wrote yourself, because Synap would treat it as your agent's thinking.

### A tool call

Report each tool your agent invokes with [`record_tool_call`](/sdk-reference/instance/record-tool-call), before you run it. The tool name and its arguments say what your agent is about to need, which is often what the user asks about next. Give each call a `tool_call_id` and pass the same id with its result, so a turn with several tools in flight still pairs each result with the call that asked for it.

```python Python theme={null}
await sdk.instance.record_tool_call(
    "lookup_order",
    {"order_id": "A-1042"},
    tool_call_id="call_1",
    **ids,
)
```

```typescript TypeScript theme={null}
await sdk.instance.record_tool_call({
  tool_name: 'lookup_order',
  tool_args: { order_id: 'A-1042' },
  tool_call_id: 'call_1',
  ...ids,
});
```

Use this method rather than `send_message(event_type="tool_call")`. Every event carries both an event type and a role, and this method sets the two together, so they always agree.

### A tool's result

Report what the tool returned with [`record_tool_result`](/sdk-reference/instance/record-tool-result). The result is the material your agent built its reply from, so it is the strongest clue to what the user will ask next. The SDK also keeps it in a [local archive](#the-local-turn-archive) so that history search can find it after a compaction.

```python Python theme={null}
await sdk.instance.record_tool_result(
    {"status": "shipped", "eta": "2026-10-02"},
    tool_name="lookup_order",
    tool_call_id="call_1",
    **ids,
)
```

```typescript TypeScript theme={null}
await sdk.instance.record_tool_result({
  result: { status: 'shipped', eta: '2026-10-02' },
  tool_name: 'lookup_order',
  tool_call_id: 'call_1',
  ...ids,
});
```

The result travels in its own field, not as message content, because it is not something a person said. A string is sent as itself; anything else is sent as JSON.

> [!WARNING]
>
> A tool result is usually your customer's data: an order, an account, a record. It leaves your process when you send it. Send what your agent needs in order to be understood, not the whole row. Synap's anticipation agent reads only the first 2,000 characters of a result, and each event on the stream must fit in 4 MB.

### One turn, end to end

A small helper that wraps every tool call keeps the call, the result and their shared id together, so no tool runs unreported.

```python Python theme={null}
async def lookup_order(order_id: str) -> dict:
    return {"order_id": order_id, "status": "shipped", "eta": "2026-10-02"}

async def call_tool(name: str, args: dict, run):
    call_id = str(uuid.uuid4())
    await sdk.instance.record_tool_call(name, args, tool_call_id=call_id, **ids)
    result = await run(**args)
    await sdk.instance.record_tool_result(result, tool_name=name, tool_call_id=call_id, **ids)
    return result

await sdk.instance.send_message(
    content="Where is order A-1042?", role="user", event_type="user_message", **ids,
)
await sdk.instance.record_thinking(
    content="Look up the order before answering.", step_index=1, thought_type="plan", **ids,
)
order = await call_tool("lookup_order", {"order_id": "A-1042"}, lookup_order)
reply = f"Order A-1042 has shipped and arrives on {order['eta']}."
await sdk.instance.send_message(
    content=reply, role="assistant", event_type="assistant_message", **ids,
)
```

```typescript TypeScript theme={null}
async function lookupOrder(args: { order_id: string }) {
  return { order_id: args.order_id, status: 'shipped', eta: '2026-10-02' };
}

async function callTool<A extends Record<string, unknown>, R>(
  name: string,
  args: A,
  run: (args: A) => Promise<R>,
): Promise<R> {
  const callId = randomUUID();
  await sdk.instance.record_tool_call({ tool_name: name, tool_args: args, tool_call_id: callId, ...ids });
  const result = await run(args);
  await sdk.instance.record_tool_result({ result, tool_name: name, tool_call_id: callId, ...ids });
  return result;
}

await sdk.instance.send_message({
  content: 'Where is order A-1042?', role: 'user', event_type: 'user_message', ...ids,
});
await sdk.instance.record_thinking({
  content: 'Look up the order before answering.', step_index: 1, thought_type: 'plan', ...ids,
});
const order = await callTool('lookup_order', { order_id: 'A-1042' }, lookupOrder);
const reply = `Order A-1042 has shipped and arrives on ${order.eta}.`;
await sdk.instance.send_message({
  content: reply, role: 'assistant', event_type: 'assistant_message', ...ids,
});
```

## What Synap does with each

All three kinds of trace go to the same place: the working thread that Synap's anticipation agent keeps for the conversation.

* **They do not wake it.** A trace arrives while your agent is still working on the turn, and a bundle pushed then would arrive after your agent had already fetched. So traces join the thread, and the anticipation agent reads them when your `assistant_message` ends the turn.
* **They do not become memory.** Only `user_message` and `assistant_message` events are stored as conversation turns. Reasoning, tool calls and tool results are never compacted and never extracted into long-term memory. When your agent's reply states something a tool returned, that reply is stored as a turn like any other. For a fact that must be remembered on its own, use [`memories.create`](/sdk-reference/memories/create).
* **Their words are short-lived.** The thread keeps the conversation's 80 most recent events. It expires 30 minutes after the conversation's last event, and is deleted when the session ends.
* **Only their shape is kept longer.** Synap's anticipation debugging records keep the outline of each step for 30 days: the tool name, the call id, `step_index`, `thought_type`, and how many characters each field held. They do not keep the words of your reasoning, your tool arguments or your tool results.

## The local turn archive

The SDK keeps an archive of each conversation's turns on the machine it runs on: the turns your process recorded, and earlier turns it fetched from Synap. [`record_tool_result`](/sdk-reference/instance/record-tool-result) adds the tool's result to this archive before it sends the event, by calling `archive_tool_result`. The archived copy is never sent anywhere.

The reason is what a compaction leaves out. A compaction summarises what people said; the exact values a tool returned, such as an order number, a date or an amount, are the details a summary drops. Once a conversation has compacted, [`conversation.context.get_context_for_prompt`](/sdk-reference/conversation-context/get-context-for-prompt) searches the earlier turns in this archive (by default for the latest user message) and appends the best matches word for word after the recent messages, in `history_excerpts`. Archived tool results are part of that search, so a figure a tool returned many turns ago can still reach the prompt.

A result is archived as a turn with the role `tool`, written as `[tool result: lookup_order]` followed by the result as compact JSON (or the string itself), cut at 16,000 characters.

| | Python | JavaScript and TypeScript |
| - | - | - |
| Where | A SQLite file, `turns.db`, in the SDK's cache directory: `~/.synap/<client_id>/<instance_id>/` by default, or under your `storage_path`. In memory if the disk cache is off or the file cannot be opened. | In process memory. Gone when the process exits. |
| Limits | 200 conversations (least recently used dropped first); 5,000 turns and 8 MiB per conversation (oldest turns dropped first); a conversation idle for seven days is dropped; the file is kept under 200 MiB. | The same limits, in memory. |

> [!WARNING]
>
> The archive is a copy on your machine, so erasing a person in Synap cannot reach it. `cache.clear_user()` does not touch it either, because the archive is organised by conversation, not by user. When you [erase a person](/guides/erasure), remove the local copy too: shut the SDK down, then delete `turns.db` and its `turns.db-wal` and `turns.db-shm` files from the cache directory.

## Without a stream

`record_thinking`, `record_tool_call` and `record_tool_result` raise `ListeningNotActiveError` when `listen()` was never called or `stop_listening()` has run. While the stream is reconnecting they do not raise: events are buffered (up to 100 events or five minutes) and replayed when it returns. See [Event delivery](/concepts/event-delivery).

`record_tool_result` archives the result before it sends, so the local copy is kept even when the call raises. A long-running process that writes its turns over REST with [`record_message`](/sdk-reference/conversation/record-message) and never opens a stream can still keep tool results searchable by archiving them directly:

```python Python theme={null}
sdk.archive_tool_result(
    ids["conversation_id"],
    {"status": "shipped", "eta": "2026-10-02"},
    tool_name="lookup_order",
)
```

```typescript TypeScript theme={null}
sdk.archive_tool_result(
  ids.conversation_id,
  { status: 'shipped', eta: '2026-10-02' },
  { tool_name: 'lookup_order' },
);
```

Synap's framework integrations handle this for you: they open the stream themselves on the first turn and report reasoning, tool calls and tool results on it. When the stream cannot open, or you set `SYNAP_AUTO_LISTEN=false`, they skip the reports, write turns over REST, and still archive tool results locally.

## Common mistakes

* **A trace without `conversation_id`.** Synap cannot place it in a conversation and drops it. Use the same UUID as the turn.
* **No `assistant_message`.** The anticipation agent reads the traces when the reply arrives. A turn that never reports its reply leaves the traces unread.
* **Tool events built by hand with `send_message`.** You have to set the event type, the role and the tool fields yourself, and keep them consistent. `record_tool_call` and `record_tool_result` set all of them for you.
* **Several tools in flight without `tool_call_id`.** Three calls and three results arrive with nothing saying which result answers which call.
* **The whole record as a result.** Send the fields your agent used. The anticipation agent reads the first 2,000 characters, and the rest only adds data that leaves your process.
* **Expecting a trace to become a memory.** Traces are hints for the next turn. Store lasting facts as turns or with `memories.create`.
* **Opening a stream per request to send traces.** A serverless handler exits before a bundle can arrive. See [gRPC vs REST](/concepts/grpc-vs-rest#which-to-choose).

## Related

* [instance.record\_thinking](/sdk-reference/instance/record-thinking), [instance.record\_tool\_call](/sdk-reference/instance/record-tool-call), [instance.record\_tool\_result](/sdk-reference/instance/record-tool-result): the three trace methods in full.
* [instance.send\_message](/sdk-reference/instance/send-message): the turn events and the full event table.
* [instance.listen](/sdk-reference/instance/listen): opening the stream.
* [Real-time anticipation](/concepts/real-time-anticipation): what Synap does with everything the stream carries.
* [Erasing a person](/guides/erasure): deletion in Synap, alongside the local copy described above.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.