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

# ConditioningPack and VoiceProfile

> A system prompt, a voice hint and an org digest, composed on demand so your own model reasons and sounds like the subject.

You already have a model you like. What you do not have is a way to make it start a session knowing who it is working with: how they frame problems, which mode they are in this week, and how they write. So every session starts from a blank slate, and the user re-explains themselves.

A ConditioningPack is that context, composed on demand and shaped to drop straight into a system prompt. It works with any model, because it is text.

Get one from [`POST /v1/condition/session`](/api-reference/condition/post-condition-session).

```json 200 application/json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "systemPrompt": "You are pair-programming with 8f14e45f, working in `/repos/acme-platform` on branch `feat/ingest-batching`.\n\nUser voice profile: ~39 words/message avg; casual register.\n\nCurrent cognitive basin: 'rapid_implementation'.\n\nRecent prompts from you (most recent first):\n  - \"why is the batch endpoint returning 202 with an empty results array\"\n  - \"drop the retry wrapper, the queue already redelivers\"\n\nRecent org activity:\n  - 3d9a1c77: 4 session(s), last 2026-07-14T08:02:11Z\n\nProceed with the user's stated intent. Ask clarifying questions when context is thin.",
  "userVoiceHint": "~39 words/message avg; casual register",
  "orgRecentContext": [
    { "user": "3d9a1c77", "last_active": "2026-07-14T08:02:11Z", "session_count": 4 }
  ],
  "expiresAt": "2026-07-14T10:12:03Z",
  "usage": { "readUnits": 1, "llmInputTokens": null, "llmOutputTokens": null }
}
```

## What it is

**ConditioningPack**, everything an agent needs to answer as this subject, assembled at call time (in practice, a system prompt plus the two ingredients that went into it, so you can use the whole thing or pick it apart).

**VoiceProfile**, how the subject writes, measured from their own words (in practice, four numbers: message length, formality, technical density, and how much evidence is behind them).

**Basin**, the mode of thinking the subject is currently in (in practice, the cluster their recent positions fall into). See [ManifoldState](/concepts/manifold).

The pack is composed, not stored. Each call reads the current identity state and builds fresh text, which is why there is no route to fetch "the pack" by id and why `expiresAt` exists.

## Where it comes from

Four reads go into one prompt.

| Ingredient     | Source                                              | When it is absent                                |
| -------------- | --------------------------------------------------- | ------------------------------------------------ |
| Who and where  | The `cwd` and `gitBranch` you send                  | Never; you supply them                           |
| Voice          | The subject's measured writing style                | Omitted until there is evidence behind it        |
| Current basin  | The identity manifold's dominant basin              | Omitted before the first discovery pass          |
| Recent context | The subject's own recent prompts, plus org activity | Omitted on a quiet window or a single-member org |

Every ingredient degrades independently. A brand-new subject still gets a usable prompt, just a thinner one, rather than an error or a fabricated profile.

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  curl -X POST https://api.get-exo.com/v1/condition/session \
    -H "X-Exo-API-Key: $EXO_KEY" \
    -H "X-Exo-Subject: customer_6412" \
    -H "Content-Type: application/json" \
    -d '{
      "userId": "customer_6412",
      "orgId": "0d5b7c92-3a41-4e88-9b02-6f7c1d4a5e30",
      "cwd": "/repos/acme-platform",
      "gitBranch": "feat/ingest-batching",
      "recentOrgActivityHours": 24
    }'
  ```

  ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  import os, requests

  r = requests.post(
      "https://api.get-exo.com/v1/condition/session",
      headers={
          "X-Exo-API-Key": os.environ["EXO_KEY"],
          "X-Exo-Subject": "customer_6412",
      },
      json={
          "userId": "customer_6412",
          "orgId": "0d5b7c92-3a41-4e88-9b02-6f7c1d4a5e30",
          "cwd": "/repos/acme-platform",
          "gitBranch": "feat/ingest-batching",
          "recentOrgActivityHours": 24,
      },
  )
  r.raise_for_status()
  pack = r.json()

  # Drop it into any model.
  messages = [{"role": "system", "content": pack["systemPrompt"]}]
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  const r = await fetch("https://api.get-exo.com/v1/condition/session", {
    method: "POST",
    headers: {
      "X-Exo-API-Key": process.env.EXO_KEY!,
      "X-Exo-Subject": "customer_6412",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      userId: "customer_6412",
      orgId: "0d5b7c92-3a41-4e88-9b02-6f7c1d4a5e30",
      cwd: "/repos/acme-platform",
      gitBranch: "feat/ingest-batching",
      recentOrgActivityHours: 24,
    }),
  });
  if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
  const pack = await r.json();

  const messages = [{ role: "system", content: pack.systemPrompt }];
  ```
</CodeGroup>

`userId` and `orgId` are required by the request body but they are advisory provenance only. The API key decides which organization and which identity the call acts as, and the subject selector is the `X-Exo-Subject` header. If the body disagrees with the credential, the credential wins.

## Field reference

| Field                                           | Type            | What it means                                                                                                                                         |
| ----------------------------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `systemPrompt`                                  | string          | The composed prompt. Always present.                                                                                                                  |
| `userVoiceHint`                                 | string or null  | A short style directive. Null until the subject has written enough for a voice profile to exist.                                                      |
| `orgRecentContext`                              | array           | Recent activity by members of the org, each entry carrying `user`, `last_active` and `session_count`. Empty on a single-member org or a quiet window. |
| `expiresAt`                                     | date-time       | A freshness bound, not a hard deadline.                                                                                                               |
| `usage.readUnits`                               | integer         | One unit per compose call.                                                                                                                            |
| `usage.llmInputTokens`, `usage.llmOutputTokens` | integer or null | Null today: composition is database reads, with no model call involved.                                                                               |

`expiresAt` is worth reading carefully. The pack does not stop working when it passes: the text stays valid. What it tells you is that the identity underneath may have moved, so it is time to recompose. Cache until then, and recompose after, rather than treating it as an expiry you must handle.

## The VoiceProfile, and its maturity gate

The voice profile is returned on the identity view, [`GET /v1/recall/identity`](/api-reference/recall/get-recall):

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "formalityLevel": 0.24,
  "avgMessageLengthWords": 38.6,
  "technicalDepth": 0.71,
  "evidenceCount": 412
}
```

| Field                   | Range   | What it means                                             |
| ----------------------- | ------- | --------------------------------------------------------- |
| `formalityLevel`        | 0 to 1  | 0 is casual, 1 is formal.                                 |
| `avgMessageLengthWords` | number  | Mean length of the subject's own messages.                |
| `technicalDepth`        | 0 to 1  | 0 is plain vocabulary, 1 is dense.                        |
| `evidenceCount`         | integer | How many of their messages the profile was measured from. |

**The whole object is withheld while `evidenceCount` is zero.** You get null, not a profile of defaults. That is deliberate: a confidently wrong voice is worse than no voice, because a model told to write casually for someone who writes formally is more jarring than a model with no instruction at all.

The same gate governs `userVoiceHint` on the pack. Null hint means not enough evidence yet, and the right response is to leave your model's default voice alone rather than substituting a guess.

## What conditioning actually changes

Illustrative, not measured. The same question, asked of the same model, with and without the pack in the system slot.

Question: *should we add a retry wrapper around the batch ingest call?*

**Unconditioned**

> There are several approaches to retry handling. You could implement exponential backoff with jitter, use a circuit breaker pattern, or rely on your queue's built-in redelivery if one is available. The right choice depends on your idempotency guarantees and your latency budget. Would you like me to walk through the tradeoffs of each?

**Conditioned**

> No. The queue already redelivers, and you told me last week you had pulled a retry wrapper out for exactly that reason. Adding one back gives you two retry layers with different backoff, which is how you get duplicate jobs under load. Send `Idempotency-Key` on the batch instead and let redelivery do the work.

The difference is not tone. The conditioned answer commits to a position, references the subject's own prior decision, and skips the menu of options, because the prompt carried their basin, their recent prompts and their voice. That is what the pack buys: less context re-established per session, and answers shaped like the ones this person actually wants.

## The scaffold variant

[`POST /v1/condition/scaffold`](/api-reference/condition/post-condition-scaffold) is for agents that assemble their own prompts and want the identity header only, wrapped around a single prompt rather than composed into a full system message.

```json 200 application/json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "scaffold": "[user=8f14e45f]\n\nRewrite this handler to stream instead of buffering.\n\n---\nRecent session summary:\nWorked through the batch accept path and the 202 envelope.",
  "focalNodes": [],
  "usage": { "readUnits": 1, "llmInputTokens": null, "llmOutputTokens": null }
}
```

<Warning>
  `focalNodes` is **always empty in v1**. The scaffold is composed from identity, not from a retrieval pass, so there are no focal nodes to report yet. If you need the nodes behind an answer, read `cognition.focalNodeIds` from [`POST /v1/retrieve`](/api-reference/retrieve/post-retrieve). Treat a future non-empty list as additive.
</Warning>

<AccordionGroup>
  <Accordion title="Why is userVoiceHint null?" icon="circle-question">
    The subject has not written enough for a voice profile to exist, so `evidenceCount` is still zero and the profile is withheld entirely rather than defaulted. Ingest more of their own writing, not agent narration about them: the profile is measured from the subject's messages.
  </Accordion>

  <Accordion title="Why is the basin missing from my system prompt?" icon="circle-question">
    The identity manifold has not run a discovery pass for this subject yet, so there is no basin to name. The prompt is composed from whatever exists, so it comes back without that line rather than with a placeholder. See [ManifoldState](/concepts/manifold) for what a pass needs.
  </Accordion>

  <Accordion title="How often should I recompose?" icon="circle-question">
    Once per session is the intended shape: compose at session start, inject, and reuse for the session. `expiresAt` is the signal for a long-lived process. Recomposing per message spends read units for text that has not changed, since composition reads stored state rather than recomputing identity.
  </Accordion>

  <Accordion title="Which scope does this need?" icon="circle-question">
    Both condition routes are read-shaped and take the `read` scope. They compose from stored state and write nothing, which is also why they return 200 synchronously rather than a job id: an agent's session-start hook needs the text immediately.
  </Accordion>

  <Accordion title="Does this work with models other than Claude?" icon="circle-question">
    Yes. The pack is plain text with no model-specific formatting. Put `systemPrompt` in whatever your provider calls the system slot. One identity, any model.
  </Accordion>
</AccordionGroup>

## Limits

* `focalNodes` on the scaffold route is always empty in v1.
* `usage.llmInputTokens` and `usage.llmOutputTokens` are null, because composition makes no model call.
* There is no route that returns a previously composed pack. Packs are built per call and not stored.
* `orgRecentContext` identifies members by internal id, not by name or email. Resolve names through [`GET /v1/team/members`](/api-reference/team/list-team-members), which needs an admin key.
* The voice profile has four measurements and no free-text style description.

## Next

<Columns cols={2}>
  <Card title="Conditioning your own agent" icon="wand-sparkles" href="/guides/conditioning">
    Wiring the pack into a session, end to end.
  </Card>

  <Card title="Compose a session prompt" icon="code" href="/api-reference/condition/post-condition-session">
    Request fields, the usage block and the scope it needs.
  </Card>
</Columns>
