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

# Build an agent

> From a bare model to tools, secrets, MCP servers, memory, traces, revisions, and the audit log — one addition at a time.

## 1. Run a simple agent

A model, a system prompt, a user prompt. No tools. The model answers from what
it knows.

<CodeGroup>
  ```python Python theme={null}
  import os

  from albus_sdk import Albus

  with Albus(api_key=os.environ["ALBUS_API_KEY"]) as albus:
      response = albus.sessions.run_session(
          id="support-1234",
          agent_name="support-triage",
          user_prompt="Customer says login fails after the password reset. Triage it.",
          agent={
              "model": {"name": "gemini-3.6-flash"},
              "system_prompt": "You triage support tickets. Be terse.",
          },
          wait_timeout_seconds=120,
      )
      print(response.result.message.content)
  ```

  ```typescript TypeScript theme={null}
  import { Albus } from "@albus-ts/sdk";

  const albus = new Albus({
    security: { apiKey: process.env.ALBUS_API_KEY ?? "" },
  });

  const response = await albus.sessions.runSession({
    id: "support-1234",
    waitTimeoutSeconds: 120,
    body: {
      agentName: "support-triage",
      userPrompt: "Customer says login fails after the password reset. Triage it.",
      agent: {
        model: { name: "gemini-3.6-flash" },
        systemPrompt: "You triage support tickets. Be terse.",
      },
    },
  });
  console.log(response.result.message?.content);
  ```
</CodeGroup>

Three names. `id` is the **session**: Albus creates it on the first run and
continues it on every later run, so the next prompt to `support-1234` sees this
one. `agent_name` groups runs into an **agent**; the `agent` object is its
configuration. Albus supplies the model until you [bring your own
key](/guides/bring-your-own-key). The current date and time are prepended to
`system_prompt`; omit it and a server default applies.

## 2. Make the run retry-safe

Add an `invocation_key`. Retrying with the same key re-attaches to the same run
instead of starting a new one, so a network failure, a `504` from a long wait,
or a lost response costs nothing.

<CodeGroup>
  ```python Python focus={9} theme={null}
  import os

  from albus_sdk import Albus

  with Albus(api_key=os.environ["ALBUS_API_KEY"]) as albus:
      response = albus.sessions.run_session(
          id="support-1234",
          agent_name="support-triage",
          invocation_key="support-1234-triage-1",  # [!code ++]
          user_prompt="Customer says login fails after the password reset. Triage it.",
          agent={
              "model": {"name": "gemini-3.6-flash"},
              "system_prompt": "You triage support tickets. Be terse.",
          },
          wait_timeout_seconds=120,
      )
      print(response.result.message.content)
  ```

  ```typescript TypeScript focus={9} theme={null}
  import { Albus } from "@albus-ts/sdk";

  const albus = new Albus({
    security: { apiKey: process.env.ALBUS_API_KEY ?? "" },
  });

  const response = await albus.sessions.runSession({
    id: "support-1234",
    invocationKey: "support-1234-triage-1", // [!code ++]
    waitTimeoutSeconds: 120,
    body: {
      agentName: "support-triage",
      userPrompt: "Customer says login fails after the password reset. Triage it.",
      agent: {
        model: { name: "gemini-3.6-flash" },
        systemPrompt: "You triage support tickets. Be terse.",
      },
    },
  });
  console.log(response.result.message?.content);
  ```
</CodeGroup>

Over HTTP the key is the `Idempotency-Key` header. It is unique in your
organization and is how you read the run back later: `GET
/traces/{invocation_key}`. The response always returns the effective key in
`Idempotency-Key`, including when the server generated it.

| Situation                                                 | Result                                                                |
| --------------------------------------------------------- | --------------------------------------------------------------------- |
| Same key, identical body                                  | Re-attaches to that run and returns its current state                 |
| Same key, different body                                  | `409`                                                                 |
| New key while the session is still running another prompt | `423` — a session runs one prompt at a time                           |
| No key                                                    | A fresh run that cannot be retried safely; the server generates a key |

**Session id.** Match `^[0-9a-zA-Z._:-]+$`, 2–100 characters. Use an id you
already have: the ticket (`support-1234`), the pull request (`pr-987`), the job
(`nightly:2026-08-11`). Then you can find the session without storing a
mapping, and the conversation continues where the work does.

**Invocation key.** Derive it from the thing that must happen exactly once: the
id of the event that triggered the run, or the session id plus a turn counter
(`support-1234-triage-1`). Do not use a timestamp or a random value generated
on each retry; each one starts a new run, which is exactly what the key is
meant to prevent. The same key re-attaches even to a failed run. Reuse the key
to recover a lost response; use a new key to run again.

## 3. Add web search and a terminal

Add the two built-in tools. The agent can now search the web and run commands
in a persistent Linux sandbox, and the model decides when to call each. Runs
take longer and answers are grounded in what the tools return.

<CodeGroup>
  ```python Python focus={14} theme={null}
  import os

  from albus_sdk import Albus

  with Albus(api_key=os.environ["ALBUS_API_KEY"]) as albus:
      response = albus.sessions.run_session(
          id="support-1234",
          agent_name="support-triage",
          invocation_key="support-1234-triage-2",
          user_prompt="Check whether our status page reports an auth incident today.",
          agent={
              "model": {"name": "gemini-3.6-flash"},
              "system_prompt": "You triage support tickets. Be terse.",
              "tools": {"web_search": {}, "terminal": {}},  # [!code ++]
          },
          wait_timeout_seconds=300,
      )
      print(response.result.message.content)
  ```

  ```typescript TypeScript focus={17} theme={null}
  import { Albus } from "@albus-ts/sdk";

  const albus = new Albus({
    security: { apiKey: process.env.ALBUS_API_KEY ?? "" },
  });

  const response = await albus.sessions.runSession({
    id: "support-1234",
    invocationKey: "support-1234-triage-2",
    waitTimeoutSeconds: 300,
    body: {
      agentName: "support-triage",
      userPrompt: "Check whether our status page reports an auth incident today.",
      agent: {
        model: { name: "gemini-3.6-flash" },
        systemPrompt: "You triage support tickets. Be terse.",
        tools: { webSearch: {}, terminal: {} }, // [!code ++]
      },
    },
  });
  console.log(response.result.message?.content);
  ```
</CodeGroup>

`tools` is an allowlist. Include a tool's block to offer it; leave it out and
the model never learns the tool exists. A prompt that says "do not run
commands" is not a substitute: the model can ignore instructions, but it
cannot call a tool it was never given. `web_search` appears in traces as
`WEB_SEARCH`, `terminal` as `TERMINAL`. Details in [Built-in
tools](/guides/built-in-tools).

Tool calls take steps. Set `max_steps` to cap how many model steps a run may
take before it stops.

## 4. Add a secret and an MCP server

Give the agent your own tools through an MCP server, authenticated with a
secret. The agent can now act on your systems — here, search GitHub issues —
and the credential never appears in the configuration, the audit log, or a
revision.

Store the credential first. Secrets are administration, so the CLI is the
right tool. The value is the full header value the server expects, `Bearer`
included:

```bash theme={null}
printf 'Bearer %s' "$GITHUB_TOKEN" | albus secrets create integrations/github/token
```

Reads return the value masked to its last three characters; nothing returns the
full value. Names are `/`-separated segments of `[a-zA-Z0-9_-]`. Group them
like files.

Reference the secret from the agent as `albus.sh/secrets/<name>`:

<CodeGroup>
  ```python Python focus={15-24} theme={null}
  import os

  from albus_sdk import Albus

  with Albus(api_key=os.environ["ALBUS_API_KEY"]) as albus:
      response = albus.sessions.run_session(
          id="support-1234",
          agent_name="support-triage",
          invocation_key="support-1234-triage-3",
          user_prompt="Is this login failure a known issue? Cite issue numbers.",
          agent={
              "model": {"name": "gemini-3.6-flash"},
              "system_prompt": "You triage support tickets. Be terse.",
              "tools": {"web_search": {}, "terminal": {}},
              "mcp_servers": [  # [!code ++]
                  {  # [!code ++]
                      "name": "github",  # [!code ++]
                      "url": "https://api.githubcopilot.com/mcp/",  # [!code ++]
                      "headers": {  # [!code ++]
                          "Authorization": "albus.sh/secrets/integrations/github/token"  # [!code ++]
                      },  # [!code ++]
                      "allowed_tools": ["search_issues", "get_issue"],  # [!code ++]
                  }  # [!code ++]
              ],  # [!code ++]
          },
          wait_timeout_seconds=300,
      )
      print(response.result.message.content)
  ```

  ```typescript TypeScript focus={18-27} theme={null}
  import { Albus } from "@albus-ts/sdk";

  const albus = new Albus({
    security: { apiKey: process.env.ALBUS_API_KEY ?? "" },
  });

  const response = await albus.sessions.runSession({
    id: "support-1234",
    invocationKey: "support-1234-triage-3",
    waitTimeoutSeconds: 300,
    body: {
      agentName: "support-triage",
      userPrompt: "Is this login failure a known issue? Cite issue numbers.",
      agent: {
        model: { name: "gemini-3.6-flash" },
        systemPrompt: "You triage support tickets. Be terse.",
        tools: { webSearch: {}, terminal: {} },
        mcpServers: [ // [!code ++]
          { // [!code ++]
            name: "github", // [!code ++]
            url: "https://api.githubcopilot.com/mcp/", // [!code ++]
            headers: { // [!code ++]
              Authorization: "albus.sh/secrets/integrations/github/token", // [!code ++]
            }, // [!code ++]
            allowedTools: ["search_issues", "get_issue"], // [!code ++]
          }, // [!code ++]
        ], // [!code ++]
      },
    },
  });
  console.log(response.result.message?.content);
  ```
</CodeGroup>

Albus connects to the server for the run, discovers its tools, offers them to
the model, and dispatches the calls. Nothing runs on your machine. The server
must speak **Streamable HTTP** at a public `http(s)` URL; `stdio` servers need
an HTTP front.

| Field           | Meaning                                                                                                                                                     |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`          | Your alias. It prefixes the tool names the model sees: `search_issues` becomes `github__search_issues`. Unique in the configuration; must not contain `__`. |
| `url`           | The server's Streamable HTTP endpoint.                                                                                                                      |
| `headers`       | Sent to the server. A value of the form `albus.sh/secrets/<name>` is resolved when the run starts; anything else is sent literally.                         |
| `allowed_tools` | The server's tools the model may call, by the server's own names. Omit to allow all of them.                                                                |

References resolve when the run starts, not when the request is validated. A
reference to a missing secret fails the run, not the request, so create the
secret first. Rotate with `albus secrets update`: references keep working, the
next run picks up the new value, and the agent configuration does not change.

## 5. Configure memory

Add a memory group. What the agent learns now outlives the session: every run
configured with the same `group` reads the same memories, whichever session it
runs in, and writes new ones at the points you list.

<CodeGroup>
  ```python Python focus={25-28} theme={null}
  import os

  from albus_sdk import Albus

  with Albus(api_key=os.environ["ALBUS_API_KEY"]) as albus:
      response = albus.sessions.run_session(
          id="support-1234",
          agent_name="support-triage",
          invocation_key="support-1234-triage-4",
          user_prompt="Is this login failure a known issue? Cite issue numbers.",
          agent={
              "model": {"name": "gemini-3.6-flash"},
              "system_prompt": "You triage support tickets. Be terse.",
              "tools": {"web_search": {}, "terminal": {}},
              "mcp_servers": [
                  {
                      "name": "github",
                      "url": "https://api.githubcopilot.com/mcp/",
                      "headers": {
                          "Authorization": "albus.sh/secrets/integrations/github/token"
                      },
                      "allowed_tools": ["search_issues", "get_issue"],
                  }
              ],
              "memory": {  # [!code ++]
                  "group": "support",  # [!code ++]
                  "generation": ["agent", "end_of_invocation"],  # [!code ++]
              },  # [!code ++]
          },
          wait_timeout_seconds=300,
      )
      print(response.result.message.content)
  ```

  ```typescript TypeScript focus={28-31} theme={null}
  import { Albus } from "@albus-ts/sdk";

  const albus = new Albus({
    security: { apiKey: process.env.ALBUS_API_KEY ?? "" },
  });

  const response = await albus.sessions.runSession({
    id: "support-1234",
    invocationKey: "support-1234-triage-4",
    waitTimeoutSeconds: 300,
    body: {
      agentName: "support-triage",
      userPrompt: "Is this login failure a known issue? Cite issue numbers.",
      agent: {
        model: { name: "gemini-3.6-flash" },
        systemPrompt: "You triage support tickets. Be terse.",
        tools: { webSearch: {}, terminal: {} },
        mcpServers: [
          {
            name: "github",
            url: "https://api.githubcopilot.com/mcp/",
            headers: {
              Authorization: "albus.sh/secrets/integrations/github/token",
            },
            allowedTools: ["search_issues", "get_issue"],
          },
        ],
        memory: { // [!code ++]
          group: "support", // [!code ++]
          generation: ["agent", "end_of_invocation"], // [!code ++]
        }, // [!code ++]
      },
    },
  });
  console.log(response.result.message?.content);
  ```
</CodeGroup>

`generation` says when memories are written: `end_of_invocation` after the
run answers, `agent` whenever the agent decides mid-run that something is worth
keeping. Scope a group however you like: one per customer, per team, per agent.
Without `memory`, a session still remembers its own last 1000 messages; memory
carries knowledge *between* sessions. Read and prune a group with `GET
/memories?group=support` and `DELETE /memories?group=support`, or in the
console under **Memories**. More in [Built-in
tools](/guides/built-in-tools#memory).

## 6. Read the trace and iterate

Every run is recorded as a **trace**: the run, each step, the model call in
each step, and the tool calls it requested, as spans with inputs, outputs,
timings, and token usage. The trace is how you find out what the agent did, and
the fastest loop for fixing a prompt or a tool. Point a coding agent at it.

```bash theme={null}
albus traces get support-1234-triage-4
```

```text theme={null}
inv4                     invocation             SUCCEEDED
inv4.attempt1.step1      step                   SUCCEEDED
inv4.attempt1.step1.model model_call  gemini-3.6-flash SUCCEEDED
inv4.attempt1.step1.tool1 tool_call   github__search_issues SUCCEEDED
inv4.attempt1.step2      step                   SUCCEEDED
inv4.attempt1.step2.model model_call  gemini-3.6-flash SUCCEEDED
```

Span ids read as a path: `inv4` is the fourth run of the session,
`inv4.attempt1.step1.tool1` the first tool call the first step requested. A
`model_call` span's `input` is exactly what the model was given and its
`output` what it said, including the tool calls it asked for. A `tool_call`
span's `input` is the arguments and its `output` the tool's result. Long
payloads are cut with an `[omitted: …]` marker and carry the full byte count
and SHA-256.

Questions to ask a trace:

* **Did the agent call the tool you expected?** If `WEB_SEARCH` fires where
  `github__search_issues` should have, the tool's description (from the MCP
  server) or your `system_prompt` did not make the choice obvious. Name the
  tool and when to use it in the prompt, or tighten `allowed_tools`.
* **Did the tool give the agent what it needed?** Read the `tool_call` output.
  A tool that returns 40 KB of JSON for a one-line answer wastes steps and
  tokens. A tool that returns an error the model cannot act on gets retried.
  Fix the tool, not the prompt.
* **Where did the steps go?** Count `step` spans and read `usage` on each
  `model_call`. The same tool called in a loop with slightly different
  arguments means the task is underspecified. A `max_steps` stop means the
  budget or the prompt is wrong.
* **Why did it fail?** A `FAILED` span carries `error`; the run carries a
  `failure`. A retried run has more than one attempt. Pass `--attempts all` to
  see the superseded ones; their tool calls still had their effects.

`albus traces list` shows runs across sessions, newest first, filtered by
`--agent-name`, `--agent-revision`, `--status`, `--session`, `--since`, and
`--until`. `--status FAILED` on an agent name is the first thing to look at
when a job goes wrong. Spans are kept for 90 days. The console shows the same
under **Traces**, one run per `/traces/<invocation_key>`.

Change the prompt or a tool, run again with a **new** invocation key, compare
the traces. Which raises what "change" means.

## 7. Every change is a revision

You never create an agent. You name one on a run and Albus records the
configuration. Every distinct `agent` object under one `agent_name` is a
**revision**, identified by a hash of the configuration. Each step in this
guide created a new revision of `support-triage`; running the same
configuration again reuses the existing one.

<CodeGroup>
  ```python Python theme={null}
  agent = albus.agents.get_agent(name="support-triage")
  for revision in agent.revisions:
      print(revision.revision, revision.created_at)

  config = albus.agents.get_agent_revision(
      name="support-triage", revision=agent.revisions[0].revision
  )
  ```

  ```typescript TypeScript theme={null}
  const agent = await albus.agents.getAgent({ name: "support-triage" });
  for (const revision of agent.revisions) {
    console.log(revision.revision, revision.createdAt);
  }

  const config = await albus.agents.getAgentRevision({
    name: "support-triage",
    revision: agent.revisions[0].revision,
  });
  ```
</CodeGroup>

`get_agent` returns the current revision's full configuration plus every
revision, newest first. `get_agent_revision` returns one revision's exact
model, prompt, tools, MCP servers, and memory. In the console, **Agents** lists
them; each revision lives at `/agents/<name>/revisions/<revision>`.

Every session response carries `session.agent_revision`. Every trace and audit
event carries `agent_revision`. `GET /traces?agent_name=…&agent_revision=…`
lists every run of one configuration. That is the link from an answer you do
not like back to the configuration that produced it.

Keep `agent_name` stable per job (`support-triage`, `pr-reviewer`), not per
run or per customer, so the revision list is the agent's history. Per-run
identity belongs in the session id. Record the revision next to your own
results. A revision is configuration only; credentials are references, so
rotating a secret does not create one.

## 8. The audit log

The audit log is the security record of a session: an immutable, time-ordered
account of every run, model call, and tool call, arguments and results
included, and how each run ended. A trace is for understanding and iterating.
The audit log answers *what did this agent do, on whose request, with which
configuration*.

```bash theme={null}
albus sessions audit support-1234 --limit 50
albus sessions audit support-1234 --after "$cursor"
```

| `type`                 | What it records                                                |
| ---------------------- | -------------------------------------------------------------- |
| `agent_invocation`     | The request that started the run                               |
| `agent_step`           | One step, how it ended, and how long it took                   |
| `model_call`           | A model call and the tool calls it requested                   |
| `tool_call`            | An executed tool call: arguments, response, and MCP server URL |
| `invocation_succeeded` | The run produced its reply                                     |
| `invocation_failed`    | The run failed, with the failure kind and detail               |
| `harness_exit`         | The execution environment exited                               |

Events created before the rename to `invocation_*` keep their old `run_*`
names. Every event carries `session_id`, the `invocation_key` it belongs to,
the `agent_revision` that ran, `event_time`, and a `payload` shaped by its
`type`. Model output, tool output, and tool arguments are stored up to 32 KiB
each, with the complete value's byte count, SHA-256 digest, and a truncation
flag (`contentBytes`, `contentSha256`, `contentTruncated`), so a stored value
can be checked against the original. Secrets never appear: references resolve
at run time and the values are not written anywhere the API returns.

```bash theme={null}
# Which tools did the agent call, against which servers, with what arguments?
albus sessions audit support-1234 | jq '.events[]
  | select(.type=="tool_call")
  | {tool: .payload.name, args: .payload.args, server: .payload.mcpServerUrl}'

# Which configuration was running?
albus sessions audit support-1234 | jq -r '.events[0].agent_revision'

# Why did a run fail?
albus sessions audit support-1234 | jq '.events[] | select(.type=="invocation_failed" or .type=="run_failed")'
```

Page with `--after`/`--limit` (default 100), passing the response's
`next_cursor` as the next `after`. The log is per session. For the same events
across sessions, filtered by agent, revision, status, or time, use
[traces](#6-read-the-trace-and-iterate).

## Clean up

```bash theme={null}
albus sessions delete support-1234
albus secrets delete integrations/github/token
```

## Where next

* [Bring your own key](/guides/bring-your-own-key) — run on your own model
  provider account.
* [Built-in tools](/guides/built-in-tools) — web search, the terminal, and
  memory in detail.
* [Errors](/reference/errors) — every status a run can return, and the fix.
